@reddoorla/maintenance 0.36.1 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.js +1 -0
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +1 -0
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/forms/index.d.ts +32 -1
- package/dist/forms/index.js +53 -0
- package/dist/forms/index.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/audits/util/spawn.ts","../src/audits/deps.ts","../src/util/site.ts","../src/configs/baseline-versions.ts","../src/audits/deps-outdated.ts","../src/audits/lint.ts","../src/audits/security.ts","../src/audits/lighthouse.ts","../src/configs/lighthouse.ts","../src/audits/util/site-config.ts","../src/util/free-port.ts","../src/audits/a11y.ts","../src/configs/playwright-a11y.ts","../src/audits/index.ts","../src/recipes/sync-configs.ts","../src/recipes/sync-configs/templates.ts","../src/recipes/sync-configs/gitignore.ts","../src/util/git.ts","../src/recipes/_with-recipe.ts","../src/recipes/bump-deps.ts","../src/recipes/svelte-5/index.ts","../src/util/pkg.ts","../src/recipes/svelte-5/step-bump-versions.ts","../src/recipes/svelte-5/step-svelte-config.ts","../src/recipes/svelte-5/step-svelte-migrate.ts","../src/recipes/svelte-5/step-tailwind-upgrade.ts","../src/recipes/svelte-5/step-gotchas.ts","../src/recipes/svelte-5/codemods/on-event-to-handler.ts","../src/recipes/svelte-5/codemods/dollar-props.ts","../src/util/svelte-source.ts","../src/recipes/svelte-5/codemods/dollar-restprops.ts","../src/recipes/svelte-5/codemods/state-effect-sync.ts","../src/recipes/svelte-5/codemods/dollar-props-class.ts","../src/recipes/svelte-5/codemods/legacy-reactive.ts","../src/recipes/svelte-5/step-verify.ts","../src/recipes/svelte-5/step-summary.ts","../src/recipes/svelte-codemods.ts","../src/recipes/convert-to-pnpm.ts","../src/recipes/convert-to-pnpm/script-rewrites.ts","../src/recipes/onboard.ts","../src/util/self-version.ts","../src/recipes/a11y-fixtures-page/index.ts","../src/recipes/a11y-fixtures-page/template.ts","../src/recipes/init.ts","../src/recipes/index.ts","../src/inventory/local.ts","../src/inventory/json.ts","../src/util/url.ts","../src/reports/airtable/websites.ts","../src/inventory/airtable.ts","../src/reports/draft.ts","../src/reports/render.ts","../src/reports/copy.ts","../src/reports/maintenance-email/assets/index.ts","../src/util/html.ts","../src/reports/maintenance-email/template.ts","../src/reports/launch-email/template.ts","../src/reports/airtable/reports.ts","../src/reports/airtable/attachments.ts","../src/reports/ga/config.ts","../src/util/credentials.ts","../src/reports/ga/client.ts","../src/reports/search/client.ts","../src/reports/airtable/client.ts","../src/reports/maintenance-email/header-image.ts","../src/reports/send/resend.ts","../src/reports/send/idempotency.ts","../src/reports/send/orchestrate.ts","../src/reports/due.ts","../src/dashboard/relative-time.ts","../src/dashboard/render.ts","../src/dashboard/onboarding.ts","../src/dashboard/fleet-render.ts","../src/dashboard/basic-auth.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { StringDecoder } from \"node:string_decoder\";\n\nexport type SpawnResult = { code: number; stdout: string; stderr: string };\n\nexport type SpawnOptions = {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n timeoutMs?: number;\n /** When true, the child inherits stdout/stderr so the user sees live\n * progress (useful for long-running `pnpm up` / `npm install`). The\n * returned `stdout` and `stderr` will be empty strings in that case. */\n streaming?: boolean;\n};\n\nexport type SpawnFn = (\n cmd: string,\n args: readonly string[],\n opts?: SpawnOptions,\n) => Promise<SpawnResult>;\n\ntype KillFn = (pid: number, signal: NodeJS.Signals | number) => void;\n\n/** Construction-time knobs, separated from per-call {@link SpawnOptions} mainly\n * so tests can inject deterministic `spawnImpl`/`killImpl` and a tiny grace. */\nexport type SpawnInternals = {\n spawnImpl?: typeof spawn;\n killImpl?: KillFn;\n /** Delay after SIGTERM before escalating to SIGKILL on a timeout (default 5s). */\n killGraceMs?: number;\n /** Cap on captured stdout/stderr length so a runaway child can't OOM the CLI. */\n maxOutputBytes?: number;\n};\n\nconst TRUNCATION_MARKER = \"\\n…[output truncated]\";\n\nexport function makeSpawn(internals: SpawnInternals = {}): SpawnFn {\n const spawnImpl = internals.spawnImpl ?? spawn;\n const killImpl: KillFn = internals.killImpl ?? ((pid, sig) => process.kill(pid, sig));\n const killGraceMs = internals.killGraceMs ?? 5000;\n const maxOutputBytes = internals.maxOutputBytes ?? 10 * 1024 * 1024;\n\n return (cmd, args, opts = {}) =>\n new Promise((resolve, reject) => {\n const streaming = opts.streaming === true;\n const child = spawnImpl(cmd, [...args], {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n stdio: streaming ? [\"ignore\", \"inherit\", \"inherit\"] : [\"ignore\", \"pipe\", \"pipe\"],\n // Detach ONLY when a timeout can fire: the child then leads its own\n // process group, so the timeout can kill the WHOLE tree (vite, and\n // Chromium under lhci/playwright) via process.kill(-pid), not just the\n // npx/pnpm wrapper. Without it, killing the wrapper orphaned the\n // grandchildren — a zombie vite squatting its port, Chrome left running.\n // We do NOT detach timeout-less streaming calls (pnpm install/up):\n // detaching gains nothing there (no timeout → no group-kill) and would\n // break terminal Ctrl-C, which only reaches the foreground group — i.e.\n // it would re-orphan the very children this guards. We never unref() the\n // child since we still await it.\n detached: opts.timeoutMs !== undefined,\n });\n\n // Cap appended output so an unbounded stream can't exhaust memory.\n const cap = (acc: string, chunk: string): string => {\n if (acc.length >= maxOutputBytes) return acc;\n const next = acc + chunk;\n return next.length > maxOutputBytes\n ? next.slice(0, maxOutputBytes) + TRUNCATION_MARKER\n : next;\n };\n\n let stdout = \"\";\n let stderr = \"\";\n // Decode each stream through a StringDecoder so a multibyte UTF-8 char\n // split across two `data` chunks isn't corrupted: the decoder holds the\n // partial trailing bytes until the rest arrives, instead of the old\n // `String(chunk)` which decoded each chunk in isolation (and replaced the\n // split char with U+FFFD). Flushed via `.end()` on close.\n const outDecoder = new StringDecoder(\"utf-8\");\n const errDecoder = new StringDecoder(\"utf-8\");\n if (!streaming) {\n child.stdout?.on(\n \"data\",\n (chunk: Buffer) => (stdout = cap(stdout, outDecoder.write(chunk))),\n );\n child.stderr?.on(\n \"data\",\n (chunk: Buffer) => (stderr = cap(stderr, errDecoder.write(chunk))),\n );\n }\n\n /** Signal the child's whole process group; ignore if it's already gone.\n * POSIX-only: a negative pid signals the group (the project targets\n * macOS/Linux; this is only reached when detached, i.e. on a timeout). */\n const killGroup = (sig: NodeJS.Signals): void => {\n if (child.pid === undefined) return;\n try {\n killImpl(-child.pid, sig);\n } catch {\n // ESRCH: the group already exited between the timeout and the kill.\n }\n };\n\n let killTimer: ReturnType<typeof setTimeout> | undefined;\n const timer = opts.timeoutMs\n ? setTimeout(() => {\n killGroup(\"SIGTERM\");\n // Escalate if SIGTERM is ignored (a wedged Chrome can swallow it).\n killTimer = setTimeout(() => killGroup(\"SIGKILL\"), killGraceMs);\n // Best-effort cleanup AFTER we've already rejected — it must never\n // hold the CLI open past its real work.\n killTimer.unref();\n reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));\n }, opts.timeoutMs)\n : undefined;\n\n const clearTimers = (): void => {\n if (timer) clearTimeout(timer);\n if (killTimer) clearTimeout(killTimer);\n };\n\n child.on(\"error\", (err) => {\n clearTimers();\n reject(err);\n });\n child.on(\"close\", (code) => {\n clearTimers();\n if (!streaming) {\n // Flush any bytes the decoder buffered mid-character (e.g. a truncated\n // final UTF-8 sequence). `.end()` returns \"\" when nothing is pending.\n stdout = cap(stdout, outDecoder.end());\n stderr = cap(stderr, errDecoder.end());\n }\n resolve({ code: code ?? -1, stdout, stderr });\n });\n });\n}\n\nexport const defaultSpawn: SpawnFn = makeSpawn();\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { baselineVersions } from \"../configs/baseline-versions.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport { scanOutdated, type OutdatedCounts } from \"./deps-outdated.js\";\n\nexport type Drift = \"same\" | \"patch\" | \"minor\" | \"major\" | \"newer\";\n\nexport type DepsDriftEntry = {\n pkg: string;\n baseline: string;\n actual: string;\n drift: Drift;\n};\n\n/** The deps audit reports TWO signals:\n * - `entries`: declared-range drift vs the canonical baseline (what the\n * package.json *asks for*, caret-stripped) — the long-standing signal.\n * - `outdated`: real installed-version drift vs the registry's latest, from\n * the committed lockfile (null when it can't be determined). Added so the\n * \"Deps Drifted\" dashboard number stops being the only — and misleading —\n * deps signal. */\nexport type DepsDetails = {\n entries: DepsDriftEntry[];\n outdated: OutdatedCounts | null;\n};\n\nfunction stripCaret(range: string): string {\n return range.replace(/^[\\^~]/, \"\");\n}\n\n/** A spec we can drift-compare against a semver baseline: a plain version or\n * caret/tilde range like \"5.55.10\", \"^5.55.10\", \"~5.0.0\". Excludes \"*\",\n * \"latest\", \"workspace:*\", \"npm:\"-aliases, and git/URL/file specs — those used\n * to parse to NaN and produce bogus drift, so they're skipped instead. */\nfunction isComparableRange(spec: string): boolean {\n return /^[\\^~]?\\d/.test(spec.trim());\n}\n\nfunction parseSemver(v: string): [number, number, number] {\n const cleaned = stripCaret(v).split(\"-\")[0] ?? \"0.0.0\";\n const parts = cleaned.split(\".\").map((n) => Number.parseInt(n, 10));\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n}\n\nfunction compareSemver(actual: string, baseline: string): Drift {\n const [aMajor, aMinor, aPatch] = parseSemver(actual);\n const [bMajor, bMinor, bPatch] = parseSemver(baseline);\n if (aMajor > bMajor) return \"newer\";\n if (aMajor < bMajor) return \"major\";\n if (aMinor > bMinor) return \"newer\";\n if (aMinor < bMinor) return \"minor\";\n if (aPatch > bPatch) return \"newer\";\n if (aPatch < bPatch) return \"patch\";\n return \"same\";\n}\n\nexport async function depsAudit(ctx: AuditContext): Promise<AuditResult> {\n const pkgPath = join(ctx.site.path, \"package.json\");\n let pkgRaw: string;\n try {\n pkgRaw = await readFile(pkgPath, \"utf-8\");\n } catch (err) {\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status: \"skip\",\n summary: `no package.json at ${pkgPath}`,\n details: { error: String(err) },\n };\n }\n\n let pkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };\n try {\n pkg = JSON.parse(pkgRaw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n } catch (err) {\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status: \"fail\",\n summary: `package.json is not valid JSON: ${(err as Error).message}`,\n details: { error: String(err) },\n };\n }\n const installed: Record<string, string> = {\n ...(pkg.dependencies ?? {}),\n ...(pkg.devDependencies ?? {}),\n };\n\n const entries: DepsDriftEntry[] = [];\n for (const [name, baseline] of Object.entries(baselineVersions)) {\n const actual = installed[name];\n if (!actual) continue;\n // Skip non-semver specs (\"*\", \"workspace:*\", \"npm:\"-aliases, git/URL): they\n // can't be drift-compared and used to yield NaN-driven bogus drift (LOW-3).\n if (!isComparableRange(actual)) continue;\n entries.push({\n pkg: name,\n baseline,\n actual,\n drift: compareSemver(actual, baseline),\n });\n }\n\n const anyMajor = entries.some((d) => d.drift === \"major\");\n const anyMinor = entries.some((d) => d.drift === \"minor\");\n const anyNewer = entries.some((d) => d.drift === \"newer\");\n\n // Status stays driven by the declared-range baseline drift (unchanged\n // behavior). The outdated count is an independent, informational signal.\n const status: AuditResult[\"status\"] = anyMajor ? \"fail\" : anyMinor || anyNewer ? \"warn\" : \"pass\";\n\n const driftSummary =\n status === \"pass\"\n ? `all ${entries.length} tracked deps in line with baseline`\n : status === \"warn\"\n ? `${entries.filter((d) => d.drift !== \"same\").length} of ${entries.length} tracked deps drifted`\n : `${entries.filter((d) => d.drift === \"major\").length} deps lagging by a major version`;\n\n const outdated = await scanOutdated(ctx.site.path, ctx.spawn ?? defaultSpawn);\n const summary = outdated\n ? `${driftSummary}; ${outdated.outdated} outdated install(s) (${outdated.major} major)`\n : driftSummary;\n\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status,\n summary,\n details: { entries, outdated } satisfies DepsDetails,\n };\n}\n","import type { Site } from \"../types.js\";\n\n/** Human-friendly label for log/output formatting. Prefer the inventory's\n * `name` when present (e.g. \"caltex-landing\") and fall back to the\n * filesystem `path` when unnamed. Every audit + recipe uses this.\n *\n * Uses `||` (not `??`) deliberately: an Airtable Name that slugs to the EMPTY\n * string (`siteSlug(\"!!!\")` → \"\") is `\"\"`, not null/undefined, so `??` would let\n * it through and render a blank label. `||` falls back to the path. */\nexport function siteLabel(site: Site): string {\n return site.name || site.path;\n}\n","// Curated map of the framework deps reddoor sites should stay close to.\n// Refreshed at each package release from reddoor-starter's package.json.\n// Versions are caret ranges to mirror what `pnpm add` would produce.\n\nexport const baselineVersions: Record<string, string> = {\n // SvelteKit core\n svelte: \"^5.55.10\",\n \"@sveltejs/kit\": \"^2.61.1\",\n \"@sveltejs/adapter-netlify\": \"^6.0.4\",\n \"@sveltejs/adapter-auto\": \"^7.0.1\",\n \"@sveltejs/vite-plugin-svelte\": \"^7.1.2\",\n \"svelte-check\": \"^4.4.8\",\n\n // Build tooling\n vite: \"^8.0.14\",\n vitest: \"^4.1.7\",\n typescript: \"^6.0.3\",\n\n // Tailwind 4\n tailwindcss: \"^4.3.0\",\n \"@tailwindcss/vite\": \"^4.3.0\",\n\n // Prismic\n \"@prismicio/client\": \"^7.21.8\",\n \"@prismicio/svelte\": \"^2.2.1\",\n \"@slicemachine/adapter-sveltekit\": \"^0.3.96\",\n \"slice-machine-ui\": \"^2.21.3\",\n\n // Test tooling\n \"@playwright/test\": \"^1.60.0\",\n \"@axe-core/playwright\": \"^4.11.3\",\n \"@lhci/cli\": \"^0.15.1\",\n\n // Lint\n eslint: \"^10.4.0\",\n \"eslint-plugin-svelte\": \"^3.18.0\",\n \"eslint-config-prettier\": \"^10.1.8\",\n prettier: \"^3.8.3\",\n \"prettier-plugin-svelte\": \"^4.0.1\",\n \"typescript-eslint\": \"^8.60.0\",\n \"@eslint/js\": \"^10.0.1\",\n globals: \"^17.6.0\",\n\n // Misc\n \"@lucide/svelte\": \"^1.17.0\",\n \"@zerodevx/svelte-img\": \"^2.1.2\",\n};\n\nexport default baselineVersions;\n","import { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { SpawnFn } from \"./util/spawn.js\";\n\n/** Real installed-version drift, distinct from the declared-range \"drift\" the\n * deps audit computes against the baseline: how many dependencies are behind\n * the registry's latest, per the committed lockfile. */\nexport type OutdatedCounts = { outdated: number; major: number };\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction majorOf(version: string): number {\n const head = version.replace(/^[\\^~]/, \"\").split(\".\")[0] ?? \"0\";\n const n = Number.parseInt(head, 10);\n return Number.isNaN(n) ? 0 : n;\n}\n\n/**\n * Count outdated dependencies for a site, using its committed lockfile as the\n * source of truth for \"what's installed/deployed\". Returns `null` (skip — the\n * caller degrades gracefully) when it can't determine this:\n * - no `pnpm-lock.yaml` (not a pnpm site, or never installed)\n * - the lockfile is stale vs package.json (`--frozen-lockfile` install fails)\n * - `pnpm outdated` output isn't parseable\n *\n * `pnpm outdated` exits non-zero precisely WHEN there are outdated packages, so\n * its exit code is ignored and only its JSON is parsed. `--frozen-lockfile`\n * never mutates the lockfile, so this stays read-only with respect to the repo.\n */\nexport async function scanOutdated(\n sitePath: string,\n spawn: SpawnFn,\n): Promise<OutdatedCounts | null> {\n if (!(await exists(join(sitePath, \"pnpm-lock.yaml\")))) return null;\n\n // Everything below is best-effort: a thrown spawn (timeout, `pnpm` not on\n // PATH, spawn error) must degrade to a skip (null), NOT bubble up and flip the\n // whole deps audit to a hard fail — the declared-range drift is independent of\n // pnpm and must still report. (Mirrors securityAudit's try/catch.)\n try {\n // Materialize node_modules from the lockfile, but only when it's missing —\n // an already-installed checkout skips the cold install. `--frozen-lockfile`\n // never rewrites the lockfile (read-only wrt the repo) and fails fast on a\n // lockfile out of sync with package.json → skip.\n if (!(await exists(join(sitePath, \"node_modules\")))) {\n const install = await spawn(\"pnpm\", [\"install\", \"--frozen-lockfile\"], {\n cwd: sitePath,\n timeoutMs: 180_000,\n });\n if (install.code !== 0) return null;\n }\n\n // `pnpm outdated` exits non-zero precisely WHEN there are outdated packages,\n // so its exit code is ignored and only its JSON is parsed.\n const res = await spawn(\"pnpm\", [\"outdated\", \"--json\"], {\n cwd: sitePath,\n timeoutMs: 60_000,\n });\n const parsed = JSON.parse(res.stdout || \"{}\") as Record<\n string,\n { current?: string; latest?: string }\n >;\n const entries = Object.values(parsed);\n return {\n outdated: entries.length,\n major: entries.filter((e) => e.current && e.latest && majorOf(e.latest) > majorOf(e.current))\n .length,\n };\n } catch {\n return null;\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { ESLint } from \"eslint\";\nimport { check as prettierCheck, resolveConfig as prettierResolveConfig } from \"prettier\";\nimport { glob } from \"tinyglobby\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport type { AuditContext } from \"./util/inject.js\";\n\nconst TARGET_GLOBS = [\"**/*.{ts,js,svelte}\"];\nconst IGNORE = [\"node_modules/**\", \"dist/**\", \".svelte-kit/**\", \"build/**\", \".netlify/**\"];\n\nasync function listFiles(cwd: string): Promise<string[]> {\n return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });\n}\n\nexport async function lintAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const configPath = join(site.path, \"eslint.config.js\");\n\n if (!existsSync(configPath)) {\n return {\n audit: \"lint\",\n site: siteLabel(site),\n status: \"skip\",\n summary: \"no eslint config at site root\",\n };\n }\n\n const eslint = new ESLint({\n cwd: site.path,\n overrideConfigFile: configPath,\n errorOnUnmatchedPattern: false,\n });\n\n const relFiles = await listFiles(site.path);\n\n // Pass relative paths to ESLint; its cwd is already site.path. Avoids\n // dereferencing symlinks on pnpm workspaces.\n const eslintResults = await eslint.lintFiles(relFiles);\n const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);\n const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);\n\n const prettierUnformatted: string[] = [];\n for (const rel of relFiles) {\n const absForResolve = join(site.path, rel);\n const source = await readFile(absForResolve, \"utf-8\");\n const options = (await prettierResolveConfig(absForResolve)) ?? {};\n const ok = await prettierCheck(source, { ...options, filepath: absForResolve });\n if (!ok) prettierUnformatted.push(rel);\n }\n\n const status: AuditResult[\"status\"] =\n eslintErrors > 0 || prettierUnformatted.length > 0\n ? \"fail\"\n : eslintWarnings > 0\n ? \"warn\"\n : \"pass\";\n\n const summary =\n status === \"pass\"\n ? `lint clean across ${relFiles.length} files`\n : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;\n\n return {\n audit: \"lint\",\n site: siteLabel(site),\n status,\n summary,\n details: {\n eslintErrors,\n eslintWarnings,\n prettierUnformatted,\n files: relFiles.length,\n },\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { defaultSpawn, type SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\n\ntype Severity = \"low\" | \"moderate\" | \"high\" | \"critical\";\n\ntype Counts = { low: number; moderate: number; high: number; critical: number };\n\ntype AdvisoryEntry = {\n module: string;\n severity: Severity;\n title: string;\n cves?: string[];\n url?: string;\n};\n\n// pnpm audit output (npm-compat with extra advisories map keyed by ID).\ntype PnpmAuditJson = {\n metadata?: { vulnerabilities?: Partial<Counts> };\n advisories?: Record<\n string,\n {\n id?: number;\n title?: string;\n module_name?: string;\n severity?: string;\n cves?: string[];\n url?: string;\n }\n >;\n};\n\n// npm v7+ shape (vulnerabilities keyed by package name).\ntype NpmAuditJson = {\n metadata?: { vulnerabilities?: Partial<Counts> };\n vulnerabilities?: Record<\n string,\n {\n name?: string;\n severity?: string;\n via?: unknown;\n url?: string;\n }\n >;\n};\n\nfunction classify(v: Counts) {\n if (v.critical > 0 || v.high > 0) return \"fail\" as const;\n if (v.moderate > 0 || v.low > 0) return \"warn\" as const;\n return \"pass\" as const;\n}\n\nfunction normalizeSeverity(s: unknown): Severity {\n if (s === \"low\" || s === \"moderate\" || s === \"high\" || s === \"critical\") return s;\n // npm/pnpm sometimes emit \"info\" for informational advisories. Map down\n // rather than defaulting to \"moderate\" (which would inflate severity).\n return \"low\";\n}\n\nfunction extractAdvisoriesFromPnpm(parsed: PnpmAuditJson): AdvisoryEntry[] {\n const out: AdvisoryEntry[] = [];\n for (const a of Object.values(parsed.advisories ?? {})) {\n if (!a) continue;\n out.push({\n module: a.module_name ?? \"unknown\",\n severity: normalizeSeverity(a.severity),\n title: a.title ?? \"(no title)\",\n ...(a.cves ? { cves: a.cves } : {}),\n ...(a.url ? { url: a.url } : {}),\n });\n }\n return out;\n}\n\n/** Walk an npm v7+ `via` chain to find the root entry whose `via` array\n * contains a real advisory object (rather than another package name string).\n * Returns the package name at the root and the advisory detail. */\nfunction resolveNpmAdvisoryRoot(\n startName: string,\n vulnerabilities: NonNullable<NpmAuditJson[\"vulnerabilities\"]>,\n): { rootName: string; detail?: { title?: string; url?: string } } {\n const seen = new Set<string>();\n let current = startName;\n while (!seen.has(current)) {\n seen.add(current);\n const entry = vulnerabilities[current];\n if (!entry || !Array.isArray(entry.via)) return { rootName: current };\n\n const detailed = entry.via.find(\n (e): e is { title?: string; url?: string } => typeof e === \"object\" && e !== null,\n );\n if (detailed) return { rootName: current, detail: detailed };\n\n const next = entry.via.find((e): e is string => typeof e === \"string\");\n if (!next || next === current) return { rootName: current };\n current = next;\n }\n return { rootName: current };\n}\n\nfunction extractAdvisoriesFromNpm(parsed: NpmAuditJson): AdvisoryEntry[] {\n const vulnerabilities = parsed.vulnerabilities ?? {};\n const roots = new Map<string, AdvisoryEntry>();\n\n for (const [name, v] of Object.entries(vulnerabilities)) {\n if (!v) continue;\n const { rootName, detail } = resolveNpmAdvisoryRoot(name, vulnerabilities);\n if (roots.has(rootName)) continue; // already surfaced via another transitive entry\n\n const rootEntry = vulnerabilities[rootName];\n const severity = normalizeSeverity(rootEntry?.severity ?? v.severity);\n const title = detail?.title ?? rootName;\n const url = detail?.url;\n\n roots.set(rootName, {\n module: rootEntry?.name ?? rootName,\n severity,\n title,\n ...(url ? { url } : {}),\n });\n }\n\n return [...roots.values()];\n}\n\ntype ToolResult =\n | { kind: \"missing\" }\n | { kind: \"error\"; reason: string }\n | { kind: \"ok\"; parsed: PnpmAuditJson & NpmAuditJson };\n\nasync function runAuditTool(\n spawn: (cmd: string, args: readonly string[], opts?: { cwd?: string }) => Promise<SpawnResult>,\n cmd: string,\n args: readonly string[],\n cwd: string,\n): Promise<ToolResult> {\n let raw: SpawnResult;\n try {\n raw = await spawn(cmd, args, { cwd });\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) return { kind: \"missing\" };\n return { kind: \"error\", reason: `spawn failed: ${String(err).slice(0, 200)}` };\n }\n\n // 0 = clean, 1 = vulns found. Anything else is a real error.\n if (raw.code !== 0 && raw.code !== 1) {\n return {\n kind: \"error\",\n reason: `exit ${raw.code}${raw.stderr ? `: ${raw.stderr.slice(0, 150)}` : \"\"}`,\n };\n }\n\n let parsed: PnpmAuditJson & NpmAuditJson;\n try {\n parsed = JSON.parse(raw.stdout || \"{}\") as PnpmAuditJson & NpmAuditJson;\n } catch (err) {\n return { kind: \"error\", reason: `unparseable JSON: ${String(err).slice(0, 100)}` };\n }\n\n // pnpm error envelope: { error: { code, message } }. npm sometimes emits\n // a top-level error too. Either means the audit didn't actually run.\n const errEnvelope = (parsed as unknown as { error?: { code?: string } }).error;\n if (errEnvelope && typeof errEnvelope === \"object\") {\n return { kind: \"error\", reason: errEnvelope.code ?? \"error envelope returned\" };\n }\n\n // Without metadata.vulnerabilities there are no counts to report and we\n // can't trust the result. An empty `{}` is just as suspect as a missing\n // key — counts default to 0 and we'd silently report \"pass\". Treat both\n // as a tool failure so the caller can fall through to the other audit.\n const vulnsMeta = parsed.metadata?.vulnerabilities;\n if (!vulnsMeta || Object.keys(vulnsMeta).length === 0) {\n return { kind: \"error\", reason: \"no metadata.vulnerabilities in output\" };\n }\n\n return { kind: \"ok\", parsed };\n}\n\nexport async function securityAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n let used: \"pnpm audit\" | \"npm audit\" = \"pnpm audit\";\n let result = await runAuditTool(spawn, \"pnpm\", [\"audit\", \"--json\", \"--prod\"], site.path);\n\n // Fall through to npm if pnpm is missing OR pnpm couldn't actually\n // audit the project (e.g., no pnpm-lock.yaml). Previously we only fell\n // through on ENOENT, which meant npm-using sites silently reported \"pass\"\n // because pnpm returned an error envelope with no metadata.\n if (result.kind !== \"ok\") {\n const pnpmReason = result.kind === \"missing\" ? \"not installed\" : result.reason;\n const npmResult = await runAuditTool(\n spawn,\n \"npm\",\n [\"audit\", \"--json\", \"--omit=dev\"],\n site.path,\n );\n if (npmResult.kind === \"ok\") {\n result = npmResult;\n used = \"npm audit\";\n } else {\n const npmReason = npmResult.kind === \"missing\" ? \"not installed\" : npmResult.reason;\n return {\n audit: \"security\",\n site: label,\n status: \"skip\",\n summary: `cannot run audit — pnpm: ${pnpmReason}; npm: ${npmReason}`,\n };\n }\n }\n\n const parsed = result.parsed;\n\n const counts: Counts = {\n low: parsed.metadata?.vulnerabilities?.low ?? 0,\n moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,\n high: parsed.metadata?.vulnerabilities?.high ?? 0,\n critical: parsed.metadata?.vulnerabilities?.critical ?? 0,\n };\n\n const advisories =\n used === \"pnpm audit\" ? extractAdvisoriesFromPnpm(parsed) : extractAdvisoriesFromNpm(parsed);\n\n const status = classify(counts);\n const total = counts.low + counts.moderate + counts.high + counts.critical;\n const summary =\n status === \"pass\"\n ? `${used}: 0 vulnerabilities`\n : `${used}: ${total} vulnerabilities (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;\n\n return {\n audit: \"security\",\n site: label,\n status,\n summary,\n details: { counts, advisories },\n };\n}\n","import { readFile, writeFile, mkdtemp, rm, readdir } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AuditResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { lighthouseConfig } from \"../configs/lighthouse.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn, SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { readSiteConfig } from \"./util/site-config.js\";\nimport { findFreePort, withFreePort } from \"../util/free-port.js\";\n\ntype ManifestEntry = {\n url: string;\n summary: Record<string, number>;\n htmlPath?: string;\n jsonPath?: string;\n};\n\ntype AssertionResult = {\n name: string;\n actual: number;\n expected: number;\n operator: string;\n passed: boolean;\n level: \"warn\" | \"error\";\n auditProperty?: string;\n auditId?: string;\n};\n\ntype NormalizedLhciResult = {\n summary: Record<string, number>;\n assertionsFailed: number;\n assertions: Array<{ category: string; level: \"warn\" | \"error\"; message: string }>;\n};\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\ntype LhrFile = {\n requestedUrl: string;\n finalUrl?: string;\n categories: Record<string, { score: number | null }>;\n};\n\n/**\n * Build manifest-equivalent entries by scanning the `.lighthouseci/` dir\n * for `lhr-*.json` files written by `lhci collect`. We used to read\n * `manifest.json` directly, but lhci 0.15+ no longer writes it — the\n * audit would silently return \"no manifest written\" against a perfectly\n * healthy run. Reproduced on caltex 2026-05-28 (0.10.5 dogfood).\n */\nasync function readLhrEntries(resultsDir: string): Promise<ManifestEntry[]> {\n const files = await readdir(resultsDir).catch(() => [] as string[]);\n const entries: ManifestEntry[] = [];\n for (const f of files) {\n if (!f.startsWith(\"lhr-\") || !f.endsWith(\".json\")) continue;\n const lhr = await readJsonMaybe<LhrFile>(join(resultsDir, f));\n if (!lhr || !lhr.categories) continue;\n const summary: Record<string, number> = {};\n for (const [k, v] of Object.entries(lhr.categories)) {\n if (typeof v?.score === \"number\") summary[k] = v.score;\n }\n entries.push({ url: lhr.requestedUrl, summary });\n }\n return entries;\n}\n\nfunction averageSummaries(entries: ManifestEntry[]): Record<string, number> {\n if (entries.length === 0) return {};\n const sums: Record<string, number> = {};\n const counts: Record<string, number> = {};\n for (const e of entries) {\n for (const [k, v] of Object.entries(e.summary ?? {})) {\n if (typeof v !== \"number\") continue;\n sums[k] = (sums[k] ?? 0) + v;\n counts[k] = (counts[k] ?? 0) + 1;\n }\n }\n const out: Record<string, number> = {};\n for (const k of Object.keys(sums)) {\n const total = sums[k] ?? 0;\n const count = counts[k] ?? 1;\n out[k] = total / count;\n }\n return out;\n}\n\nfunction categoryFromAssertion(a: AssertionResult): string {\n // `name` looks like \"categories:accessibility\" or \"audits:uses-http2\".\n const colonIdx = a.name.indexOf(\":\");\n return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;\n}\n\nfunction messageForAssertion(a: AssertionResult): string {\n // `a.actual` is parsed from external lhci/Lighthouse JSON; a malformed or\n // missing value (not a number) would make `.toFixed` throw and crash the whole\n // audit. Guard it and fall back to a readable string instead.\n const actual = typeof a.actual === \"number\" ? a.actual.toFixed(2) : \"n/a\";\n return `${a.name} ${a.operator} ${a.expected} (actual: ${actual})`;\n}\n\n/** Shared tail: scan `.lighthouseci/` for lhr-*.json + assertion-results.json and\n * build the AuditResult. Identical for the checkout and deployed paths. */\nasync function parseLhciResults(\n resultsDir: string,\n label: string,\n raw: SpawnResult,\n): Promise<AuditResult> {\n const manifest = await readLhrEntries(resultsDir);\n\n if (manifest.length === 0) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"fail\",\n summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const assertionResults =\n (await readJsonMaybe<AssertionResult[]>(join(resultsDir, \"assertion-results.json\"))) ?? [];\n\n const failed = assertionResults.filter((a) => !a.passed);\n const assertions = failed.map((a) => ({\n category: categoryFromAssertion(a),\n level: a.level,\n message: messageForAssertion(a),\n }));\n\n const anyError = assertions.some((a) => a.level === \"error\");\n const anyWarn = assertions.some((a) => a.level === \"warn\");\n const status: AuditResult[\"status\"] = anyError ? \"fail\" : anyWarn ? \"warn\" : \"pass\";\n\n const normalized: NormalizedLhciResult = {\n summary: averageSummaries(manifest),\n assertionsFailed: failed.length,\n assertions,\n };\n\n const summary =\n status === \"pass\"\n ? \"lighthouse: all categories passing\"\n : `lighthouse: ${failed.length} assertion(s) failed`;\n\n return { audit: \"lighthouse\", site: label, status, summary, details: normalized };\n}\n\n/** Checkout mode (unchanged behavior): boot the site's vite dev server on a\n * pinned free port and audit the local fixtures/override URL. */\nasync function checkoutLighthouse(spawn: SpawnFn, site: Site, label: string): Promise<AuditResult> {\n const siteCfg = await readSiteConfig(site.path);\n // Allocate a free port + force vite to `--strictPort` so the spawned dev\n // server either binds the port we picked or fails loudly (caltex 2026-05-28\n // zombie-vite incident).\n const port = await findFreePort();\n const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];\n const resolvedConfig = {\n ...lighthouseConfig,\n ci: {\n ...lighthouseConfig.ci,\n collect: {\n ...lighthouseConfig.ci.collect,\n url: [withFreePort(baseUrl, port)],\n startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`,\n },\n },\n };\n\n const configDir = await mkdtemp(join(tmpdir(), \"reddoor-lhci-\"));\n const configPath = join(configDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(site.path, \".lighthouseci\");\n await rm(resultsDir, { recursive: true, force: true });\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: site.path,\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(configDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n await rm(configDir, { recursive: true, force: true });\n\n return parseLhciResults(resultsDir, label, raw);\n}\n\n/** Deployed mode: audit a production URL directly — no checkout, no dev server.\n * Runs in a throwaway tmp cwd; uploads to the filesystem so fleet runs never\n * push 200 public reports to temporary-public-storage. */\nasync function deployedLighthouse(\n spawn: SpawnFn,\n deployedUrl: string,\n label: string,\n): Promise<AuditResult> {\n const workDir = await mkdtemp(join(tmpdir(), \"reddoor-lh-deployed-\"));\n const resolvedConfig = {\n ci: {\n // Deliberately NOT spread from lighthouseConfig.ci.collect: deployed mode\n // must omit startServerCommand and the dev-server settings entirely.\n collect: {\n url: [deployedUrl],\n // 3 runs to damp Lighthouse's run-to-run variance; parseLhciResults\n // averages the lhr files. (Median is a tracked future refinement.)\n numberOfRuns: 3,\n settings: { preset: \"desktop\", skipAudits: [\"uses-http2\"] },\n },\n assert: lighthouseConfig.ci.assert,\n upload: { target: \"filesystem\", outputDir: join(workDir, \"lhci-report\") },\n },\n };\n\n const configPath = join(workDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(workDir, \".lighthouseci\");\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: workDir,\n // 3 serial cold runs of a slow deployed site (lhci's own maxWaitForLoad\n // ~45-60s each) + first-use Chrome download can plausibly exceed 3 min →\n // SIGTERM → no lhr-*.json → spurious \"no scores\". Match the 5-min budget\n // the checkout path already gives (erp-industrials nightly flake,\n // morning-brief 2026-06-10 MEDIUM-F).\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(workDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n\n try {\n return await parseLhciResults(resultsDir, label, raw);\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\nexport async function lighthouseAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n return site.deployedUrl\n ? deployedLighthouse(spawn, site.deployedUrl, label)\n : checkoutLighthouse(spawn, site, label);\n}\n","export const lighthouseConfig = {\n ci: {\n collect: {\n url: [\"http://localhost:5173/dev/a11y-fixtures\"],\n // `npm run vite:dev` works on both pnpm and npm sites — pnpm respects\n // the `run` form too. Keeps this config portable across the fleet\n // while sites transition to pnpm.\n startServerCommand: \"npm run vite:dev\",\n startServerReadyPattern: \"ready in\",\n startServerReadyTimeout: 120_000,\n numberOfRuns: 1,\n settings: {\n preset: \"desktop\",\n skipAudits: [\"uses-http2\"],\n },\n },\n assert: {\n assertions: {\n \"categories:accessibility\": [\"error\", { minScore: 0.95 }],\n \"categories:best-practices\": [\"error\", { minScore: 0.9 }],\n \"categories:seo\": [\"error\", { minScore: 0.9 }],\n \"categories:performance\": [\"warn\", { minScore: 0.7 }],\n },\n },\n upload: {\n target: \"temporary-public-storage\",\n },\n },\n} as const;\n\nexport default lighthouseConfig;\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport type SiteConfig = {\n /** Override URL the lighthouse audit hits. Sites without the default\n * `/dev/a11y-fixtures` dev route set this to their homepage. */\n lighthouseUrl?: string;\n};\n\n/**\n * Read per-site overrides from `package.json#reddoor`. Returns `{}` on any\n * failure (missing file, malformed JSON, missing key, wrong type) so every\n * caller can safely fall back to its built-in default. Never throws.\n */\nexport async function readSiteConfig(sitePath: string): Promise<SiteConfig> {\n let raw: string;\n try {\n raw = await readFile(join(sitePath, \"package.json\"), \"utf-8\");\n } catch {\n return {};\n }\n let pkg: unknown;\n try {\n pkg = JSON.parse(raw);\n } catch {\n return {};\n }\n if (!pkg || typeof pkg !== \"object\") return {};\n const cfg = (pkg as { reddoor?: unknown }).reddoor;\n if (!cfg || typeof cfg !== \"object\") return {};\n\n const out: SiteConfig = {};\n const url = (cfg as { lighthouseUrl?: unknown }).lighthouseUrl;\n if (typeof url === \"string\" && url.length > 0) {\n out.lighthouseUrl = url;\n }\n return out;\n}\n","import { createServer } from \"node:net\";\n\n/**\n * Bind an ephemeral TCP port, capture it, release it, and return it. Used\n * by the lighthouse and a11y audits to pick a port the audit's own dev\n * server will then bind via `--strictPort`.\n *\n * Why: vite's default behavior on a busy port is to bump to the next free\n * one (5173 → 5174 → …). When zombie vite processes (or any squatter) are\n * already on 5173, the audit's spawned vite lands on a higher port, but\n * the audit tooling (lhci, playwright) still probes 5173 — hits the\n * zombie — gets stale 404s — fails with \"no manifest written\" / \"no\n * results written (exit 1)\". Reproduced on caltex 2026-05-28 with 10\n * orphaned vite processes accumulated across this repo, the reports repo,\n * and caltex itself. Allocating a free port up front + `--strictPort`\n * makes the audit immune to port collisions.\n *\n * TOCTOU note: the small window between close() and the spawned vite\n * binding is theoretically racy, but in practice we run one audit at a\n * time and the OS keeps the port free for re-use. If vite still fails to\n * bind under `--strictPort`, the audit fails loudly — that's the correct\n * outcome (vs. silently auditing the wrong server).\n */\nexport async function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = createServer();\n server.unref();\n server.on(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const addr = server.address();\n if (typeof addr === \"object\" && addr) {\n const port = addr.port;\n server.close(() => resolve(port));\n } else {\n server.close();\n reject(new Error(\"findFreePort: could not determine assigned port from socket\"));\n }\n });\n });\n}\n\n/**\n * Swap the port (and force `localhost` host) on a URL so it points at the\n * audit's freshly-allocated dev server. Preserves the path + any query.\n * Used to rewrite the lighthouse `url` so lhci probes the correct port.\n */\nexport function withFreePort(url: string, port: number): string {\n const u = new URL(url);\n u.hostname = \"localhost\";\n u.port = String(port);\n return u.toString();\n}\n","import { readFile, writeFile, mkdtemp, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { a11yRoutes, smokeRoutes } from \"../configs/playwright-a11y.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { findFreePort } from \"../util/free-port.js\";\n\ntype Impact = \"minor\" | \"moderate\" | \"serious\" | \"critical\";\n\ntype AxeViolation = {\n id: string;\n impact: Impact;\n route: string;\n help?: string;\n helpUrl?: string;\n nodes?: Array<{ html?: string; target?: string[] }>;\n};\n\ntype NormalizedA11y = {\n totalViolations: number;\n byImpact: Partial<Record<Impact, number>>;\n violations: AxeViolation[];\n};\n\nconst RESULTS_REL = \".reddoor-a11y/results.json\";\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n// The audit-controlled playwright config. We synthesize it (rather than\n// rely on the site's playwright.config.ts) so we can pin the dev server\n// port + force `--strictPort` — same fix as the lighthouse audit, same\n// reason (zombie vite processes squatting on 5173 would otherwise eat\n// the audit's request and return stale 404s).\nfunction buildPlaywrightConfig(port: number, sitePath: string): string {\n return `import { defineConfig } from \"@playwright/test\";\n\nexport default defineConfig({\n testDir: \".\",\n testMatch: /.*\\\\.spec\\\\.ts$/,\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n reporter: process.env.CI ? \"github\" : \"list\",\n use: {\n baseURL: \"http://localhost:${port}\",\n trace: \"on-first-retry\",\n },\n webServer: {\n // --strictPort: refuse to bump to a different port if ours is taken,\n // so the audit fails loudly instead of probing a zombie.\n // reuseExistingServer:false: never reuse — we control the lifecycle.\n // cwd: playwright's default webServer.cwd is the config file's\n // directory. Our config lives in /tmp so without this override,\n // \"npm run vite:dev\" tries to read /tmp/.../package.json and\n // ENOENTs before vite ever starts. Caltex 2026-05-28 (0.10.5).\n command: \"npm run vite:dev -- --port ${port} --strictPort\",\n url: \"http://localhost:${port}/dev/a11y-fixtures\",\n cwd: ${JSON.stringify(sitePath)},\n reuseExistingServer: false,\n timeout: 120_000,\n },\n});\n`;\n}\n\n// The spec the audit writes runs all configured routes through axe in a single\n// test (so worker isolation doesn't fragment the collected violations) and\n// writes the structured result to <cwd>/.reddoor-a11y/results.json before\n// asserting. That way, the audit can read real axe details even when the\n// expect(...).toEqual([]) assertion fails.\nfunction buildSpec(): string {\n return `import { test, expect } from \"@playwright/test\";\nimport AxeBuilder from \"@axe-core/playwright\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nconst pages = ${JSON.stringify(a11yRoutes)};\nconst smokePages = ${JSON.stringify(smokeRoutes)};\nconst OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;\n\n// Playwright's default per-test timeout is 30s. We loop through every\n// configured route in a single test, so the budget needs to scale.\ntest.setTimeout(5 * 60_000);\n\ntest(\"a11y + hydration across configured routes\", async ({ page }) => {\n const violations = [];\n\n // Capture uncaught client-side exceptions across every route we visit. A page\n // that builds + SSRs cleanly can still throw on hydrate and blank itself\n // (data-dynamiq: a Svelte 4->5 run() referenced a $state declared after it) --\n // axe never sees that, so we listen for it directly and tag the route in scope.\n let currentRoute = \"\";\n page.on(\"pageerror\", (err) => {\n violations.push({\n id: \"client-error\",\n impact: \"critical\",\n route: currentRoute,\n help: String(err && err.message ? err.message : err),\n });\n });\n\n for (const { path, name } of pages) {\n currentRoute = name;\n await page.goto(path);\n // Snap CSS transitions/animations to their resting state before axe runs.\n // AnimateIn-style fixtures transition opacity 0->1; sampling mid-transition\n // makes axe compute color-contrast against semi-transparent text, yielding a\n // flaky \"serious\" color-contrast violation (~1/3 of runs on /dev/animate-in).\n // Disabling transitions/animations forces the final, rendered state\n // deterministically -- which is also what users (and prefers-reduced-motion\n // users) actually see, so it's the correct thing to assert.\n await page.addStyleTag({\n content: \"*,*::before,*::after{transition:none!important;animation:none!important;}\",\n });\n const results = await new AxeBuilder({ page })\n .withTags([\"wcag2a\",\"wcag2aa\",\"wcag21a\",\"wcag21aa\",\"wcag22aa\"])\n .analyze();\n for (const v of results.violations) {\n violations.push({\n id: v.id,\n impact: v.impact ?? \"moderate\",\n route: name,\n help: v.help,\n helpUrl: v.helpUrl,\n nodes: v.nodes.map((n) => ({ html: n.html, target: n.target })),\n });\n }\n }\n\n // Hydration smoke check: load real routes (the homepage) and fail on any\n // uncaught client-side error. No axe here -- real routes carry pre-existing\n // a11y debt we don't gate on; we only assert they don't crash on hydrate.\n // HTTP/SSR errors don't fire 'pageerror', so a data-less CI homepage that\n // renders empty-but-valid won't false-fail -- only a real client crash does.\n for (const { path, name } of smokePages) {\n currentRoute = name;\n await page.goto(path);\n // Let hydration + first effects run so a TDZ/ReferenceError surfaces.\n await page.waitForTimeout(2000);\n }\n\n const byImpact = {};\n for (const v of violations) {\n byImpact[v.impact] = (byImpact[v.impact] ?? 0) + 1;\n }\n if (OUTPUT) {\n await mkdir(dirname(OUTPUT), { recursive: true });\n await writeFile(\n OUTPUT,\n JSON.stringify({ totalViolations: violations.length, byImpact, violations }, null, 2),\n );\n }\n expect(violations).toEqual([]);\n});\n`;\n}\n\nexport async function a11yAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n // specDir lives INSIDE site.path (not /tmp) so the spec's\n // `import AxeBuilder from \"@axe-core/playwright\"` resolves via Node's\n // walk-up — the site's node_modules is the nearest one. A spec written\n // to /tmp ENOENTs at module resolution before any test runs. Caltex\n // 2026-05-28 (0.10.6 dogfood), third layer of the same class as the\n // webServer.cwd bug.\n const specDir = await mkdtemp(join(site.path, \".reddoor-a11y-spec-\"));\n // Everything past mkdtemp is wrapped so the transient specDir is removed on\n // EVERY catchable exit — success, skip-return, or any throw (a failed\n // writeFile/findFreePort used to orphan it). A timeout-SIGKILL of the parent\n // can't be caught here; `.reddoor-a11y-spec-*/` is fleet-gitignored as the\n // backstop for that. (2026-06-10 MEDIUM-D; recurred from 06-05 M3.)\n try {\n const specPath = join(specDir, \"a11y.spec.ts\");\n await writeFile(specPath, buildSpec(), \"utf-8\");\n\n const port = await findFreePort();\n const configPath = join(specDir, \"playwright.config.ts\");\n await writeFile(configPath, buildPlaywrightConfig(port, site.path), \"utf-8\");\n\n const resultsPath = join(site.path, RESULTS_REL);\n // Clear stale artifacts so a failed spawn never reports old data.\n await rm(join(site.path, \".reddoor-a11y\"), { recursive: true, force: true });\n\n let raw;\n try {\n raw = await spawn(\n \"npx\",\n [\"--yes\", \"playwright\", \"test\", `--config=${configPath}`, \"--reporter=line\", specPath],\n {\n cwd: site.path,\n env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath },\n // playwright on a cold tree downloads Chrome, boots the site's dev\n // server, and runs axe over every configured route. The shared 30 s\n // default in runAudits is fine for deps/lint/security but starves\n // playwright (mirrors the lighthouse fix shipped earlier).\n timeoutMs: 5 * 60_000,\n },\n );\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"a11y\",\n site: label,\n status: \"skip\",\n summary: \"npx/playwright not available\",\n };\n }\n throw err;\n }\n\n const artifact = await readJsonMaybe<NormalizedA11y>(resultsPath);\n\n if (!artifact) {\n return {\n audit: \"a11y\",\n site: label,\n status: \"fail\",\n summary: `a11y: no results written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const hasSerious =\n (artifact.byImpact.serious ?? 0) > 0 || (artifact.byImpact.critical ?? 0) > 0;\n const hasAny = artifact.totalViolations > 0;\n\n const status: AuditResult[\"status\"] = hasSerious ? \"fail\" : hasAny ? \"warn\" : \"pass\";\n const summary =\n status === \"pass\"\n ? `a11y: 0 violations across ${a11yRoutes.length} routes (+${smokeRoutes.length} hydration smoke)`\n : `a11y: ${artifact.totalViolations} violations`;\n\n return {\n audit: \"a11y\",\n site: label,\n status,\n summary,\n details: artifact,\n };\n } finally {\n await rm(specDir, { recursive: true, force: true });\n }\n}\n","import { defineConfig, devices, type PlaywrightTestConfig } from \"@playwright/test\";\n\nexport type A11yRoute = { path: string; name: string };\n\nexport const a11yRoutes: A11yRoute[] = [\n { path: \"/dev/a11y-fixtures\", name: \"a11y fixtures\" },\n { path: \"/dev/animate-in\", name: \"animate-in demo\" },\n];\n\n// Routes smoke-loaded for client-side (hydration) errors only — NOT axe-scanned.\n// Catches the class of bug where build + SSR succeed but client hydration throws\n// and blanks the page (data-dynamiq 2026-06-09: a Svelte 4->5 `run()` referenced\n// a `$state` declared after it → TDZ ReferenceError on hydrate). `/` is the one\n// route every site has; real routes carry a11y debt we don't gate on here, so we\n// assert only that they don't crash on hydrate.\nexport const smokeRoutes: A11yRoute[] = [{ path: \"/\", name: \"home\" }];\n\nexport const playwrightA11yConfig: PlaywrightTestConfig = defineConfig({\n testDir: \"tests\",\n testMatch: /.*\\.spec\\.ts$/,\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n reporter: process.env.CI ? \"github\" : \"list\",\n use: {\n baseURL: \"http://localhost:5173\",\n trace: \"on-first-retry\",\n },\n projects: [\n {\n name: \"chromium\",\n use: { ...devices[\"Desktop Chrome\"] },\n },\n ],\n webServer: {\n // Portable across pnpm and npm sites — pnpm respects `npm run` too.\n command: \"npm run vite:dev\",\n url: \"http://localhost:5173/dev/a11y-fixtures\",\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n },\n});\n\nexport default playwrightA11yConfig;\n","import type { AuditName, AuditResult, Site } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn } from \"./util/spawn.js\";\nimport { depsAudit } from \"./deps.js\";\nimport { lintAudit } from \"./lint.js\";\nimport { securityAudit } from \"./security.js\";\nimport { lighthouseAudit } from \"./lighthouse.js\";\nimport { a11yAudit } from \"./a11y.js\";\n\nconst REGISTRY: Record<AuditName, (ctx: AuditContext) => Promise<AuditResult>> = {\n deps: depsAudit,\n lint: lintAudit,\n security: securityAudit,\n lighthouse: lighthouseAudit,\n a11y: a11yAudit,\n};\n\nexport const ALL_AUDIT_NAMES = Object.keys(REGISTRY) as AuditName[];\n\n/** Default per-audit spawn timeout when running via runAudits (30 s). */\nconst DEFAULT_AUDIT_TIMEOUT_MS = 30_000;\n\nfunction timedSpawn(timeoutMs: number): SpawnFn {\n return (cmd, args, opts = {}) =>\n defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });\n}\n\n/** Single-audit runner with the same error-to-result conversion that\n * `runAudits` applies. Exposed so the CLI can wrap each audit in its\n * own progress task (listr2) and surface per-audit completion timing,\n * while keeping audit implementations UI-free. */\nexport async function runOneAudit(site: Site, name: AuditName): Promise<AuditResult> {\n if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);\n const spawn = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);\n // `||` not `??`: an empty-string slug (Airtable Name with no slug-able chars)\n // must fall back to the path, not render a blank `AuditResult.site` that would\n // then collapse fleet write-back grouping under the \"\" key.\n const label = site.name || site.path;\n try {\n return await REGISTRY[name]({ site, spawn });\n } catch (err) {\n return {\n audit: name,\n site: label,\n status: \"fail\",\n summary: `${name}: unexpected error — ${String(err)}`,\n };\n }\n}\n\nexport async function runAudits(site: Site, which?: AuditName[]): Promise<AuditResult[]> {\n const names = which ?? ALL_AUDIT_NAMES;\n for (const n of names) {\n if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);\n }\n return Promise.all(names.map((n) => runOneAudit(site, n)));\n}\n\nexport async function runAuditsAcross(sites: Site[], which?: AuditName[]): Promise<AuditResult[]> {\n const all = await Promise.all(sites.map((s) => runAudits(s, which)));\n return all.flat();\n}\n\nexport { depsAudit, lintAudit, securityAudit, lighthouseAudit, a11yAudit };\n","import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport type { RecipeResult, Site, ConfigName } from \"../types.js\";\nimport { ALL_TEMPLATES, templatesByName, type ConfigTemplate } from \"./sync-configs/templates.js\";\nimport {\n CANONICAL_GITIGNORE_ENTRIES,\n mergeGitignore,\n findTrackedArtifacts,\n} from \"./sync-configs/gitignore.js\";\nimport { listTrackedFiles, removeFromIndex } from \"../util/git.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type SyncConfigsOptions = {\n which?: ConfigName[];\n};\n\nconst GITIGNORE_CONFIG: ConfigName = \"gitignore\";\nconst SVELTE_CONFIG: ConfigName = \"svelte\";\nconst NETLIFY_CONFIG: ConfigName = \"netlify\";\n\n/** A site's `svelte.config.js` is \"compliant\" — and left untouched by sync —\n * once it builds on the canonical helpers (createSvelteConfig + adapter-netlify).\n *\n * Unlike the other exact-match templates, svelte.config legitimately carries\n * site-specific `kit.alias` and `compilerOptions`; an exact overwrite would\n * clobber those on every sync (it silently dropped MSOT's $utils alias,\n * 2026-06-04). So once a config is on the canonical pattern we preserve it as-is\n * and only rewrite a genuinely off-pattern (or missing) config. `createSvelteConfig`\n * now provides the canonical `$lib` aliases itself, and a site's own `kit.alias`\n * overrides per key (and may add more), so a site's additive customization is safe\n * to preserve. */\nfunction isSvelteConfigCompliant(contents: string): boolean {\n return contents.includes(\"createSvelteConfig\") && contents.includes(\"@sveltejs/adapter-netlify\");\n}\n\n/** Any of the baseline security headers — the marker that a netlify.toml is\n * deliberately hardened (vs. e.g. a cache-control-only `[[headers]]` block). */\nconst SECURITY_HEADER_RE =\n /Strict-Transport-Security|Content-Security-Policy|X-Frame-Options|X-Content-Type-Options|Referrer-Policy|Permissions-Policy|Cross-Origin-Opener-Policy/i;\n\n/** A site's `netlify.toml` is \"compliant\" — and left untouched by sync — once it\n * carries a `[[headers]]` block AND a security header (HSTS/CSP/X-Frame-Options/…).\n *\n * Like svelte.config, netlify.toml legitimately holds site-specific config\n * (custom CSP, redirects, per-route headers). The canonical template ships the\n * baseline security headers, but an exact overwrite would CLOBBER a site's own\n * hardening — that bug stripped gallerysonder's headers on a routine sync\n * (2026-06-10). So a genuinely-hardened file is left alone, while a missing,\n * header-less (previously-stripped), OR merely cache-header file is non-compliant\n * and gets the canonical template, which backfills the security baseline. */\nfunction isNetlifyConfigCompliant(contents: string): boolean {\n return contents.includes(\"[[headers]]\") && SECURITY_HEADER_RE.test(contents);\n}\n\n/** Runtime enumeration of every `ConfigName`. Mirror of the union in\n * `src/types.ts`. Used by CLI `--only` validation; a missing entry would\n * silently accept typos. The type-test in `tests/types.test.ts` guards\n * against drift between this array and the union. */\nexport const ALL_CONFIG_NAMES: ConfigName[] = [\n \"lighthouse\",\n \"eslint\",\n \"prettier\",\n \"prettier-ignore\",\n \"playwright-a11y\",\n \"svelte\",\n \"gitignore\",\n \"ci\",\n \"renovate-action\",\n \"renovate-config\",\n \"netlify\",\n];\n\nexport function isConfigName(value: string): value is ConfigName {\n return (ALL_CONFIG_NAMES as string[]).includes(value);\n}\n\nasync function readMaybe(path: string): Promise<string | null> {\n try {\n return await readFile(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nasync function planTemplateDiffs(\n cwd: string,\n templates: ConfigTemplate[],\n): Promise<ConfigTemplate[]> {\n const diffs: ConfigTemplate[] = [];\n for (const t of templates) {\n const existing = await readMaybe(join(cwd, t.path));\n if (existing === t.contents) continue;\n // svelte.config is compliance-checked, not exact-matched: an existing config\n // already on the canonical pattern is left alone so its aliases/compilerOptions\n // survive. A missing (null) or off-pattern config still gets the canonical template.\n if (t.config === SVELTE_CONFIG && existing !== null && isSvelteConfigCompliant(existing)) {\n continue;\n }\n // netlify.toml is likewise compliance-checked: a file that already carries\n // `[[headers]]` is hardened and left alone (an exact overwrite would strip\n // its security headers). A header-less / missing file gets the template.\n if (t.config === NETLIFY_CONFIG && existing !== null && isNetlifyConfigCompliant(existing)) {\n continue;\n }\n diffs.push(t);\n }\n return diffs;\n}\n\ntype GitignorePlan =\n | { kind: \"noop\" }\n | { kind: \"apply\"; content: string; toUntrack: string[]; added: string[] };\n\nasync function planGitignore(cwd: string): Promise<GitignorePlan> {\n const existing = await readMaybe(join(cwd, \".gitignore\"));\n const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);\n const tracked = await listTrackedFiles(cwd);\n const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);\n if (merge.added.length === 0 && toUntrack.length === 0) return { kind: \"noop\" };\n return { kind: \"apply\", content: merge.content, toUntrack, added: merge.added };\n}\n\nasync function applyGitignore(\n cwd: string,\n plan: Extract<GitignorePlan, { kind: \"apply\" }>,\n): Promise<void> {\n await writeFile(join(cwd, \".gitignore\"), plan.content, \"utf-8\");\n if (plan.toUntrack.length > 0) {\n await removeFromIndex(cwd, plan.toUntrack);\n }\n}\n\nexport async function syncConfigs(\n site: Site,\n opts: SyncConfigsOptions = {},\n): Promise<RecipeResult> {\n const requested = opts.which ?? ALL_TEMPLATES.map((t) => t.config).concat(GITIGNORE_CONFIG);\n const templateNames = requested.filter((c): c is ConfigName => c !== GITIGNORE_CONFIG);\n const templates = templatesByName(templateNames);\n const includeGitignore = requested.includes(GITIGNORE_CONFIG);\n\n return withRecipe({\n name: \"sync-configs\",\n site,\n plan: async () => {\n const templateDiffs = await planTemplateDiffs(site.path, templates);\n const gitignorePlan: GitignorePlan = includeGitignore\n ? await planGitignore(site.path)\n : { kind: \"noop\" };\n if (templateDiffs.length === 0 && gitignorePlan.kind === \"noop\") {\n return { kind: \"noop\", notes: \"all targeted configs already match\" };\n }\n return { kind: \"apply\", plan: { templateDiffs, gitignorePlan } };\n },\n apply: async ({ templateDiffs, gitignorePlan }, { commit }) => {\n for (const t of templateDiffs) {\n const dest = join(site.path, t.path);\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, t.contents, \"utf-8\");\n await commit(`chore: sync ${t.config} config from @reddoorla/maintenance`);\n }\n if (gitignorePlan.kind === \"apply\") {\n await applyGitignore(site.path, gitignorePlan);\n await commit(`chore: sync gitignore from @reddoorla/maintenance`);\n }\n return { kind: \"ok\" };\n },\n });\n}\n","import type { ConfigName } from \"../../types.js\";\n\nexport type ConfigTemplate = {\n config: ConfigName;\n path: string;\n contents: string;\n};\n\nconst eslint: ConfigTemplate = {\n config: \"eslint\",\n path: \"eslint.config.js\",\n contents: `import { createEslintConfig } from \"@reddoorla/maintenance/configs/eslint\";\nimport svelteConfig from \"./svelte.config.js\";\n\nexport default createEslintConfig({ svelteConfig });\n`,\n};\n\nconst prettier: ConfigTemplate = {\n config: \"prettier\",\n path: \".prettierrc.json\",\n contents: `{\n \"trailingComma\": \"all\",\n \"singleQuote\": false,\n \"printWidth\": 100,\n \"plugins\": [\"prettier-plugin-svelte\"]\n}\n`,\n};\n\nconst prettierIgnore: ConfigTemplate = {\n config: \"prettier-ignore\",\n path: \".prettierignore\",\n contents: `pnpm-lock.yaml\n.svelte-kit/\nbuild/\n.netlify/\ndist/\n`,\n};\n\nconst lighthouse: ConfigTemplate = {\n config: \"lighthouse\",\n path: \"lighthouserc.json\",\n contents: `${JSON.stringify(\n {\n $note:\n \"Generated by @reddoorla/maintenance sync-configs; edit src/configs/lighthouse.ts in the package instead.\",\n extends: \"@reddoorla/maintenance/configs/lighthouse\",\n },\n null,\n 2,\n )}\n`,\n};\n\nconst playwrightA11y: ConfigTemplate = {\n config: \"playwright-a11y\",\n path: \"playwright.config.ts\",\n contents: `export { default } from \"@reddoorla/maintenance/configs/playwright-a11y\";\n`,\n};\n\nconst svelte: ConfigTemplate = {\n config: \"svelte\",\n path: \"svelte.config.js\",\n contents: `import { createSvelteConfig } from \"@reddoorla/maintenance/configs/svelte\";\nimport adapter from \"@sveltejs/adapter-netlify\";\n\n/** @type {import('@sveltejs/kit').Config} */\nexport default createSvelteConfig({\n kit: { adapter: adapter({ edge: false, split: false }) },\n});\n`,\n};\n\n// The `ci:` job name below + the reusable workflow's `ci` job name produce the\n// branch-protection check context \"ci / ci\" — kept in sync with REQUIRED_CHECK in\n// src/recipes/self-updating/index.ts. Renaming this job means updating that constant.\nconst ci: ConfigTemplate = {\n config: \"ci\",\n path: \".github/workflows/ci.yml\",\n contents: `name: ci\non:\n pull_request:\n push:\n branches: [main]\njobs:\n ci:\n uses: reddoorla/.github/.github/workflows/ci.yml@78c4da64b675f0f474961f12715f2a4c09d46eb5 # v1.0.0\n`,\n};\n\nconst renovateAction: ConfigTemplate = {\n config: \"renovate-action\",\n path: \".github/workflows/renovate.yml\",\n contents: `name: renovate\non:\n schedule:\n - cron: \"0 7 * * 1\"\n workflow_dispatch:\njobs:\n renovate:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: renovatebot/github-action@v46.1.14\n with:\n token: \\${{ secrets.RENOVATE_TOKEN }}\n env:\n RENOVATE_REPOSITORIES: \\${{ github.repository }}\n`,\n};\n\nconst renovateConfig: ConfigTemplate = {\n config: \"renovate-config\",\n path: \"renovate.json\",\n contents: `{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\"github>reddoorla/.github:renovate-config\"]\n}\n`,\n};\n\nconst netlify: ConfigTemplate = {\n config: \"netlify\",\n path: \"netlify.toml\",\n contents: `[build]\n command = \"pnpm build\"\n publish = \"build/\"\n functions = \"functions/\"\n\n[build.environment]\n NODE_VERSION = \"22\"\n COREPACK_INTEGRITY_KEYS = \"0\"\n\n# Baseline security headers for all responses. CSP is emitted per-response by\n# SvelteKit (see \\`kit.csp\\` in svelte.config.js) so it is intentionally omitted\n# here to avoid conflicting duplicates.\n[[headers]]\n for = \"/*\"\n [headers.values]\n Strict-Transport-Security = \"max-age=63072000; includeSubDomains; preload\"\n X-Content-Type-Options = \"nosniff\"\n X-Frame-Options = \"SAMEORIGIN\"\n Referrer-Policy = \"strict-origin-when-cross-origin\"\n Permissions-Policy = \"camera=(), microphone=(), geolocation=(), interest-cohort=()\"\n Cross-Origin-Opener-Policy = \"same-origin\"\n\n[[headers]]\n for = \"/favicon.png\"\n [headers.values]\n Cache-Control = \"public, max-age=31536000, immutable\"\n\n[[headers]]\n for = \"/_app/immutable/*\"\n [headers.values]\n Cache-Control = \"public, max-age=31536000, immutable\"\n`,\n};\n\nexport const ALL_TEMPLATES: ConfigTemplate[] = [\n eslint,\n prettier,\n prettierIgnore,\n lighthouse,\n playwrightA11y,\n svelte,\n ci,\n renovateAction,\n renovateConfig,\n netlify,\n];\n\nexport function templatesByName(which: ConfigName[]): ConfigTemplate[] {\n return ALL_TEMPLATES.filter((t) => which.includes(t.config));\n}\n","/**\n * Comment line written above the appended block so future runs (and humans)\n * can recognize the managed section. Presence of this line is incidental —\n * the merge logic is keyed on each entry's normalized form, not on the marker.\n */\nexport const MANAGED_MARKER = \"# canonical entries from @reddoorla/maintenance sync-configs\";\n\n/**\n * Build artifacts, test outputs, deploy caches, and secrets that should never\n * be tracked across the reddoor fleet. Sites may keep additional site-specific\n * entries — they are preserved on merge.\n */\nexport const CANONICAL_GITIGNORE_ENTRIES: readonly string[] = [\n \"node_modules/\",\n \"build/\",\n \"dist/\",\n \".svelte-kit/\",\n \"coverage/\",\n \".vitest-cache/\",\n \"playwright-report/\",\n \"test-results/\",\n \".lighthouseci/\",\n \".tsbuildinfo\",\n \".env\",\n \".env.*\",\n \"!.env.example\",\n \".DS_Store\",\n \"*.log\",\n \".vercel/\",\n \".netlify/\",\n \".reddoor-a11y/\",\n // The a11y audit's transient spec dir, written inside the checkout and\n // normally cleaned, but a timeout-SIGKILL of the parent orphans it. Ignored\n // fleet-wide so it never dirties a self-updating repo's tree (2026-06-10 M-D).\n \".reddoor-a11y-spec-*/\",\n];\n\nexport type MergeResult = { content: string; added: string[] };\n\nfunction stripLeadingSlash(s: string): string {\n return s.startsWith(\"/\") ? s.slice(1) : s;\n}\n\nfunction stripTrailingSlash(s: string): string {\n return s.endsWith(\"/\") ? s.slice(0, -1) : s;\n}\n\n/**\n * Normalize for presence comparison only: strip leading `/`, trailing `/`,\n * and surrounding whitespace. `build`, `/build`, `build/`, and `/build/` all\n * collapse to the same key.\n */\nfunction normalizePresence(line: string): string {\n return stripTrailingSlash(stripLeadingSlash(line.trim()));\n}\n\nfunction presentSet(existing: string): Set<string> {\n const set = new Set<string>();\n for (const raw of existing.split(/\\r?\\n/)) {\n const trimmed = raw.trim();\n if (!trimmed) continue;\n if (trimmed.startsWith(\"#\")) continue;\n set.add(normalizePresence(trimmed));\n }\n return set;\n}\n\n/**\n * Merge `canonical` entries into `existing` .gitignore content.\n *\n * - Missing entries are appended under a managed marker comment.\n * - Existing entries (in any normalized variant — `/build`, `build/`, etc.)\n * are preserved as-is; we never rewrite the site's own lines.\n * - When every canonical entry is already present, returns the original\n * content unchanged with `added: []` — the recipe can treat that as noop.\n */\nexport function mergeGitignore(existing: string | null, canonical: readonly string[]): MergeResult {\n if (existing === null) {\n const body = [MANAGED_MARKER, ...canonical].join(\"\\n\") + \"\\n\";\n return { content: body, added: [...canonical] };\n }\n const present = presentSet(existing);\n const added: string[] = [];\n for (const entry of canonical) {\n const norm = normalizePresence(entry);\n if (!present.has(norm)) {\n added.push(entry);\n present.add(norm);\n }\n }\n if (added.length === 0) {\n return { content: existing, added: [] };\n }\n let base = existing;\n if (!base.endsWith(\"\\n\")) base += \"\\n\";\n const block = [\"\", MANAGED_MARKER, ...added].join(\"\\n\") + \"\\n\";\n return { content: base + block, added };\n}\n\n/**\n * Of the tracked paths, return those that fall under a canonical *directory*\n * entry — i.e., paths that the freshly-synced .gitignore now wants ignored\n * but which git currently has in the index.\n *\n * File-pattern entries (`.env`, `*.log`, `.DS_Store`) are intentionally\n * skipped: they may contain user-meaningful data, and `git rm --cached`\n * cannot scrub secrets from history anyway. Surfaced for manual review\n * instead of auto-removing.\n */\nexport function findTrackedArtifacts(\n tracked: readonly string[],\n canonical: readonly string[],\n): string[] {\n const dirEntries: string[] = [];\n for (const raw of canonical) {\n const t = raw.trim();\n if (!t) continue;\n if (t.startsWith(\"!\")) continue;\n if (/[*?[]/.test(t)) continue;\n const noLead = stripLeadingSlash(t);\n if (!noLead.endsWith(\"/\")) continue;\n const name = stripTrailingSlash(noLead);\n if (!name) continue;\n dirEntries.push(name);\n }\n const matched: string[] = [];\n for (const path of tracked) {\n for (const dir of dirEntries) {\n if (path === dir || path.startsWith(dir + \"/\")) {\n matched.push(path);\n break;\n }\n }\n }\n return matched;\n}\n","import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst exec = promisify(execFile);\n\nasync function git(cwd: string, args: string[]): Promise<{ stdout: string; stderr: string }> {\n return exec(\"git\", args, { cwd, env: process.env });\n}\n\nexport function branchName(recipe: string, when: Date = new Date()): string {\n // ISO with millisecond precision: 2026-05-20T10:30:00.123Z → 20260520T103000123Z.\n // Millis (vs. second-precision) shrinks the collision window for parallel runs.\n const compact = when.toISOString().replace(/[-:.]/g, \"\");\n return `maint/${recipe}-${compact}`;\n}\n\nexport async function currentBranch(cwd: string): Promise<string> {\n const { stdout } = await git(cwd, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n return stdout.trim();\n}\n\nexport async function isWorkingTreeClean(cwd: string): Promise<boolean> {\n const { stdout } = await git(cwd, [\"status\", \"--porcelain\"]);\n return stdout.trim().length === 0;\n}\n\nexport async function createBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", \"-b\", name]);\n}\n\n/** Check out an existing branch. Throws (via git) if the branch is missing or\n * the checkout is blocked (e.g. uncommitted changes that would be overwritten). */\nexport async function checkoutBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", name]);\n}\n\n/**\n * Force-check-out an existing branch, DISCARDING any uncommitted changes on the\n * current branch. Used by the recipe failure path to return the operator to\n * their original branch even when a recipe left the work-in-progress branch\n * dirty. Only ever called with the operator's ORIGINAL branch as `name`. Does\n * not run `git clean`, so untracked operator files are left untouched.\n */\nexport async function forceCheckoutBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", \"-f\", name]);\n}\n\n/**\n * Delete a local branch with `-D` (force). Used by the recipe failure path to\n * remove the branch the recipe itself created so a re-run starts clean. Callers\n * MUST only ever pass the recipe-created branch here, never the operator's\n * original branch.\n */\nexport async function deleteBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"branch\", \"-D\", name]);\n}\n\nexport async function stageAll(cwd: string): Promise<void> {\n await git(cwd, [\"add\", \"-A\"]);\n}\n\nexport async function listTrackedFiles(cwd: string): Promise<string[]> {\n const { stdout } = await git(cwd, [\"ls-files\"]);\n return stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n}\n\nexport async function removeFromIndex(cwd: string, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n await git(cwd, [\"rm\", \"-r\", \"--cached\", \"--\", ...paths]);\n}\n\n/**\n * Stages all current changes and commits with `message`. Returns the commit SHA,\n * or `null` if there was nothing to commit.\n */\nexport async function commit(cwd: string, message: string): Promise<string | null> {\n await stageAll(cwd);\n const { stdout: status } = await git(cwd, [\"status\", \"--porcelain\"]);\n if (status.trim().length === 0) return null;\n await git(cwd, [\"commit\", \"-m\", message]);\n const { stdout: sha } = await git(cwd, [\"rev-parse\", \"HEAD\"]);\n return sha.trim();\n}\n\n/**\n * Strict GitHub repo identity: exactly two `[A-Za-z0-9._-]` segments separated\n * by a single slash. Rejects a scheme, host, extra path segment, traversal\n * (`..`), whitespace, or an argv flag — anything that could retarget a `gh`\n * write at an unintended (attacker/typo-controlled) repo.\n */\nexport const OWNER_REPO_RE = /^[A-Za-z0-9._-]+\\/[A-Za-z0-9._-]+$/;\n\n/** True when `repo` is a clean `owner/repo` (see {@link OWNER_REPO_RE}). The\n * explicit `..` reject covers traversal segments the char-class would otherwise\n * admit (`.` is a legal repo char, so `../evil` matches the shape regex). */\nexport function isOwnerRepo(repo: string): boolean {\n if (repo.includes(\"..\")) return false;\n return OWNER_REPO_RE.test(repo);\n}\n\n/**\n * True when two repo references name the same `owner/repo`. Each side may be a\n * full remote URL (https or scp-style), or already an `owner/repo`. Used to\n * verify an existing checkout's `origin` matches the site's expected repo\n * before reusing it. Returns false if either side is unparseable.\n */\nexport function sameOwnerRepo(a: string, b: string): boolean {\n const na = isOwnerRepo(a.trim()) ? a.trim() : parseOwnerRepo(a);\n const nb = isOwnerRepo(b.trim()) ? b.trim() : parseOwnerRepo(b);\n if (na === null || nb === null) return false;\n return na.toLowerCase() === nb.toLowerCase();\n}\n\n/** Derive `owner/repo` from a git remote URL (https or scp-style). Null if unparseable. */\nexport function parseOwnerRepo(remoteUrl: string): string | null {\n const trimmed = remoteUrl\n .trim()\n .replace(/\\.git$/, \"\")\n .replace(/\\/$/, \"\");\n // scp-style: git@github.com:owner/repo\n const scp = trimmed.match(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:(.+)$/);\n const path = scp ? scp[1]! : trimmed.replace(/^https?:\\/\\/[^/]+\\//, \"\");\n const segments = path.split(\"/\").filter(Boolean);\n if (segments.length < 2) return null;\n return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;\n}\n\n/** `origin` remote URL for a checkout, trimmed. Throws (via git) if there's no origin. */\nexport async function getRemoteUrl(cwd: string): Promise<string> {\n const { stdout } = await git(cwd, [\"remote\", \"get-url\", \"origin\"]);\n return stdout.trim();\n}\n\n/** Push a branch to origin, setting upstream. Throws on non-zero (execFile rejects). */\nexport async function push(cwd: string, branch: string): Promise<void> {\n await git(cwd, [\"push\", \"-u\", \"origin\", branch]);\n}\n","import type { RecipeName, RecipeResult, Site } from \"../types.js\";\nimport {\n branchName,\n checkoutBranch,\n commit as gitCommit,\n createBranch,\n currentBranch,\n deleteBranch,\n forceCheckoutBranch,\n isWorkingTreeClean,\n} from \"../util/git.js\";\nimport { siteLabel } from \"../util/site.js\";\n\n/** Outcome of the read-only planning phase. `noop` and `failed` short-circuit\n * without creating a branch; `apply` carries the recipe-specific plan data\n * forward to the apply phase. */\nexport type RecipePlan<P> =\n | { kind: \"noop\"; notes?: string }\n | { kind: \"failed\"; notes: string }\n | { kind: \"apply\"; plan: P };\n\nexport type RecipeApplyCtx = {\n /** Stage all current changes and commit. Returns the SHA, or null if\n * nothing was staged. The wrapper accumulates SHAs into the final\n * RecipeResult. */\n commit: (message: string) => Promise<string | null>;\n /** Branch name that was created for this run. */\n branch: string;\n /** Site path — same as `site.path`. */\n cwd: string;\n};\n\nexport type RecipeApplyResult = { kind: \"ok\"; notes?: string } | { kind: \"failed\"; notes: string };\n\nexport type RecipeBody<P> = {\n name: RecipeName;\n site: Site;\n /** Inspect the site and decide: noop, failed, or proceed (with plan data\n * passed to apply). Runs before the working-tree clean check unless\n * `checkTreeFirst: true` is set, so most recipes can noop on a dirty\n * tree without throwing. */\n plan: () => Promise<RecipePlan<P>>;\n /** Make the actual changes. Use `ctx.commit(msg)` for each logical step;\n * the wrapper collects SHAs into `RecipeResult.commits`. Return\n * `{ kind: \"failed\", notes }` to abort partway and surface the failure. */\n apply: (plan: P, ctx: RecipeApplyCtx) => Promise<RecipeApplyResult>;\n /** Check working tree clean BEFORE `plan()` runs. Use only when plan\n * itself mutates the tree (e.g. `bump-deps` runs `pnpm install` in plan\n * for an accurate outdated probe). Default false — clean check happens\n * after plan only if plan returns proceed, allowing noop-on-dirty for\n * read-only plans (a tree with stray edits + no recipe work to do\n * should not throw). */\n checkTreeFirst?: boolean;\n};\n\n/** Wrap a recipe's plan/apply phases. Centralises the siteLabel /\n * clean-tree check / branch creation / commit accumulation / RecipeResult\n * construction boilerplate that every recipe used to re-implement. */\nexport async function withRecipe<P>(body: RecipeBody<P>): Promise<RecipeResult> {\n const label = siteLabel(body.site);\n\n if (body.checkTreeFirst && !(await isWorkingTreeClean(body.site.path))) {\n throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);\n }\n\n const planned = await body.plan();\n\n if (planned.kind === \"noop\") {\n return {\n recipe: body.name,\n site: label,\n status: \"noop\",\n commits: [],\n ...(planned.notes ? { notes: planned.notes } : {}),\n };\n }\n if (planned.kind === \"failed\") {\n return {\n recipe: body.name,\n site: label,\n status: \"failed\",\n commits: [],\n notes: planned.notes,\n };\n }\n\n if (!body.checkTreeFirst && !(await isWorkingTreeClean(body.site.path))) {\n throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);\n }\n\n // Capture the operator's branch BEFORE we create the recipe branch, so we can\n // return them to it afterwards (#2) and so the failure path (#3) knows which\n // branch to force-restore to. Best-effort: if we can't read it (detached HEAD,\n // git error) we proceed with `original = null` and skip any force operations\n // rather than guess — we must NEVER force-discard/delete a branch we're unsure\n // of.\n let original: string | null = null;\n try {\n original = await currentBranch(body.site.path);\n } catch {\n original = null;\n }\n\n const branch = branchName(body.name);\n await createBranch(body.site.path, branch);\n\n /**\n * Best-effort restore to the operator's original branch. Never throws — a\n * restore failure must not turn an otherwise-clean recipe result into a\n * failure (#2). Skipped when we couldn't capture the original branch.\n *\n * IMPORTANT (composition): this is invoked only on the NOOP-from-apply path\n * (the recipe created a branch but committed nothing — leaving the operator\n * parked on an empty maint branch is pure downside). It is deliberately NOT\n * invoked on the APPLIED path: the fleet onboarding pipeline composes recipes\n * by running them in sequence against the SAME checkout, each building on the\n * prior's committed files in the working tree (convert-to-pnpm's lockfile →\n * onboard's deps → sync-configs → svelte-codemods). Restoring to the base\n * branch after an applied recipe would strip those files from the working\n * tree and break composition (verified live across the fleet). selfUpdating,\n * which PUSHES its branch, does its own post-push restore since its local\n * branch is disposable.\n */\n const restoreOriginal = async (): Promise<void> => {\n if (original === null || original === branch) return;\n try {\n await checkoutBranch(body.site.path, original);\n } catch (err) {\n // Leave the operator on the recipe branch rather than fail the result.\n console.warn(\n `warning: could not restore branch ${original} after ${body.name}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n };\n\n /**\n * Failure restore (#3): force the checkout back to the captured original\n * branch (discarding the recipe branch's uncommitted changes) and delete the\n * recipe branch, so a re-run starts clean. SAFETY: only ever force-checks-out\n * the captured `original` and only ever deletes `branch` (the recipe-created\n * branch); never deletes `original`, never runs `git clean`. If `original` is\n * unavailable we do nothing (the safe subset) — better to leave the operator\n * parked than to force anything we're unsure about. Best-effort: never throws.\n */\n const restoreAfterFailure = async (): Promise<void> => {\n if (original === null || original === branch) return;\n try {\n await forceCheckoutBranch(body.site.path, original);\n } catch (err) {\n console.warn(\n `warning: could not force-restore branch ${original} after failed ${body.name}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n // If we couldn't get off the recipe branch, deleting it would fail anyway;\n // and we must never delete the branch we're still on.\n return;\n }\n try {\n await deleteBranch(body.site.path, branch);\n } catch (err) {\n console.warn(\n `warning: could not delete recipe branch ${branch} after failed ${body.name}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n };\n\n const shas: string[] = [];\n let result: RecipeApplyResult;\n try {\n result = await body.apply(planned.plan, {\n cwd: body.site.path,\n branch,\n commit: async (msg) => {\n const sha = await gitCommit(body.site.path, msg);\n if (sha) shas.push(sha);\n return sha;\n },\n });\n } catch (err) {\n // Body threw mid-mutation: force-restore + delete the recipe branch so the\n // checkout is retriable, then re-throw (preserve the prior throw semantics —\n // callers like init treat an uncaught throw as an `error` step).\n await restoreAfterFailure();\n throw err;\n }\n\n if (result.kind === \"failed\") {\n await restoreAfterFailure();\n return {\n recipe: body.name,\n site: label,\n status: \"failed\",\n commits: shas,\n notes: result.notes,\n };\n }\n\n // NOOP-from-apply only: no commits to compose, so don't leave the operator\n // parked on an empty maint branch. The APPLIED path intentionally stays on the\n // maint branch so the onboarding pipeline can compose (see restoreOriginal).\n if (shas.length === 0) {\n await restoreOriginal();\n }\n\n const notes = result.notes ? `${result.notes}; branch: ${branch}` : `branch: ${branch}`;\n return {\n recipe: body.name,\n site: label,\n status: shas.length > 0 ? \"applied\" : \"noop\",\n commits: shas,\n notes,\n };\n}\n","import { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../types.js\";\nimport { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type BumpDepsGroup = \"patch\" | \"minor\" | \"major\";\n\nexport type BumpDepsOptions = {\n group?: BumpDepsGroup;\n spawn?: SpawnFn;\n};\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction outdatedFlagsForGroup(group: BumpDepsGroup): string[] {\n if (group === \"major\") return [\"--latest\"];\n if (group === \"minor\") return [];\n return [\"--depth\", \"0\"];\n}\n\nfunction upFlagsForGroup(group: BumpDepsGroup): string[] {\n if (group === \"major\") return [\"--latest\"];\n return [];\n}\n\ntype Plan = { group: BumpDepsGroup };\n\nexport async function bumpDeps(site: Site, opts: BumpDepsOptions = {}): Promise<RecipeResult> {\n const group: BumpDepsGroup = opts.group ?? \"minor\";\n const spawn = opts.spawn ?? defaultSpawn;\n\n return withRecipe<Plan>({\n name: \"bump-deps\",\n site,\n // pnpm install (in plan) mutates the lockfile, so the clean-tree check\n // MUST happen first — otherwise a desynced-lockfile resync would silently\n // land on top of whatever else was in the tree.\n checkTreeFirst: true,\n plan: async () => {\n // Pre-flight: the recipe is pnpm-only. A package-lock.json or yarn.lock\n // without pnpm-lock.yaml means the site is still on a different package\n // manager; we refuse to run rather than emit confusing pnpm errors.\n const hasPnpmLock = await exists(join(site.path, \"pnpm-lock.yaml\"));\n if (!hasPnpmLock) {\n const hasNpmLock = await exists(join(site.path, \"package-lock.json\"));\n const hasYarnLock = await exists(join(site.path, \"yarn.lock\"));\n if (hasNpmLock || hasYarnLock) {\n const competing = hasNpmLock ? \"package-lock.json\" : \"yarn.lock\";\n return {\n kind: \"failed\",\n notes: `site has ${competing} but no pnpm-lock.yaml — run convert-to-pnpm first`,\n };\n }\n }\n\n // Ensure the lockfile reflects the current package.json before we ask\n // pnpm what's outdated. Without this, a desynced lockfile can produce\n // stale or empty outdated reports.\n await spawn(\"pnpm\", [\"install\"], { cwd: site.path, streaming: true });\n\n const outdated = await spawn(\n \"pnpm\",\n [\"outdated\", \"--json\", ...outdatedFlagsForGroup(group)],\n { cwd: site.path },\n );\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(outdated.stdout || \"{}\") as Record<string, unknown>;\n } catch {\n parsed = {};\n }\n if (Object.keys(parsed).length === 0) {\n return { kind: \"noop\", notes: `pnpm outdated reported nothing for group=${group}` };\n }\n return { kind: \"apply\", plan: { group } };\n },\n apply: async ({ group: g }, { commit, cwd }) => {\n // Stream pnpm up's output so long-running upgrades don't look frozen.\n await spawn(\"pnpm\", [\"up\", ...upFlagsForGroup(g)], { cwd, streaming: true });\n await commit(`chore(deps): bump dependencies (${g})`);\n return { kind: \"ok\" };\n },\n });\n}\n","import { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { readPackageJson } from \"../../util/pkg.js\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\nimport { bumpToSvelte5Versions } from \"./step-bump-versions.js\";\nimport { migrateSvelteConfig } from \"./step-svelte-config.js\";\nimport { runSvelteMigrate } from \"./step-svelte-migrate.js\";\nimport { upgradeTailwind } from \"./step-tailwind-upgrade.js\";\nimport { applyGotchaCodemods } from \"./step-gotchas.js\";\nimport { verifyMigration } from \"./step-verify.js\";\nimport { writeMigrationSummary } from \"./step-summary.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\n\nexport type UpgradeSvelte4to5Options = {\n spawn?: SpawnFn;\n};\n\nasync function alreadyOnSvelte5(cwd: string): Promise<boolean> {\n try {\n const pkg = await readPackageJson(join(cwd, \"package.json\"));\n const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;\n return !!v && /^\\^?5\\./.test(v);\n } catch {\n return false;\n }\n}\n\nexport async function upgradeSvelte4to5(\n site: Site,\n opts: UpgradeSvelte4to5Options = {},\n): Promise<RecipeResult> {\n const spawn = opts.spawn ?? defaultSpawn;\n\n return withRecipe<true>({\n name: \"svelte-4-to-5\",\n site,\n plan: async () => {\n if (await alreadyOnSvelte5(site.path)) {\n return { kind: \"noop\", notes: \"site already declares svelte ^5.x\" };\n }\n return { kind: \"apply\", plan: true };\n },\n apply: async (_plan, { commit, cwd }) => {\n const bumped = await bumpToSvelte5Versions(cwd);\n if (bumped) {\n await commit(\"chore(svelte5): bump svelte/kit/vite/vite-plugin-svelte\");\n }\n\n const configChanged = await migrateSvelteConfig(cwd);\n if (configChanged) {\n await commit(\"refactor(svelte5): migrate svelte.config.js (drop vitePreprocess)\");\n }\n\n const migrate = await runSvelteMigrate(cwd, spawn);\n if (migrate.ran) {\n await commit(\"refactor(svelte5): run official svelte-migrate codemod\");\n }\n\n const tw = await upgradeTailwind(cwd, spawn);\n if (tw.ran) {\n await commit(\"chore(svelte5): tailwindcss 3 → 4 upgrade\");\n }\n\n const codemods = await applyGotchaCodemods(cwd);\n if (codemods.filesChanged > 0) {\n await commit(`refactor(svelte5): apply gotcha codemods (${codemods.filesChanged} files)`);\n }\n\n await verifyMigration(cwd, spawn);\n await commit(\"chore(svelte5): pnpm install + check\");\n\n await writeMigrationSummary({\n cwd,\n filesChangedByCodemods: codemods.filesChanged,\n svelteMigrateRan: migrate.ran,\n tailwindUpgraded: tw.ran,\n });\n await commit(\"docs(svelte5): add MIGRATION_SVELTE_5.md summary\");\n\n return { kind: \"ok\" };\n },\n });\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\n\nexport type PackageJsonLike = {\n name?: string;\n version?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n [key: string]: unknown;\n};\n\nexport async function readPackageJson(path: string): Promise<PackageJsonLike> {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as PackageJsonLike;\n}\n\n/** Sniff the indent style (tab vs 2 vs 4 vs N spaces) from existing package.json\n * content by looking at the first indented `\"key\"` line. Defaults to two spaces. */\nfunction detectIndentFromContent(raw: string): string {\n const match = raw.match(/\\n([ \\t]+)\"/);\n return match ? (match[1] ?? \" \") : \" \";\n}\n\nexport async function writePackageJson(path: string, pkg: PackageJsonLike): Promise<void> {\n let indent = \" \";\n try {\n const existing = await readFile(path, \"utf-8\");\n indent = detectIndentFromContent(existing);\n } catch {\n // file doesn't exist yet — first write — keep the 2-space default\n }\n const content = JSON.stringify(pkg, null, indent) + \"\\n\";\n await writeFile(path, content, \"utf-8\");\n}\n\nexport type BumpDepMode =\n | \"ensure\" // default: add to devDependencies if missing\n | \"bump-only\"; // never add; only update existing entries\n\nexport type BumpDepOptions = {\n mode?: BumpDepMode;\n};\n\nexport function bumpDep(\n pkg: PackageJsonLike,\n name: string,\n version: string,\n opts: BumpDepOptions = {},\n): PackageJsonLike {\n const mode = opts.mode ?? \"ensure\";\n\n const next: PackageJsonLike = {\n ...pkg,\n };\n\n if (pkg.dependencies) {\n next.dependencies = { ...pkg.dependencies };\n }\n if (pkg.devDependencies) {\n next.devDependencies = { ...pkg.devDependencies };\n }\n\n if (next.dependencies && name in next.dependencies) {\n if (next.dependencies[name] === version) return pkg;\n next.dependencies[name] = version;\n return next;\n }\n if (next.devDependencies && name in next.devDependencies) {\n if (next.devDependencies[name] === version) return pkg;\n next.devDependencies[name] = version;\n return next;\n }\n // Not present in either map. bump-only leaves the pkg alone so recipes\n // can express \"raise the floor on packages this site already uses\" without\n // also installing every related dep across the fleet.\n if (mode === \"bump-only\") return pkg;\n next.devDependencies = { ...(next.devDependencies ?? {}), [name]: version };\n return next;\n}\n","import { join } from \"node:path\";\nimport { readPackageJson, writePackageJson, bumpDep } from \"../../util/pkg.js\";\n\nconst SVELTE_5_VERSIONS: Record<string, string> = {\n svelte: \"^5.55.5\",\n \"@sveltejs/kit\": \"^2.59.0\",\n \"@sveltejs/vite-plugin-svelte\": \"^7.0.0\",\n \"@sveltejs/adapter-netlify\": \"^6.0.4\",\n \"@sveltejs/adapter-auto\": \"^7.0.0\",\n vite: \"^8.0.10\",\n \"svelte-check\": \"^4.4.7\",\n typescript: \"^6.0.3\",\n \"typescript-svelte-plugin\": \"^0.3.52\",\n};\n\nexport async function bumpToSvelte5Versions(cwd: string): Promise<boolean> {\n const pkgPath = join(cwd, \"package.json\");\n const pkg = await readPackageJson(pkgPath);\n let next = pkg;\n // bump-only: a svelte-4 site that doesn't declare e.g. adapter-netlify\n // should not get it added during the upgrade.\n for (const [name, version] of Object.entries(SVELTE_5_VERSIONS)) {\n next = bumpDep(next, name, version, { mode: \"bump-only\" });\n }\n if (next === pkg) return false;\n await writePackageJson(pkgPath, next);\n return true;\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nconst VITE_PLUGIN_PKG = \"@sveltejs/vite-plugin-svelte\";\n\n/** Match an import statement that pulls one or more named bindings from\n * `@sveltejs/vite-plugin-svelte`. Group 1 is the comma-separated name list. */\nconst IMPORT_FROM_VITE_PLUGIN = new RegExp(\n String.raw`^import\\s+\\{\\s*([^}]+?)\\s*\\}\\s+from\\s+[\"']` +\n VITE_PLUGIN_PKG.replace(/[/]/g, \"\\\\/\") +\n String.raw`[\"'];?[ \\t]*\\n`,\n \"m\",\n);\n\n/** Rewrite the import to drop only `vitePreprocess`, preserving any other\n * named bindings. If `vitePreprocess` was the sole import, the whole line\n * is removed. */\nfunction dropVitePreprocessImport(source: string): string {\n return source.replace(IMPORT_FROM_VITE_PLUGIN, (full, names: string) => {\n const remaining = names\n .split(\",\")\n .map((n) => n.trim())\n .filter((n) => n.length > 0 && n !== \"vitePreprocess\");\n if (remaining.length === 0) return \"\"; // drop entire line including its trailing newline\n return `import { ${remaining.join(\", \")} } from \"${VITE_PLUGIN_PKG}\";\\n`;\n });\n}\n\n/** Find the end of a balanced-paren call starting at `openIdx`, which must\n * point at the `(` character. Returns the index of the matching `)`, or -1\n * if unbalanced. */\nfunction findMatchingParen(source: string, openIdx: number): number {\n if (source[openIdx] !== \"(\") return -1;\n let depth = 0;\n for (let i = openIdx; i < source.length; i++) {\n const ch = source[i];\n if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n if (depth === 0) return i;\n }\n }\n return -1;\n}\n\n/** Remove a `preprocess: vitePreprocess(<anything>),?` key from a config\n * object. Handles the call with empty parens or with an options object. */\nfunction dropPreprocessKey(source: string): string {\n // Anchor on the start of the preprocess key on its own line so we don't\n // also strip whitespace / commas from neighboring keys.\n const startRe = /^(\\s*)preprocess:\\s*vitePreprocess\\(/m;\n const m = startRe.exec(source);\n if (!m) return source;\n\n const indent = m[1] ?? \"\";\n const parenOpenAbs = m.index + m[0].length - 1; // points at `(`\n const parenCloseAbs = findMatchingParen(source, parenOpenAbs);\n if (parenCloseAbs < 0) return source;\n\n // Consume an optional trailing comma and whitespace through end-of-line.\n let tailIdx = parenCloseAbs + 1;\n while (tailIdx < source.length && /[ \\t,]/.test(source[tailIdx] ?? \"\")) tailIdx++;\n if (source[tailIdx] === \"\\n\") tailIdx++;\n\n return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\\\n`), \"\");\n}\n\nexport async function migrateSvelteConfig(cwd: string): Promise<boolean> {\n const path = join(cwd, \"svelte.config.js\");\n let src: string;\n try {\n src = await readFile(path, \"utf-8\");\n } catch {\n return false;\n }\n\n let next = src;\n next = dropPreprocessKey(next);\n next = dropVitePreprocessImport(next);\n\n if (next === src) return false;\n await writeFile(path, next, \"utf-8\");\n return true;\n}\n","import { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\n\nexport async function runSvelteMigrate(\n cwd: string,\n spawn: SpawnFn = defaultSpawn,\n): Promise<{ ran: boolean; stderr: string }> {\n try {\n const { code, stderr } = await spawn(\n \"npx\",\n [\"--yes\", \"svelte-migrate\", \"svelte-5\", \"--no-install\"],\n { cwd, timeoutMs: 5 * 60_000 },\n );\n if (code !== 0) {\n return { ran: false, stderr };\n }\n return { ran: true, stderr };\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return { ran: false, stderr: \"npx unavailable\" };\n }\n throw err;\n }\n}\n","import { readPackageJson } from \"../../util/pkg.js\";\nimport { join } from \"node:path\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\n\nexport async function upgradeTailwind(\n cwd: string,\n spawn: SpawnFn = defaultSpawn,\n): Promise<{ ran: boolean; reason?: string }> {\n const pkg = await readPackageJson(join(cwd, \"package.json\"));\n const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;\n if (!tailwindVersion) return { ran: false, reason: \"tailwindcss not installed\" };\n if (/^\\^?4\\./.test(tailwindVersion)) return { ran: false, reason: \"already on tailwind 4.x\" };\n\n try {\n const { code, stderr } = await spawn(\"npx\", [\"--yes\", \"@tailwindcss/upgrade\", \"--force\"], {\n cwd,\n timeoutMs: 5 * 60_000,\n });\n if (code !== 0) return { ran: false, reason: stderr.slice(0, 200) };\n return { ran: true };\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return { ran: false, reason: \"npx unavailable\" };\n }\n throw err;\n }\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { glob } from \"tinyglobby\";\nimport { onEventToHandler } from \"./codemods/on-event-to-handler.js\";\nimport { exportLetToProps } from \"./codemods/dollar-props.js\";\nimport { removeDollarRestProps } from \"./codemods/dollar-restprops.js\";\nimport { stateEffectSyncToDerived } from \"./codemods/state-effect-sync.js\";\nimport { dollarPropsClass } from \"./codemods/dollar-props-class.js\";\nimport { legacyReactiveToRunes } from \"./codemods/legacy-reactive.js\";\n\nconst SVELTE_GLOBS = [\"src/**/*.svelte\"];\nconst IGNORE = [\"node_modules/**\", \".svelte-kit/**\", \"build/**\"];\n\ntype Codemod = (src: string) => string;\n\n// Order matters: exportLetToProps creates the $props() destructuring that\n// dollarPropsClass extends with a `class:` named prop.\nconst CODEMODS: Codemod[] = [\n onEventToHandler,\n exportLetToProps,\n removeDollarRestProps,\n stateEffectSyncToDerived,\n dollarPropsClass,\n legacyReactiveToRunes,\n];\n\nexport type CodemodChange = { rel: string; after: string };\n\nexport async function planGotchaCodemods(cwd: string): Promise<CodemodChange[]> {\n const changes: CodemodChange[] = [];\n const relPaths = await glob(SVELTE_GLOBS, { cwd, ignore: IGNORE, absolute: false });\n for (const rel of relPaths) {\n const path = join(cwd, rel);\n const before = await readFile(path, \"utf-8\");\n const after = CODEMODS.reduce((s, fn) => fn(s), before);\n if (after !== before) changes.push({ rel, after });\n }\n return changes;\n}\n\nexport async function applyGotchaCodemods(cwd: string): Promise<{ filesChanged: number }> {\n const changes = await planGotchaCodemods(cwd);\n for (const c of changes) {\n await writeFile(join(cwd, c.rel), c.after, \"utf-8\");\n }\n return { filesChanged: changes.length };\n}\n","const SCRIPT_BLOCK = /<script\\b[^>]*>[\\s\\S]*?<\\/script>/g;\nconst SIMPLE_ON_EVENT = /\\bon:([a-z]+)(?=\\s*=)/g;\nconst MODIFIER_EVENT = /\\bon:[a-z]+\\|[a-zA-Z]+(?=\\s*=)/g;\n\n/** Svelte 5 removed event modifier syntax (`on:click|preventDefault={fn}`).\n * The rewrite is non-trivial — the modifier behavior must be inlined into\n * the handler body — so this codemod doesn't attempt it automatically.\n * Instead it inserts a `@migration-task` marker immediately above each\n * offending element so the user gets a visible audit trail rather than\n * a silent build error from the Svelte 5 compiler. */\nfunction flagEventModifiers(source: string): string {\n const insertions: Array<{ tagStart: number; indent: string; modifier: string }> = [];\n let m: RegExpExecArray | null;\n MODIFIER_EVENT.lastIndex = 0;\n while ((m = MODIFIER_EVENT.exec(source)) !== null) {\n const tagStart = source.lastIndexOf(\"<\", m.index);\n if (tagStart === -1) continue;\n\n // Idempotency: if the line immediately above the tag already carries an\n // @migration-task marker for this site, don't double-insert on re-run.\n const prevLineEnd = tagStart - 1;\n if (prevLineEnd >= 0) {\n const prevLineStart = source.lastIndexOf(\"\\n\", prevLineEnd - 1) + 1;\n const prevLine = source.slice(prevLineStart, prevLineEnd + 1);\n if (/<!--\\s*@migration-task/.test(prevLine)) continue;\n }\n\n const lineStart = source.lastIndexOf(\"\\n\", tagStart - 1) + 1;\n const indent = source.slice(lineStart, tagStart);\n const safeIndent = /^[ \\t]*$/.test(indent) ? indent : \"\";\n insertions.push({ tagStart, indent: safeIndent, modifier: m[0] });\n }\n\n // Apply back-to-front so earlier insertion offsets stay valid.\n let out = source;\n for (let i = insertions.length - 1; i >= 0; i--) {\n const { tagStart, indent, modifier } = insertions[i]!;\n const comment = `<!-- @migration-task: Svelte 5 removed event modifier syntax (\\`${modifier}\\`). Rewrite inline, e.g. onclick={(e) => { e.preventDefault(); ... }}. -->\\n${indent}`;\n out = out.slice(0, tagStart) + comment + out.slice(tagStart);\n }\n return out;\n}\n\nexport function onEventToHandler(source: string): string {\n const masked: string[] = [];\n const placeholder = (i: number): string => ` SCRIPT_${i} `;\n const intermediate = source.replace(SCRIPT_BLOCK, (match) => {\n masked.push(match);\n return placeholder(masked.length - 1);\n });\n\n let processed = intermediate.replace(SIMPLE_ON_EVENT, (_full, name: string) => `on${name}`);\n processed = flagEventModifiers(processed);\n\n let out = processed;\n masked.forEach((blk, i) => {\n out = out.replace(placeholder(i), blk);\n });\n\n return out;\n}\n","const SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/;\nconst EXPORT_LET = /^\\s*export\\s+let\\s+(\\w+)\\s*(?::\\s*([^=;\\n]+))?\\s*(?:=\\s*([^;\\n]+))?;?\\s*$/gm;\n\ntype Prop = { name: string; type?: string | undefined; defaultExpr?: string | undefined };\n\nfunction transformScript(scriptBody: string, isTs: boolean): { body: string; changed: boolean } {\n const props: Prop[] = [];\n const cleaned = scriptBody.replace(\n EXPORT_LET,\n (_full, name: string, type?: string, defaultExpr?: string) => {\n props.push({\n name,\n type: type?.trim(),\n defaultExpr: defaultExpr?.trim(),\n });\n return \"\";\n },\n );\n if (props.length === 0) return { body: scriptBody, changed: false };\n\n const destructured = props\n .map((p) => (p.defaultExpr ? `${p.name} = ${p.defaultExpr}` : p.name))\n .join(\", \");\n\n let decl: string;\n if (isTs) {\n const typeSig = props\n .map((p) => {\n const optional = p.defaultExpr ? \"?\" : \"\";\n return `${p.name}${optional}: ${p.type ?? \"unknown\"}`;\n })\n .join(\"; \");\n decl = ` let { ${destructured} }: { ${typeSig} } = $props();`;\n } else {\n decl = ` let { ${destructured} } = $props();`;\n }\n\n const next = cleaned.replace(/^(\\s*)/, (m) => `${m}${decl}\\n`);\n return { body: next, changed: true };\n}\n\nexport function exportLetToProps(source: string): string {\n const match = source.match(SCRIPT_BLOCK);\n if (!match) return source;\n const attrs = match[1] ?? \"\";\n const inner = match[2] ?? \"\";\n const isTs = /\\blang=[\"']ts[\"']/.test(attrs);\n const { body, changed } = transformScript(inner, isTs);\n if (!changed) return source;\n return source.replace(SCRIPT_BLOCK, (full) => full.replace(inner, body));\n}\n","/** Find the index of the closing quote for a string literal that opens at\n * `openIdx`. Handles backslash escapes. Returns -1 if the string is\n * unterminated.\n *\n * Treats backtick template literals the same as `'…'` / `\"…\"` — the\n * closing backtick terminates. Callers needing precise `${…}` interpolation\n * handling will need a real parser; this helper is intentionally simple\n * and good enough for the codemod-grade string masking we do today. */\nexport function findStringEnd(source: string, openIdx: number): number {\n const quote = source[openIdx];\n let i = openIdx + 1;\n while (i < source.length) {\n const ch = source[i];\n if (ch === \"\\\\\") {\n i += 2;\n continue;\n }\n if (ch === quote) return i;\n i++;\n }\n return -1;\n}\n","/** Locate `interface $$Props {` declarations and remove them, including\n * the matching closing `}` even if the body has nested braces or spans\n * multiple lines. Regex alone can't do balanced-brace matching, so we\n * walk the string manually. */\nfunction removeInterfaceBlock(source: string): string {\n const re = /^\\s*interface\\s+\\$\\$Props\\s*\\{/m;\n let out = source;\n while (true) {\n const match = re.exec(out);\n if (!match) return out;\n\n const openBraceIdx = match.index + match[0].length - 1;\n let depth = 1;\n let i = openBraceIdx + 1;\n while (i < out.length && depth > 0) {\n const ch = out[i];\n if (ch === \"{\") depth++;\n else if (ch === \"}\") depth--;\n i++;\n }\n if (depth !== 0) return out; // unbalanced; bail rather than corrupt\n\n // Consume trailing whitespace through end-of-line.\n let endIdx = i;\n while (endIdx < out.length && /[ \\t]/.test(out[endIdx] ?? \"\")) endIdx++;\n if (out[endIdx] === \"\\n\") endIdx++;\n\n out = out.slice(0, match.index) + out.slice(endIdx);\n }\n}\n\nimport { findStringEnd } from \"../../../util/svelte-source.js\";\n\n/** Mask every `'…'`, `\"…\"`, and template literal in `source` with a placeholder\n * so subsequent regex passes can rewrite identifiers without corrupting string\n * contents. Returns the masked body and a function to restore originals. */\nfunction maskStringLiterals(source: string): {\n masked: string;\n restore: (s: string) => string;\n} {\n const strings: string[] = [];\n let out = \"\";\n let i = 0;\n while (i < source.length) {\n const ch = source[i];\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n const closeIdx = findStringEnd(source, i);\n if (closeIdx === -1) {\n out += source.slice(i);\n break;\n }\n const literal = source.slice(i, closeIdx + 1);\n out += `__RDMNT_STR_${strings.length}__`;\n strings.push(literal);\n i = closeIdx + 1;\n } else {\n out += ch;\n i++;\n }\n }\n return {\n masked: out,\n restore: (s) => s.replace(/__RDMNT_STR_(\\d+)__/g, (_full, idx) => strings[Number(idx)] ?? \"\"),\n };\n}\n\nconst PROPS_DECL = /let\\s*\\{([^}]*)\\}\\s*(?::\\s*\\{([^}]*)\\})?\\s*=\\s*\\$props\\(\\)\\s*;?/;\n\n/** If the script declares `let { … } = $props();` (with or without an inline\n * type annotation) and doesn't already collect `...rest`, inject it. For TS,\n * widen the inline type with an `[key: string]: unknown` index signature so\n * the rest binding actually captures excess attributes (without the widening,\n * TS infers `rest` as `{}` and the spread forwards nothing). */\nfunction injectRestIntoProps(scriptBody: string): string {\n const match = scriptBody.match(PROPS_DECL);\n if (!match) return scriptBody;\n const destructured = match[1] ?? \"\";\n if (/\\.\\.\\.\\s*\\w+/.test(destructured)) return scriptBody; // already has rest\n\n // Strip any trailing comma left over from a multi-line destructuring shape\n // (e.g. `{ foo, bar, }`). Without this, the template literal below emits\n // `bar,, ...rest` — invalid syntax that the codemod was happily committing\n // (regression: caltex's Accordian.svelte, 2026-05-27).\n const trimmed = destructured.trim().replace(/,\\s*$/, \"\");\n const newDestructured = trimmed === \"\" ? \" ...rest \" : ` ${trimmed}, ...rest `;\n\n let replacement: string;\n if (match[2] !== undefined) {\n const typeBody = match[2];\n const hasIndexSig = /\\[\\s*key\\s*:\\s*string\\s*\\]\\s*:/.test(typeBody);\n const newTypeBody = hasIndexSig\n ? typeBody\n : `${typeBody.trimEnd().replace(/;?\\s*$/, \"\")}; [key: string]: unknown `;\n replacement = `let {${newDestructured}}: {${newTypeBody}} = $props();`;\n } else {\n replacement = `let {${newDestructured}} = $props();`;\n }\n return scriptBody.replace(PROPS_DECL, replacement);\n}\n\nconst SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/;\nconst HAS_PROPS_CALL = /\\$props\\(\\s*\\)/;\n\nexport function removeDollarRestProps(source: string): string {\n const next = removeInterfaceBlock(source);\n\n const scriptMatch = next.match(SCRIPT_BLOCK);\n if (!scriptMatch) return next;\n if (!HAS_PROPS_CALL.test(scriptMatch[2] ?? \"\")) {\n // No $props() in this script — refuse to rewrite $$restProps anywhere, since\n // doing so would emit references to an undeclared identifier. The user sees\n // the original $$restProps and a clear Svelte 5 build error to migrate by hand.\n return next;\n }\n\n const scriptInner = scriptMatch[2] ?? \"\";\n const { masked, restore } = maskStringLiterals(scriptInner);\n let processed = injectRestIntoProps(masked);\n processed = processed.replace(/\\$\\$restProps/g, \"rest\");\n const restoredInner = restore(processed);\n\n // Use a function callback so `$$` in the restored script body isn't\n // interpreted as the `$` substitution pattern by String.prototype.replace.\n const newScriptBlock = scriptMatch[0].replace(scriptInner, () => restoredInner);\n const before = next.slice(0, scriptMatch.index!);\n const after = next.slice(scriptMatch.index! + scriptMatch[0].length);\n\n // Template (outside script) gets a plain swap. Template attribute strings\n // containing the literal text \"$$restProps\" are vanishingly rare in practice;\n // accept the limitation rather than parse the whole template.\n return (\n before.replace(/\\$\\$restProps/g, \"rest\") +\n newScriptBlock +\n after.replace(/\\$\\$restProps/g, \"rest\")\n );\n}\n","/**\n * Collapses the \"manual sync state with prop\" anti-pattern into `$derived`.\n *\n * Input:\n * let content = $state(data.page.data);\n * $effect(() => { data; content = data.page.data });\n *\n * Output:\n * let content = $derived(data.page.data);\n *\n * Only transforms when the `$state(...)` initializer expression matches the\n * effect's right-hand assignment exactly (after trim). Intervening statements\n * between the `let` and the `$effect` block prevent the match — keeps the\n * codemod conservative.\n *\n * Triggered by Svelte 5's `state_referenced_locally` warning, which fires\n * whenever a local `let X = $state(prop.expr)` captures a prop reference\n * only at init time.\n */\n// `;?` before the closing `}` so the multi-line effect form matches:\n// $effect(() => {\n// data;\n// content = data.page.data;\n// });\n// as well as the single-line form: $effect(() => { data; content = data.page.data })\nconst PATTERN =\n /let\\s+(\\w+)\\s*=\\s*\\$state\\(\\s*([^)]+?)\\s*\\)\\s*;[ \\t\\r\\n]*\\$effect\\(\\s*\\(\\s*\\)\\s*=>\\s*\\{\\s*\\w+\\s*;\\s*\\1\\s*=\\s*([^;}]+?)\\s*;?\\s*\\}\\s*\\)\\s*;?/g;\n\nexport function stateEffectSyncToDerived(source: string): string {\n return source.replace(PATTERN, (full, name: string, initExpr: string, effectExpr: string) => {\n if (initExpr.trim() !== effectExpr.trim()) return full;\n return `let ${name} = $derived(${initExpr.trim()});`;\n });\n}\n","/**\n * Converts the legacy `$$props.class` pattern (passing extra HTML class from\n * a parent component) to a Svelte 5 named-prop destructuring.\n *\n * Input:\n * <script lang=\"ts\">\n * let { foo }: { foo?: string } = $props();\n * </script>\n * <div class=\"other {$$props.class || ''}\">x</div>\n *\n * Output:\n * <script lang=\"ts\">\n * let { foo, class: className = \"\" }: { foo?: string; class?: string } = $props();\n * </script>\n * <div class=\"other {className || ''}\">x</div>\n *\n * Triggered by Svelte 5 build errors:\n * \"Cannot use `$$props` in runes mode\" (svelte.dev/e/legacy_props_invalid)\n *\n * The original svelte-migrate tool flagged this with a `@migration-task`\n * comment because it couldn't safely combine `$$props` with already-named\n * props. We can: `class` is the dominant case across the reddoor fleet,\n * so we destructure it as `class: className = \"\"` (renamed because `class`\n * is a JS reserved word as a bare binding) and rewrite template references.\n *\n * Conservative: only transforms files that have BOTH a template\n * `$$props.class` reference AND an existing `$props()` destructuring.\n * Files using `$$props.class` without a `$props()` declaration are left\n * for the `exportLetToProps` codemod to handle in a prior pass.\n */\n// Note: lazy `[\\s\\S]*?` (not `[^}]*`) so default values containing braces\n// — `() => {}`, `{ foo: 1 }`, etc. — don't truncate the match early.\nconst PROPS_DESTRUCTURE = /let\\s*\\{([\\s\\S]*?)\\}(\\s*:\\s*\\{([\\s\\S]*?)\\})?\\s*=\\s*\\$props\\(\\)/;\n// Two regexes: a stateless one for \"does this string contain $$props.class?\"\n// existence checks, and the /g one for the iterating template rewrite. Mixing\n// .test() and .replace() on the same /g regex makes lastIndex management\n// fragile — easy to forget the reset on a future edit.\nconst HAS_DOLLAR_PROPS_CLASS = /\\$\\$props\\.class\\b/;\nconst DOLLAR_PROPS_CLASS_GLOBAL = /\\$\\$props\\.class\\b/g;\nconst DOLLAR_PROPS_ANY = /\\$\\$props\\b/;\nconst SCRIPT_BLOCK = /<script\\b[^>]*>[\\s\\S]*?<\\/script>/g;\nconst MIGRATION_TASK = /^<!--\\s*@migration-task[\\s\\S]*?-->\\s*\\n?/gm;\nconst IDENT = \"className\";\n\nfunction maskScripts(source: string): { masked: string; blocks: string[] } {\n const blocks: string[] = [];\n const masked = source.replace(SCRIPT_BLOCK, (m) => {\n blocks.push(m);\n return `__SCRIPT_${blocks.length - 1}__`;\n });\n return { masked, blocks };\n}\n\nfunction restoreScripts(masked: string, blocks: string[]): string {\n let out = masked;\n blocks.forEach((blk, i) => {\n out = out.replace(`__SCRIPT_${i}__`, blk);\n });\n return out;\n}\n\nexport function dollarPropsClass(source: string): string {\n // Bail early if the template doesn't reference $$props.class\n const { masked } = maskScripts(source);\n if (!HAS_DOLLAR_PROPS_CLASS.test(masked)) return source;\n\n // Bail if there's no $props() destructuring to extend\n if (!PROPS_DESTRUCTURE.test(source)) return source;\n\n let updated = source.replace(PROPS_DESTRUCTURE, (full, body, typeAnno, typeBody) => {\n // Already migrated (someone added class manually)\n if (/\\bclass\\s*:/.test(body as string)) return full;\n\n const cleanBody = (body as string).trim().replace(/,\\s*$/, \"\").trim();\n const newBody = cleanBody ? `${cleanBody}, class: ${IDENT} = \"\"` : `class: ${IDENT} = \"\"`;\n\n if (typeAnno) {\n const cleanType = ((typeBody as string) ?? \"\").trim().replace(/;\\s*$/, \"\").trim();\n const newType = cleanType ? `${cleanType}; class?: string` : `class?: string`;\n return `let { ${newBody} }: { ${newType} } = $props()`;\n }\n return `let { ${newBody} } = $props()`;\n });\n\n // Replace $$props.class in template only (re-mask after destructuring update)\n const reMasked = maskScripts(updated);\n const templateRewritten = reMasked.masked.replace(DOLLAR_PROPS_CLASS_GLOBAL, IDENT);\n updated = restoreScripts(templateRewritten, reMasked.blocks);\n\n // Strip @migration-task comments if no $$props references remain anywhere\n // EXCEPT inside those very comments. Strip-then-check, restore if still dirty.\n const stripped = updated.replace(MIGRATION_TASK, \"\");\n if (!DOLLAR_PROPS_ANY.test(stripped)) {\n updated = stripped;\n }\n\n return updated;\n}\n","/**\n * Converts Svelte 4 `$:` reactive statements to Svelte 5 runes.\n *\n * - `$: var = expr;` → `let var = $derived(expr);`\n * - `$: { body }` → `$effect(() => { body });`\n *\n * Triggered by:\n * \"`$:` is not allowed in runes mode, use `$derived` or `$effect` instead\"\n * (svelte.dev/e/legacy_reactive_statement_invalid)\n *\n * Block patterns become `$effect` rather than per-variable `$derived` calls\n * because the block typically mutates multiple already-declared `let`\n * variables with conditional logic — too contextual for a safe automatic\n * decomposition into discrete derived values. The user can refine each\n * `$effect` into idiomatic `$derived` calls afterward if desired.\n *\n * Scoped to `<script>` content only — `$:` in template/style text is left\n * alone (it would only ever appear there as literal text anyway).\n */\nimport { findStringEnd } from \"../../../util/svelte-source.js\";\n\nconst SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/g;\nconst SIMPLE_REACTIVE = /^([ \\t]*)\\$:\\s*(\\w+)\\s*=\\s*([^;\\n]+);?[ \\t]*$/gm;\nconst BLOCK_REACTIVE_HEAD = /(^|\\n)([ \\t]*)\\$:\\s*\\{/g;\n\nfunction findMatchingClose(source: string, openIdx: number): number {\n let depth = 0;\n let i = openIdx;\n while (i < source.length) {\n const ch = source[i];\n // Skip over string literals so braces inside strings don't fool the counter.\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n const closeStr = findStringEnd(source, i);\n if (closeStr === -1) return -1;\n i = closeStr + 1;\n continue;\n }\n // Skip over comments so braces inside `// }` or `/* } */` don't fool the\n // counter. Regression: the old version silently corrupted source (depth\n // went off, real closing brace mis-matched) on inputs like `$: { // } ... }`.\n // The corrupted output still compiles in Svelte 5 — no parser to scream.\n if (ch === \"/\") {\n const next = source[i + 1];\n if (next === \"/\") {\n const eol = source.indexOf(\"\\n\", i + 2);\n i = eol === -1 ? source.length : eol; // step onto newline; outer loop handles it\n continue;\n }\n if (next === \"*\") {\n const end = source.indexOf(\"*/\", i + 2);\n if (end === -1) return -1; // unterminated block comment — bail rather than corrupt\n i = end + 2;\n continue;\n }\n }\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n depth--;\n if (depth === 0) return i;\n }\n i++;\n }\n return -1;\n}\n\n/** Flag each converted `$effect` block for manual review. The conversion is\n * syntactically safe (compiles), but if any of the locals the block mutates\n * was declared as plain `let` (not `$state`), the `$effect` runs once on\n * mount and never again — code silently loses its reactivity. We can't\n * detect that automatically (it would require scope analysis on the\n * declaration sites), so we leave a breadcrumb for the human reviewer. */\nconst MIGRATION_MARKER =\n \"// @migration-task: $effect won't trigger UI updates on plain `let` bindings — refine mutated locals to $state or split into per-variable $derived.\";\n\nfunction transformBlocks(body: string): string {\n const out: string[] = [];\n let last = 0;\n BLOCK_REACTIVE_HEAD.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = BLOCK_REACTIVE_HEAD.exec(body)) !== null) {\n const leadingNewline = m[1] ?? \"\";\n const indent = m[2] ?? \"\";\n const headEnd = m.index + m[0].length; // position just after `{`\n const openBraceIdx = headEnd - 1;\n const closeBraceIdx = findMatchingClose(body, openBraceIdx);\n if (closeBraceIdx === -1) continue;\n out.push(body.slice(last, m.index));\n out.push(leadingNewline);\n const blockBody = body.slice(openBraceIdx + 1, closeBraceIdx);\n out.push(`${indent}${MIGRATION_MARKER}\\n`);\n out.push(`${indent}$effect(() => {${blockBody}});`);\n last = closeBraceIdx + 1;\n BLOCK_REACTIVE_HEAD.lastIndex = last;\n }\n out.push(body.slice(last));\n return out.join(\"\");\n}\n\nfunction transformSimple(body: string): string {\n return body.replace(SIMPLE_REACTIVE, (_full, indent: string, name: string, expr: string) => {\n return `${indent}let ${name} = $derived(${expr.trim()});`;\n });\n}\n\nexport function legacyReactiveToRunes(source: string): string {\n return source.replace(SCRIPT_BLOCK, (full, _attrs: string, body: string) => {\n // Blocks first so an outer `$: { ... }` containing nothing matchable\n // for the simple pass still gets wrapped. Order doesn't matter for the\n // patterns currently in the fleet but keeps the codemod robust to future\n // shapes.\n let next = transformBlocks(body);\n next = transformSimple(next);\n if (next === body) return full;\n return full.replace(body, next);\n });\n}\n","import { defaultSpawn, type SpawnFn, type SpawnResult } from \"../../audits/util/spawn.js\";\n\nexport type VerifyResult = {\n install: SpawnResult | { skipped: true };\n check: SpawnResult | { skipped: true };\n};\n\nexport async function verifyMigration(\n cwd: string,\n spawn: SpawnFn = defaultSpawn,\n): Promise<VerifyResult> {\n let install: VerifyResult[\"install\"];\n try {\n install = await spawn(\"pnpm\", [\"install\"], { cwd, timeoutMs: 10 * 60_000 });\n } catch {\n install = { skipped: true };\n }\n\n let check: VerifyResult[\"check\"];\n try {\n check = await spawn(\"pnpm\", [\"run\", \"check\"], { cwd, timeoutMs: 5 * 60_000 });\n } catch {\n check = { skipped: true };\n }\n\n return { install, check };\n}\n","import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport type SummaryInput = {\n cwd: string;\n filesChangedByCodemods: number;\n svelteMigrateRan: boolean;\n tailwindUpgraded: boolean;\n};\n\nexport async function writeMigrationSummary(input: SummaryInput): Promise<string> {\n const lines = [\n `# Svelte 4 → 5 migration summary`,\n ``,\n `Generated by @reddoorla/maintenance.`,\n ``,\n `- svelte-migrate run: ${input.svelteMigrateRan ? \"yes\" : \"no\"}`,\n `- @tailwindcss/upgrade run: ${input.tailwindUpgraded ? \"yes\" : \"no\"}`,\n `- .svelte files touched by gotcha codemods: ${input.filesChangedByCodemods}`,\n ``,\n `Next steps:`,\n `- Run \\`pnpm run check\\` and resolve any remaining warnings.`,\n `- Spot-check rune migrations in components that use \\`reactive\\` statements.`,\n `- Verify Playwright a11y tests still pass.`,\n ];\n const content = lines.join(\"\\n\") + \"\\n\";\n const path = join(input.cwd, \"MIGRATION_SVELTE_5.md\");\n await writeFile(path, content, \"utf-8\");\n return path;\n}\n","import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../types.js\";\nimport { planGotchaCodemods } from \"./svelte-5/step-gotchas.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\ntype Change = { rel: string; after: string };\n\n/**\n * Standalone codemod pass for sites already on Svelte 5.\n *\n * Applies the same gotcha codemods the full `svelte-4-to-5` migration runs,\n * but skips the version checks and migration steps — useful when Svelte 5\n * surfaces new strictness warnings post-upgrade (e.g. `state_referenced_locally`)\n * and the fleet needs a clean re-application.\n *\n * Plans changes in memory first; only creates the branch + writes + commits\n * when there is something to apply. Re-runs against a clean tree are noop.\n */\nexport async function svelteCodemods(site: Site): Promise<RecipeResult> {\n return withRecipe<Change[]>({\n name: \"svelte-codemods\",\n site,\n plan: async () => {\n const changes = await planGotchaCodemods(site.path);\n if (changes.length === 0) {\n return { kind: \"noop\", notes: \"no codemod targets matched\" };\n }\n return { kind: \"apply\", plan: changes };\n },\n apply: async (changes, { commit, cwd }) => {\n for (const c of changes) {\n await writeFile(join(cwd, c.rel), c.after, \"utf-8\");\n }\n await commit(`refactor(svelte5): apply codemods (${changes.length} files)`);\n return { kind: \"ok\" };\n },\n });\n}\n","import { rm, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../types.js\";\nimport { readPackageJson, writePackageJson, type PackageJsonLike } from \"../util/pkg.js\";\nimport { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\nimport { rewriteScriptsForPnpm } from \"./convert-to-pnpm/script-rewrites.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type ConvertToPnpmOptions = {\n spawn?: SpawnFn;\n /** Version string written into package.json's `packageManager` field.\n * Defaults to the version baked into this package's own pnpm setup. */\n pnpmVersion?: string;\n};\n\n/** Pinned default — matches the `packageManager` field of this package\n * (kept in sync with package.json). Sites can override per-recipe. */\nconst DEFAULT_PNPM_VERSION = \"10.33.1\";\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\ntype Plan = { hasNpmLock: boolean; hasYarnLock: boolean };\n\nexport async function convertToPnpm(\n site: Site,\n opts: ConvertToPnpmOptions = {},\n): Promise<RecipeResult> {\n const spawn = opts.spawn ?? defaultSpawn;\n const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;\n\n const pnpmLockPath = join(site.path, \"pnpm-lock.yaml\");\n const npmLockPath = join(site.path, \"package-lock.json\");\n const yarnLockPath = join(site.path, \"yarn.lock\");\n\n return withRecipe<Plan>({\n name: \"convert-to-pnpm\",\n site,\n plan: async () => {\n if (await exists(pnpmLockPath)) {\n return { kind: \"noop\", notes: \"site already has pnpm-lock.yaml\" };\n }\n const hasNpmLock = await exists(npmLockPath);\n const hasYarnLock = await exists(yarnLockPath);\n if (!hasNpmLock && !hasYarnLock) {\n return {\n kind: \"noop\",\n notes: \"no convertible lockfile (package-lock.json or yarn.lock) at site root\",\n };\n }\n return { kind: \"apply\", plan: { hasNpmLock, hasYarnLock } };\n },\n apply: async ({ hasNpmLock, hasYarnLock }, { commit, cwd }) => {\n // Step 1: remove the npm/yarn lockfile(s).\n if (hasNpmLock) await rm(npmLockPath, { force: true });\n if (hasYarnLock) await rm(yarnLockPath, { force: true });\n const sourceLock = hasNpmLock ? \"package-lock.json\" : \"yarn.lock\";\n await commit(`chore(pnpm): remove ${sourceLock}`);\n\n // Step 2: pin packageManager + rewrite scripts (single commit — they\n // both touch package.json).\n const pkgPath = join(cwd, \"package.json\");\n const pkg = await readPackageJson(pkgPath);\n const next: PackageJsonLike = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };\n\n if (pkg.scripts && typeof pkg.scripts === \"object\") {\n const { scripts: rewritten, changedCount } = rewriteScriptsForPnpm(\n pkg.scripts as Record<string, string>,\n );\n if (changedCount > 0) {\n next.scripts = rewritten;\n }\n }\n\n await writePackageJson(pkgPath, next);\n await commit(\"chore(pnpm): pin packageManager + rewrite npm scripts\");\n\n // Step 3: remove any existing flat node_modules from a prior npm/yarn run\n // before pnpm installs. Sharing a node_modules across package managers\n // produces phantom-dep resolution issues (pnpm's nested layout disagrees\n // with what's already on disk). node_modules is gitignored on every\n // reddoor site so this doesn't dirty the tree.\n await rm(join(cwd, \"node_modules\"), { recursive: true, force: true });\n\n // Step 4: run pnpm install to materialize pnpm-lock.yaml.\n const installResult = await spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (installResult.code !== 0) {\n return { kind: \"failed\", notes: `pnpm install failed (exit ${installResult.code})` };\n }\n\n await commit(\"chore(pnpm): add pnpm-lock.yaml\");\n return { kind: \"ok\" };\n },\n });\n}\n","/**\n * Rewrite a single package.json script value to use pnpm equivalents\n * where the substitution is safe. Conservative on purpose: we only touch\n * patterns whose semantics are identical under pnpm.\n *\n * - `npm run <token>` → `pnpm run <token>` (identical behavior)\n * - `npx <token>` → `pnpm dlx <token>` (identical behavior in pnpm 7+)\n *\n * Intentionally NOT rewritten:\n * - `npm install`, `npm install <pkg>`, `npm install --save-dev <pkg>` —\n * subtle flag mapping (e.g. `--save-dev` → `-D`) and edge cases like\n * `--save-exact` / `--save-optional`. Better to leave for an operator\n * eyeball than to silently mis-translate.\n * - Hyphenated identifiers like `npm-check-updates` (word-boundary protected).\n * - `concurrently \"npm:scriptName\"` shorthand syntax — it isn't actually\n * running npm; it's a concurrently-specific script reference.\n */\nexport function rewriteScriptForPnpm(script: string): string {\n let out = script;\n // `npm run <name>` → `pnpm run <name>`. \\b before npm prevents\n // matching inside hyphenated identifiers. Lookahead `(?=\\s)` after run\n // ensures we don't match `runner`.\n out = out.replace(/\\bnpm run(?=\\s)/g, \"pnpm run\");\n // `npx ` → `pnpm dlx `. \\b before npx prevents matching `npx-something`.\n out = out.replace(/\\bnpx(?=\\s)/g, \"pnpm dlx\");\n return out;\n}\n\n/**\n * Rewrite every entry in a package.json `scripts` map. Returns the new\n * map alongside a count of scripts that were actually changed.\n */\nexport function rewriteScriptsForPnpm(scripts: Record<string, string>): {\n scripts: Record<string, string>;\n changedCount: number;\n} {\n const next: Record<string, string> = {};\n let changedCount = 0;\n for (const [name, value] of Object.entries(scripts)) {\n const rewritten = rewriteScriptForPnpm(value);\n next[name] = rewritten;\n if (rewritten !== value) changedCount++;\n }\n return { scripts: next, changedCount };\n}\n","import { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../types.js\";\nimport { readPackageJson, writePackageJson, bumpDep, type PackageJsonLike } from \"../util/pkg.js\";\nimport { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\nimport { selfCaretRange } from \"../util/self-version.js\";\nimport { baselineVersions } from \"../configs/baseline-versions.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type OnboardAudit = \"lighthouse\" | \"a11y\";\n\nexport type OnboardOptions = {\n spawn?: SpawnFn;\n /** Which audit-related deps to ensure. Defaults to all known audits. */\n audits?: OnboardAudit[];\n /** Version range to pin for @reddoorla/maintenance. Defaults to a caret\n * range against this package's own version at runtime — no manual\n * syncing required at each minor bump. */\n packageVersion?: string;\n};\n\nconst PACKAGE_NAME = \"@reddoorla/maintenance\";\n\nconst AUDIT_DEP_NAMES: Record<OnboardAudit, string[]> = {\n lighthouse: [\"@lhci/cli\"],\n a11y: [\"@playwright/test\", \"@axe-core/playwright\"],\n};\n\n/** Framework deps onboard ensures for every site, independent of which audits\n * are requested. The sync-configs svelte.config.js template does\n * `import adapter from \"@sveltejs/adapter-netlify\"`, so a site that lacks the\n * adapter declared can't build once configs are synced — onboard closes that\n * gap at the same time it adds the maintenance package. */\nconst FRAMEWORK_DEP_NAMES = [\"@sveltejs/adapter-netlify\"];\n\n/** Resolve framework dep versions from baselineVersions at module load so they\n * can't drift from the single source of truth — mirrors AUDIT_DEPS. Throws at\n * import time if a name is missing there (a programming error). */\nexport const FRAMEWORK_DEPS: Array<{ name: string; version: string }> = FRAMEWORK_DEP_NAMES.map(\n (name) => {\n const version = baselineVersions[name];\n if (!version) {\n throw new Error(\n `baseline-versions is missing framework dep \"${name}\" — add it to src/configs/baseline-versions.ts`,\n );\n }\n return { name, version };\n },\n);\n\n/** Look up each audit dep's version in baselineVersions at module load so\n * AUDIT_DEPS can't drift from the single source of truth across releases.\n * Throws at import time if baseline-versions is missing an audit dep —\n * which would be a programming error (every audit dep name above must\n * appear in baselineVersions). */\nexport const AUDIT_DEPS: Record<\n OnboardAudit,\n Array<{ name: string; version: string }>\n> = Object.fromEntries(\n (Object.entries(AUDIT_DEP_NAMES) as Array<[OnboardAudit, string[]]>).map(([audit, names]) => [\n audit,\n names.map((name) => {\n const version = baselineVersions[name];\n if (!version) {\n throw new Error(\n `baseline-versions is missing audit dep \"${name}\" — add it to src/configs/baseline-versions.ts`,\n );\n }\n return { name, version };\n }),\n ]),\n) as Record<OnboardAudit, Array<{ name: string; version: string }>>;\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction isDeclared(pkg: PackageJsonLike, name: string): boolean {\n return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);\n}\n\ntype Plan = {\n pkg: PackageJsonLike;\n toAdd: Array<{ name: string; version: string }>;\n};\n\nexport async function onboard(site: Site, opts: OnboardOptions = {}): Promise<RecipeResult> {\n const spawn = opts.spawn ?? defaultSpawn;\n const audits = opts.audits ?? ([\"lighthouse\", \"a11y\"] as OnboardAudit[]);\n const packageVersion = opts.packageVersion ?? selfCaretRange(import.meta.url);\n\n return withRecipe<Plan>({\n name: \"onboard\",\n site,\n plan: async () => {\n // Pre-flight: site must already be on pnpm. We don't auto-convert here;\n // that's the convert-to-pnpm recipe's job, and combining them would\n // hide the package-manager transition inside a bigger PR.\n if (!(await exists(join(site.path, \"pnpm-lock.yaml\")))) {\n return {\n kind: \"failed\",\n notes: \"no pnpm-lock.yaml at site root — run convert-to-pnpm first\",\n };\n }\n\n const pkgPath = join(site.path, \"package.json\");\n const pkg = await readPackageJson(pkgPath);\n\n // Determine what's missing. Anything already declared (even at a wildly\n // different version) is left alone — onboard never downgrades.\n const toAdd: Array<{ name: string; version: string }> = [];\n if (!isDeclared(pkg, PACKAGE_NAME)) {\n toAdd.push({ name: PACKAGE_NAME, version: packageVersion });\n }\n for (const dep of FRAMEWORK_DEPS) {\n if (!isDeclared(pkg, dep.name)) toAdd.push(dep);\n }\n for (const audit of audits) {\n for (const dep of AUDIT_DEPS[audit]) {\n if (!isDeclared(pkg, dep.name)) toAdd.push(dep);\n }\n }\n\n if (toAdd.length === 0) {\n return {\n kind: \"noop\",\n notes: `site already has ${PACKAGE_NAME}, framework deps, and audit deps (${audits.join(\"+\")})`,\n };\n }\n return { kind: \"apply\", plan: { pkg, toAdd } };\n },\n apply: async ({ pkg, toAdd }, { commit, cwd }) => {\n const pkgPath = join(cwd, \"package.json\");\n let next: PackageJsonLike = pkg;\n for (const dep of toAdd) {\n next = bumpDep(next, dep.name, dep.version);\n }\n await writePackageJson(pkgPath, next);\n\n // Run pnpm install so the lockfile reflects the new deps before we commit.\n // Stream output — install on a real site can take 30s+.\n const installResult = await spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (installResult.code !== 0) {\n return {\n kind: \"failed\",\n notes: `pnpm install failed (exit ${installResult.code})`,\n };\n }\n\n await commit(`chore(reddoor): onboard with ${PACKAGE_NAME} ${packageVersion}`);\n return {\n kind: \"ok\",\n notes: `Added ${toAdd.length} dep(s): ${toAdd.map((d) => d.name).join(\", \")}`,\n };\n },\n });\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Read this package's own version at runtime so recipe defaults don't go\n * stale at each minor bump.\n *\n * Pass `import.meta.url` from the calling file. Walks UP from the caller\n * looking for the first `package.json` whose `name` matches this package\n * (`@reddoorla/maintenance`). The older \"two levels up\" shortcut held for\n * `src/X/Y.ts` and `dist/cli/bin.js` (both happen to be 2 dirs deep) but\n * broke for `dist/index.js` (only 1 dir deep) — silently returned \"0.0.0\"\n * and pinned consumers to `^0.0.0`. Same bug class as the 0.10.1 bundled-\n * assets ENOENT (2026-05-27). Walk-up is robust regardless of bundling\n * layout.\n *\n * Returns \"0.0.0\" if no matching package.json is reachable (defensive\n * fallback; callers should treat that as a signal to either override\n * explicitly or fail loudly).\n */\nexport function selfPackageVersion(callerImportMetaUrl: string): string {\n try {\n let dir = dirname(fileURLToPath(callerImportMetaUrl));\n while (true) {\n const candidate = join(dir, \"package.json\");\n if (existsSync(candidate)) {\n const raw = readFileSync(candidate, \"utf-8\");\n const pkg = JSON.parse(raw) as { name?: string; version?: string };\n // Only accept OUR package.json — keep walking past random ancestor\n // package.jsons (the consumer's own, anything in node_modules) that\n // happen to sit above the bundle.\n if (pkg.name === \"@reddoorla/maintenance\") {\n return pkg.version ?? \"0.0.0\";\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return \"0.0.0\";\n dir = parent;\n }\n } catch {\n return \"0.0.0\";\n }\n}\n\n/** Caret-pinned range against this package's own version: e.g. \"^0.6.2\". */\nexport function selfCaretRange(callerImportMetaUrl: string): string {\n return `^${selfPackageVersion(callerImportMetaUrl)}`;\n}\n","import { access, mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\nimport { A11Y_FIXTURES_PAGE_RELATIVE, A11Y_FIXTURES_PAGE_TEMPLATE } from \"./template.js\";\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Writes a starter `src/routes/dev/a11y-fixtures/+page.svelte` if the route\n * doesn't already exist. The hardcoded URL in `src/configs/lighthouse.ts` +\n * `src/configs/playwright-a11y.ts` targets this path — newly-onboarded sites\n * need the route to exist for either audit to pass. Operator edits to an\n * existing page are never clobbered (noop on existing file).\n */\nexport async function a11yFixturesPage(site: Site): Promise<RecipeResult> {\n const target = join(site.path, A11Y_FIXTURES_PAGE_RELATIVE);\n return withRecipe<{ target: string }>({\n name: \"a11y-fixtures-page\",\n site,\n plan: async () => {\n if (await fileExists(target)) {\n return { kind: \"noop\", notes: `${A11Y_FIXTURES_PAGE_RELATIVE} already exists` };\n }\n return { kind: \"apply\", plan: { target } };\n },\n apply: async (planned, { commit }) => {\n await mkdir(dirname(planned.target), { recursive: true });\n await writeFile(planned.target, A11Y_FIXTURES_PAGE_TEMPLATE, \"utf-8\");\n await commit(\"feat: add /dev/a11y-fixtures starter route\");\n return { kind: \"ok\" };\n },\n });\n}\n","/** Relative path inside a site where the a11y fixtures route lives. The\n * hardcoded URL in `src/configs/lighthouse.ts` + `src/configs/playwright-a11y.ts`\n * is `/dev/a11y-fixtures`, so a SvelteKit `+page.svelte` here resolves. */\nexport const A11Y_FIXTURES_PAGE_RELATIVE = \"src/routes/dev/a11y-fixtures/+page.svelte\";\n\n/** Stub `+page.svelte` for newly-onboarded sites. Generic on purpose —\n * landmarks, heading hierarchy, and a relative link cover the axe-core +\n * lhci defaults without committing the operator to any specific fixture\n * shape. Replace with site-specific patterns over time. */\nexport const A11Y_FIXTURES_PAGE_TEMPLATE = `<svelte:head>\n <title>a11y fixtures — Reddoor</title>\n <meta\n name=\"description\"\n content=\"Reddoor accessibility fixtures — semantic landmarks, heading hierarchy, and a stable target for @lhci/cli and Playwright + axe-core coverage. Not linked from the public site.\"\n />\n</svelte:head>\n\n<main>\n <header>\n <h1>Accessibility fixtures</h1>\n <p>\n This page exists so <code>@lhci/cli</code> and Playwright + axe-core have a\n stable target with predictable a11y characteristics. It is not linked from\n the public site.\n </p>\n </header>\n\n <section aria-labelledby=\"landmarks-heading\">\n <h2 id=\"landmarks-heading\">Landmarks</h2>\n <p>\n A single <code>main</code> wraps the page; sections each declare\n <code>aria-labelledby</code> matched to their heading id so screen readers\n and axe both see a clean outline.\n </p>\n </section>\n\n <section aria-labelledby=\"links-heading\">\n <h2 id=\"links-heading\">Links</h2>\n <p>\n <a href=\"/\">Back to home</a> — relative link with descriptive visible text,\n so no <code>aria-label</code> override is needed.\n </p>\n </section>\n</main>\n`;\n","import type { AuditResult, RecipeResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { convertToPnpm } from \"./convert-to-pnpm.js\";\nimport { onboard } from \"./onboard.js\";\nimport { syncConfigs } from \"./sync-configs.js\";\nimport { svelteCodemods } from \"./svelte-codemods.js\";\nimport { a11yFixturesPage } from \"./a11y-fixtures-page/index.js\";\nimport { runAudits } from \"../audits/index.js\";\n\nexport type InitStepResult =\n | { kind: \"recipe\"; result: RecipeResult }\n | { kind: \"audit\"; results: AuditResult[] }\n | { kind: \"error\"; message: string };\n\nexport type InitStep = {\n name: string;\n run: (site: Site) => Promise<InitStepResult>;\n};\n\nexport type InitResult = {\n site: string;\n steps: Array<{ name: string; result: InitStepResult }>;\n /** True if every step ran; false if an `error` or `failed` recipe result\n * short-circuited the chain. `noop` recipes do not break completeness. */\n complete: boolean;\n};\n\nexport type InitOptions = {\n /** Override the default step list. Tests inject mocked steps; production\n * code relies on the default. */\n steps?: InitStep[];\n};\n\nfunction recipeStep(name: string, fn: (site: Site) => Promise<RecipeResult>): InitStep {\n return {\n name,\n run: async (site) => ({ kind: \"recipe\", result: await fn(site) }),\n };\n}\n\n/** convert-to-pnpm → onboard → sync-configs → svelte-codemods →\n * a11y-fixtures-page → audit. Order is deliberate — every step depends on\n * the prior one's output (pnpm before onboard's installs, onboard's deps\n * before sync-configs writes lighthouserc, fixtures page before audit\n * actually has a route to hit). */\nexport const DEFAULT_INIT_STEPS: InitStep[] = [\n recipeStep(\"convert-to-pnpm\", convertToPnpm),\n recipeStep(\"onboard\", onboard),\n recipeStep(\"sync-configs\", syncConfigs),\n recipeStep(\"svelte-codemods\", svelteCodemods),\n recipeStep(\"a11y-fixtures-page\", a11yFixturesPage),\n {\n name: \"audit\",\n run: async (site) => ({ kind: \"audit\", results: await runAudits(site) }),\n },\n];\n\n/**\n * One-shot guided onboarding. Runs the default step sequence against a\n * site, collecting per-step results into an InitResult. Each underlying\n * recipe still creates its own branch — init is a thin orchestrator, not\n * a branch-collapser; the operator ends up with one stack of branches per\n * mutated step (recipes that noop don't branch).\n *\n * Stops the chain on the first uncaught error or `failed` recipe result.\n * `noop` results are expected (e.g. running init twice) and continue the\n * chain. The final audit pass runs if no prior step errored.\n */\nexport async function init(site: Site, opts: InitOptions = {}): Promise<InitResult> {\n const steps = opts.steps ?? DEFAULT_INIT_STEPS;\n const out: Array<{ name: string; result: InitStepResult }> = [];\n\n for (const step of steps) {\n let result: InitStepResult;\n try {\n result = await step.run(site);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n out.push({ name: step.name, result: { kind: \"error\", message } });\n return { site: siteLabel(site), steps: out, complete: false };\n }\n out.push({ name: step.name, result });\n if (result.kind === \"recipe\" && result.result.status === \"failed\") {\n return { site: siteLabel(site), steps: out, complete: false };\n }\n }\n\n return { site: siteLabel(site), steps: out, complete: true };\n}\n","import type { RecipeName } from \"../types.js\";\nimport { syncConfigs, type SyncConfigsOptions } from \"./sync-configs.js\";\nimport { bumpDeps, type BumpDepsOptions } from \"./bump-deps.js\";\nimport { upgradeSvelte4to5, type UpgradeSvelte4to5Options } from \"./svelte-5/index.js\";\nimport { svelteCodemods } from \"./svelte-codemods.js\";\nimport { convertToPnpm, type ConvertToPnpmOptions } from \"./convert-to-pnpm.js\";\nimport { onboard, type OnboardOptions, type OnboardAudit } from \"./onboard.js\";\nimport { a11yFixturesPage } from \"./a11y-fixtures-page/index.js\";\nimport {\n init,\n DEFAULT_INIT_STEPS,\n type InitOptions,\n type InitResult,\n type InitStep,\n type InitStepResult,\n} from \"./init.js\";\n\nexport {\n syncConfigs,\n bumpDeps,\n upgradeSvelte4to5,\n svelteCodemods,\n convertToPnpm,\n onboard,\n a11yFixturesPage,\n init,\n DEFAULT_INIT_STEPS,\n};\nexport type {\n SyncConfigsOptions,\n BumpDepsOptions,\n UpgradeSvelte4to5Options,\n ConvertToPnpmOptions,\n OnboardOptions,\n OnboardAudit,\n InitOptions,\n InitResult,\n InitStep,\n InitStepResult,\n};\n\nexport const ALL_RECIPE_NAMES: RecipeName[] = [\n \"sync-configs\",\n \"bump-deps\",\n \"svelte-4-to-5\",\n \"svelte-codemods\",\n \"convert-to-pnpm\",\n \"onboard\",\n \"a11y-fixtures-page\",\n \"self-updating\",\n \"init\",\n];\n\nexport function isRecipeName(value: string): value is RecipeName {\n return (ALL_RECIPE_NAMES as string[]).includes(value);\n}\n","import { basename } from \"node:path\";\nimport type { InventoryProvider, Site } from \"../types.js\";\n\nexport type LocalPathOptions = {\n name?: string;\n};\n\nexport function localPath(path: string, opts: LocalPathOptions = {}): InventoryProvider {\n // `||` not `??`: an explicit empty `--name \"\"` should fall back to the path's\n // basename, not become a blank site name.\n const site: Site = { path, name: opts.name || basename(path) };\n return async () => [site];\n}\n","import { readFile } from \"node:fs/promises\";\nimport { isAbsolute } from \"node:path\";\nimport type { InventoryProvider, Site } from \"../types.js\";\nimport { isHttpUrl } from \"../util/url.js\";\n\nfunction validate(raw: unknown): Site[] {\n if (!Array.isArray(raw)) {\n throw new Error(\"inventory JSON must be an array of sites\");\n }\n return raw.map((entry, i) => {\n if (!entry || typeof entry !== \"object\") {\n throw new Error(`inventory entry ${i} is not an object`);\n }\n const e = entry as Record<string, unknown>;\n if (typeof e.path !== \"string\" || e.path.length === 0) {\n throw new Error(`inventory entry ${i} is missing required field: path`);\n }\n if (!isAbsolute(e.path)) {\n throw new Error(\n `inventory entry ${i}: path must be absolute (got \"${e.path}\"). ` +\n `Relative paths are rejected so cwd at invocation can't change which site is targeted.`,\n );\n }\n const site: Site = { path: e.path };\n if (typeof e.name === \"string\") site.name = e.name;\n if (typeof e.repoUrl === \"string\") site.repoUrl = e.repoUrl;\n // Carry gitRepo/deployedUrl like the Airtable provider does, so a JSON\n // inventory can drive checkout (clone-from-gitRepo) and deployed-URL audits.\n if (typeof e.gitRepo === \"string\") site.gitRepo = e.gitRepo;\n // Scheme-allowlist deployedUrl before it can reach Chrome/lhci (same SSRF /\n // local-file gate as the Airtable provider). A non-http(s) value is dropped\n // with a warning rather than trusted into the deployed audit.\n if (typeof e.deployedUrl === \"string\") {\n if (isHttpUrl(e.deployedUrl)) {\n site.deployedUrl = e.deployedUrl;\n } else {\n console.warn(\n `[inventory] entry ${i}: ignoring deployedUrl that is not http(s): ${JSON.stringify(e.deployedUrl)}`,\n );\n }\n }\n if (typeof e.meta === \"object\" && e.meta !== null) {\n site.meta = e.meta as Record<string, unknown>;\n }\n return site;\n });\n}\n\nexport function fromJsonFile(path: string): InventoryProvider {\n return async () => {\n const text = await readFile(path, \"utf-8\");\n let raw: unknown;\n try {\n raw = JSON.parse(text);\n } catch (e) {\n // A bare JSON.parse SyntaxError (\"Unexpected token … at position N\") names\n // neither the file nor that it's the inventory — useless to an operator\n // running a fleet command. Rethrow with the path for an actionable message,\n // preserving the original SyntaxError as `cause`.\n throw new Error(`could not parse inventory file ${path}: ${(e as Error).message}`, {\n cause: e,\n });\n }\n return validate(raw);\n };\n}\n","/**\n * True when `s` parses as an absolute URL whose scheme is `http:` or `https:`.\n *\n * The single allowlist gate for any value we hand to Chrome/Lighthouse. A\n * deployed-audit URL flows in from Airtable's `url` column (or a JSON\n * inventory's `deployedUrl`), so a `file://`/`gopher://`/`data:` value — or a\n * value pointing at an internal host — would otherwise become a local-file read\n * or SSRF when lhci drives a headless browser at it. Restricting to http(s)\n * keeps the audit to the real, network-reachable site.\n */\nexport function isHttpUrl(s: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(s);\n } catch {\n return false;\n }\n return parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n}\n","import type { FieldSet } from \"airtable\";\nimport type { AirtableBase } from \"./client.js\";\nimport type { LighthouseScores } from \"../types.js\";\n\nexport const WEBSITES_TABLE = \"Websites\";\n\nexport type Frequency = \"None\" | \"Monthly\" | \"Quarterly\" | \"Yearly\";\n\nexport type Status =\n | \"in development\"\n | \"launch period\"\n | \"maintenance\"\n | \"hosting\"\n | \"probably not our problem\"\n | \"deprecated\";\n\nexport type WebsiteRow = {\n id: string;\n name: string;\n url: string;\n status: Status | null;\n pointOfContact: string | null;\n maintenanceFreq: Frequency;\n testingFreq: Frequency;\n /** Last manually-recorded maintenance day (used as fallback when no Reports row exists). */\n maintenanceDay: string | null;\n testingDay: string | null;\n ga4PropertyId: string | null;\n /** Operator-supplied query for the Google search-presence check (e.g. the business name).\n * Null = no query set → the check is skipped for this site. */\n searchQuery: string | null;\n /** Explicit Search Console property for this site (`sc-domain:...` or `https://.../`).\n * Null = auto-resolve from the SA's visible properties by host. */\n searchConsoleProperty: string | null;\n /** GitHub repo identity as `owner/repo`. Null = no git wiring → self-update ops skip\n * (or, for local runs, fall back to the checkout's origin remote). */\n gitRepo: string | null;\n reportRecipientsTo: string | null;\n reportRecipientsCc: string | null;\n /** First attachment in the Header image field (Airtable's signed URL — fetch before expiry). */\n headerImage: { url: string; filename: string; type: string } | null;\n /** Lighthouse \"current state\" snapshot, kept fresh by `audit lighthouse --write-airtable`. */\n pScore: number | null;\n rScore: number | null;\n bpScore: number | null;\n seoScore: number | null;\n /** ISO timestamp set by `audit lighthouse --write-airtable` when scores were last refreshed. */\n lastLighthouseAuditAt: string | null;\n /** Last-known counts from non-lighthouse audits, written by\n * `audit --write-airtable`. `null` = never audited (or this audit\n * type was skipped on the last run). 0 = audited, clean. */\n a11yViolations: number | null;\n /** Declared-range drift vs the Reddoor baseline (what package.json asks for). */\n depsDrifted: number | null;\n depsMajorBehind: number | null;\n /** Real installed-version drift: deps behind the registry's latest, from the\n * committed lockfile (`pnpm outdated`). Null = not determined this run. */\n depsOutdated: number | null;\n securityVulnsCritical: number | null;\n securityVulnsHigh: number | null;\n securityVulnsModerate: number | null;\n securityVulnsLow: number | null;\n /** Per-site copy overrides (M6a). Blank → null → the DEFAULT_COPY value. */\n copyIntro: string | null;\n copyContact: string | null;\n copyFooter: string | null;\n /** Go-live timestamp, stamped when a Launch report sends (M6b). Null = not yet launched. */\n launchedAt: string | null;\n /** GitHub-signals sweep (slice 2a), written nightly by `github-signals --fleet`. */\n renovateFailingCis: number | null;\n defaultBranchCi: string | null; // \"passing\" | \"failing\" | \"pending\" | \"none\"\n lastCommitAt: string | null;\n githubSignalsAt: string | null;\n};\n\nexport function siteSlug(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\n/** Blank-trim-to-null: a non-string or whitespace-only value becomes null,\n * otherwise the trimmed string. */\nfunction trimToNull(raw: unknown): string | null {\n if (typeof raw !== \"string\") return null;\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n/**\n * Active sites: actively-maintained or pre-launch. Single source of truth for\n * \"is this a live site\" — the operator cockpit shows these, and the fleet\n * audit/report path runs against these. A `null` status (not-yet-active) is\n * deliberately excluded.\n */\nexport const ACTIVE_STATUSES: ReadonlySet<Status> = new Set<Status>([\n \"maintenance\",\n \"launch period\",\n]);\n\nexport function isDashboardVisible(site: WebsiteRow): boolean {\n return site.status !== null && ACTIVE_STATUSES.has(site.status);\n}\n\n// NOTE: every `f[\"...\"]` key below is a load-bearing magic string that must match\n// the live Airtable \"Websites\" column name EXACTLY — including the legacy\n// misspelling `\"maintenence freq\"`, the mixed-case `\"GA4 property ID\"`, and the\n// lowercase `\"url\"` / `\"point of contact\"`. A column rename in Airtable silently\n// returns undefined here (→ null), which degrades quietly (GA skipped, recipients\n// empty) with no error. If you rename a column, change it here too.\nexport function mapRow(rec: { id: string; fields: Record<string, unknown> }): WebsiteRow {\n const f = rec.fields;\n const attachments =\n (f[\"Header image\"] as Array<{ url: string; filename: string; type: string }> | undefined) ?? [];\n const header = attachments[0] ?? null;\n return {\n id: rec.id,\n name: String(f[\"Name\"] ?? \"\"),\n url: String(f[\"url\"] ?? \"\"),\n status: (f[\"Status\"] as Status | undefined) ?? null,\n pointOfContact: (f[\"point of contact\"] as string | undefined) ?? null,\n maintenanceFreq: ((f[\"maintenence freq\"] as string | undefined) ?? \"None\") as Frequency,\n testingFreq: ((f[\"testing freq\"] as string | undefined) ?? \"None\") as Frequency,\n maintenanceDay: (f[\"maintenance day\"] as string | undefined) ?? null,\n testingDay: (f[\"testing day\"] as string | undefined) ?? null,\n ga4PropertyId: (f[\"GA4 property ID\"] as string | undefined) ?? null,\n searchQuery: (f[\"Search query\"] as string | undefined) ?? null,\n searchConsoleProperty: (f[\"Search Console property\"] as string | undefined) ?? null,\n gitRepo: (f[\"Git repo\"] as string | undefined) ?? null,\n reportRecipientsTo: (f[\"Report recipients (To)\"] as string | undefined) ?? null,\n reportRecipientsCc: (f[\"Report recipients (CC)\"] as string | undefined) ?? null,\n headerImage: header,\n pScore: (f[\"pScore\"] as number | undefined) ?? null,\n rScore: (f[\"rScore\"] as number | undefined) ?? null,\n bpScore: (f[\"bpScore\"] as number | undefined) ?? null,\n seoScore: (f[\"seoScore\"] as number | undefined) ?? null,\n lastLighthouseAuditAt: (f[\"Last lighthouse audit at\"] as string | undefined) ?? null,\n a11yViolations: (f[\"A11y Violations\"] as number | undefined) ?? null,\n depsDrifted: (f[\"Deps Drifted\"] as number | undefined) ?? null,\n depsMajorBehind: (f[\"Deps Major Behind\"] as number | undefined) ?? null,\n depsOutdated: (f[\"Deps Outdated\"] as number | undefined) ?? null,\n securityVulnsCritical: (f[\"Security Vulns Critical\"] as number | undefined) ?? null,\n securityVulnsHigh: (f[\"Security Vulns High\"] as number | undefined) ?? null,\n securityVulnsModerate: (f[\"Security Vulns Moderate\"] as number | undefined) ?? null,\n securityVulnsLow: (f[\"Security Vulns Low\"] as number | undefined) ?? null,\n copyIntro: trimToNull(f[\"Copy — Intro\"]),\n copyContact: trimToNull(f[\"Copy — Contact\"]),\n copyFooter: trimToNull(f[\"Copy — Footer\"]),\n launchedAt: (f[\"Launched at\"] as string | undefined) ?? null,\n renovateFailingCis: (f[\"Renovate Failing CIs\"] as number | undefined) ?? null,\n defaultBranchCi: (f[\"Default Branch CI\"] as string | undefined) ?? null,\n lastCommitAt: (f[\"Last Commit At\"] as string | undefined) ?? null,\n githubSignalsAt: (f[\"GitHub Signals At\"] as string | undefined) ?? null,\n };\n}\n\nexport async function listWebsites(base: AirtableBase): Promise<WebsiteRow[]> {\n const out: WebsiteRow[] = [];\n await base(WEBSITES_TABLE)\n .select({ pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\nexport async function getWebsiteBySlug(\n base: AirtableBase,\n slug: string,\n): Promise<WebsiteRow | null> {\n // Slugs are siteSlug() output: [a-z0-9] segments joined by single hyphens.\n // Reject anything else — it can't match a real row, and it keeps URL-supplied\n // input out of the filter formula below (formula-injection guard).\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) return null;\n\n // Narrow the fetch to the slug-matching row server-side instead of paging the\n // whole table per request (MEDIUM-H). The formula replicates siteSlug() on\n // {Name} — lowercase → non-alnum runs to \"-\" → strip leading/trailing \"-\" —\n // verified against the live base. maxRecords caps it (slug collisions keep the\n // prior first-match-wins behavior).\n const formula = `REGEX_REPLACE(REGEX_REPLACE(LOWER({Name}),\"[^a-z0-9]+\",\"-\"),\"^-|-$\",\"\")=${JSON.stringify(\n slug,\n )}`;\n const rows: WebsiteRow[] = [];\n await base(WEBSITES_TABLE)\n .select({ filterByFormula: formula, maxRecords: 1 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n // Confirm the match in JS too: keeps the function correct if the formula and\n // siteSlug() ever drift, and under test fakes that don't evaluate the formula.\n return rows.find((w) => siteSlug(w.name) === slug) ?? null;\n}\n\n// ── audit-field builders ─────────────────────────────────────────────────────\n// One source of truth for the column-name → value mappings of each audit type.\n// The per-audit `updateXxxCounts` writers delegate to these (for their other\n// callers), and `updateAuditFields` merges whichever are present into ONE write —\n// so the field-name magic strings live in exactly one place.\n\nexport type A11yCounts = { violations: number };\nexport type DepsCounts = { drifted: number; majorBehind: number; outdated: number | null };\nexport type SecurityCounts = { critical: number; high: number; moderate: number; low: number };\n\nfunction scoreFields(scores: LighthouseScores): FieldSet {\n return {\n pScore: scores.performance,\n rScore: scores.accessibility,\n bpScore: scores.bestPractices,\n seoScore: scores.seo,\n \"Last lighthouse audit at\": new Date().toISOString(),\n };\n}\n\nfunction a11yFields(counts: A11yCounts): FieldSet {\n return { \"A11y Violations\": counts.violations };\n}\n\nfunction depsFields(counts: DepsCounts): FieldSet {\n const fields: FieldSet = {\n \"Deps Drifted\": counts.drifted,\n \"Deps Major Behind\": counts.majorBehind,\n };\n // Only write the outdated count when it was determined — a null (no/stale\n // lockfile this run) must not clobber a previously-good value.\n if (counts.outdated !== null) {\n fields[\"Deps Outdated\"] = counts.outdated;\n }\n return fields;\n}\n\nfunction securityFields(counts: SecurityCounts): FieldSet {\n return {\n \"Security Vulns Critical\": counts.critical,\n \"Security Vulns High\": counts.high,\n \"Security Vulns Moderate\": counts.moderate,\n \"Security Vulns Low\": counts.low,\n };\n}\n\n/**\n * Write the four Lighthouse scores + a refreshed-at timestamp onto a Websites row.\n * Called by `audit lighthouse --write-airtable` after a successful audit run, so\n * the operator never has to paste numbers manually before drafting a report.\n */\nexport async function updateScores(\n base: AirtableBase,\n recordId: string,\n scores: LighthouseScores,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: scoreFields(scores) }]);\n}\n\n/** Persist a11y violation count. */\nexport async function updateA11yCounts(\n base: AirtableBase,\n recordId: string,\n counts: A11yCounts,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: a11yFields(counts) }]);\n}\n\n/** Persist deps drift counts (declared-range drift + real outdated installs). */\nexport async function updateDepsCounts(\n base: AirtableBase,\n recordId: string,\n counts: DepsCounts,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: depsFields(counts) }]);\n}\n\n/** Persist security vulnerability counts by severity. */\nexport async function updateSecurityCounts(\n base: AirtableBase,\n recordId: string,\n counts: SecurityCounts,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: securityFields(counts) }]);\n}\n\n/**\n * Persist all of a single audit run's results to one Websites row in ONE atomic\n * `update()` — instead of up to four sequential updates on the same id (which left\n * a row half-written on a mid-sequence failure and quadrupled the request volume).\n * Pass only the audit slices that produced real values; each present slice is merged\n * via the SAME field mappings the per-audit writers use. Omit a slice (or pass\n * undefined) to leave those columns untouched. Returns the merged FieldSet so the\n * caller can enumerate what was written.\n */\nexport async function updateAuditFields(\n base: AirtableBase,\n recordId: string,\n audits: {\n scores?: LighthouseScores;\n a11y?: A11yCounts;\n deps?: DepsCounts;\n security?: SecurityCounts;\n },\n): Promise<FieldSet> {\n const fields: FieldSet = {};\n if (audits.scores) Object.assign(fields, scoreFields(audits.scores));\n if (audits.a11y) Object.assign(fields, a11yFields(audits.a11y));\n if (audits.deps) Object.assign(fields, depsFields(audits.deps));\n if (audits.security) Object.assign(fields, securityFields(audits.security));\n await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);\n return fields;\n}\n\n/** Persist the GitHub-signals sweep onto a Websites row (slice 2a). A null\n * `lastCommitAt` is OMITTED so a not-determined-this-run value never clobbers a\n * previously-good timestamp (mirrors updateDepsCounts' outdated handling). */\nexport async function updateGitHubSignals(\n base: AirtableBase,\n recordId: string,\n signals: {\n renovateFailingCis: number;\n ciState: string;\n lastCommitAt: string | null;\n sweptAt: string;\n },\n): Promise<void> {\n const fields: FieldSet = {\n \"Renovate Failing CIs\": signals.renovateFailingCis,\n \"Default Branch CI\": signals.ciState,\n \"GitHub Signals At\": signals.sweptAt,\n };\n if (signals.lastCommitAt !== null) {\n fields[\"Last Commit At\"] = signals.lastCommitAt;\n }\n await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);\n}\n\n/** Mark a site launched: flip Status → maintenance + stamp Launched at (M6b).\n * The first code that writes Status. Called after a Launch report sends. */\nexport async function updateLaunched(\n base: AirtableBase,\n recordId: string,\n at: string,\n): Promise<void> {\n const fields: FieldSet = { Status: \"maintenance\", \"Launched at\": at };\n await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);\n}\n","import type { Site, InventoryProvider } from \"../types.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { listWebsites, siteSlug, ACTIVE_STATUSES } from \"../reports/airtable/websites.js\";\nimport { isHttpUrl } from \"../util/url.js\";\n\nexport type AirtableInventoryOptions = {\n /**\n * Local workdir to compute each site's path as `{workdir}/{slug}`.\n * Defaults to REDDOOR_FLEET_WORKDIR env var if not provided.\n * Airtable doesn't store local checkout paths, so this is required.\n */\n workdir?: string;\n};\n\n/**\n * Read sites from the Airtable Websites table as an InventoryProvider.\n * Each row becomes one Site; `path` is computed as `{workdir}/{slug}`.\n * Only `maintenance` / `launch period` sites that have a `url` are included\n * (the live sites we audit + report on). The production URL is exposed as\n * `Site.deployedUrl` so the lighthouse audit can run against it with no\n * checkout. `repoUrl` is intentionally NOT set from `url` — a clone source\n * must come from `gitRepo` (`owner/repo`), never the production URL.\n */\nexport function fromAirtableBase(\n base: AirtableBase,\n opts: AirtableInventoryOptions = {},\n): InventoryProvider {\n return async (): Promise<Site[]> => {\n const workdir = opts.workdir ?? process.env.REDDOOR_FLEET_WORKDIR;\n if (!workdir) {\n throw new Error(\n \"fromAirtableBase requires `workdir` option or REDDOOR_FLEET_WORKDIR env (sites need a local path)\",\n );\n }\n const websites = await listWebsites(base);\n return websites\n .filter((w) => w.status !== null && ACTIVE_STATUSES.has(w.status) && w.url.length > 0)\n .flatMap((w) => {\n const slug = siteSlug(w.name);\n // An empty slug (a Name with no slug-able characters) can't form a stable\n // path and — fatally — can't be matched back to its Websites row on\n // write-back: every empty-slug site would collapse under the \"\" key and\n // mis-write or fail. Skip it loudly rather than silently mis-map it.\n if (slug.length === 0) {\n console.warn(\n `[inventory] skipping \"${w.name}\" (row ${w.id}): Name has no slug-able characters (empty slug)`,\n );\n return [];\n }\n const site: Site = {\n path: `${workdir}/${slug}`,\n name: slug,\n meta: { airtableRowId: w.id, displayName: w.name },\n };\n // Scheme-allowlist the Airtable `url` before exposing it as the\n // deployed-audit target (it's handed straight to Chrome/lhci). A\n // `file://`/`gopher://`/internal-host value would be a local-file read\n // or SSRF — skip the deployed audit for that site rather than trust it.\n if (isHttpUrl(w.url)) {\n site.deployedUrl = w.url;\n } else {\n console.warn(\n `[inventory] skipping deployed audit for \"${w.name}\": url is not http(s): ${JSON.stringify(w.url)}`,\n );\n }\n if (w.gitRepo) site.gitRepo = w.gitRepo;\n return [site];\n });\n };\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { ReportType, LighthouseScores } from \"./types.js\";\nimport { renderReportHtml } from \"./render.js\";\nimport { siteSlug } from \"./airtable/websites.js\";\nimport { resolveCopy } from \"./copy.js\";\nimport type { WebsiteRow } from \"./airtable/websites.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { createDraft, setDraftReady, listReportsForSite } from \"./airtable/reports.js\";\nimport { uploadAttachment } from \"./airtable/attachments.js\";\nimport type { AirtableBase } from \"./airtable/client.js\";\nimport { readGaConfig } from \"./ga/config.js\";\nimport { fetchPeriodUsers } from \"./ga/client.js\";\nimport { fetchSearchPresence } from \"./search/client.js\";\nimport type { SearchPresence } from \"./search/client.js\";\n\nexport type DraftOptions = {\n /** Where to write the local preview HTML when `previewOnly`. Defaults to `reports/<slug>/draft.html`. */\n previewPath?: string;\n /** If true: render locally only, never touch Airtable. */\n previewOnly?: boolean;\n /** UTC \"YYYY-MM\" recurrence key; falls back to periodEnd's month when omitted. */\n period?: string;\n /** Airtable record id of an EXISTING (not-ready) row to COMPLETE in place rather\n * than creating a new one. When set, we skip createDraft and only re-render →\n * upload the HTML attachment → flip Draft ready on this row. Used by the --due\n * re-draft path to finish a draft whose createDraft succeeded but whose\n * setDraftReady never ran (a crash mid-sequence wedged the period). */\n completeRowId?: string;\n /** The mapped ReportRow being completed, returned as `reportRow` from the\n * complete path so callers keep the same shape they get on the create path. */\n existingRow?: ReportRow;\n};\n\n/** An enrichment fetch that *errored* (not one that was legitimately skipped\n * because it isn't configured / the site lacks the inputs). Surfaced so a\n * fleet-wide GA/Search outage is visible in a `--due` batch summary instead of\n * hiding behind one easily-missed console.warn per site. */\nexport type SoftFailure = \"ga\" | \"search\";\n\nexport type DraftResult = {\n /** null when previewOnly. */\n reportRow: ReportRow | null;\n /** Path to the local preview file (only set when previewOnly). */\n htmlPath: string | null;\n /** Always present — the rendered HTML string. */\n html: string;\n /** Enrichment fetches that errored for this site (empty on success or skip). */\n softFailures: SoftFailure[];\n};\n\nfunction scoresFromWebsite(siteRow: WebsiteRow): LighthouseScores {\n const { pScore, rScore, bpScore, seoScore } = siteRow;\n if (pScore === null || rScore === null || bpScore === null || seoScore === null) {\n throw new Error(\n `Site '${siteRow.name}' is missing one or more Lighthouse scores on the Websites row (pScore, rScore, bpScore, seoScore). ` +\n `Run 'reddoor-maint audit lighthouse' from the site's checkout and paste the four numbers into Airtable, then retry.`,\n );\n }\n return { performance: pScore, accessibility: rScore, bestPractices: bpScore, seo: seoScore };\n}\n\nfunction daysAgo(today: Date, n: number): Date {\n // UTC accessors to stay TZ-consistent with `due.ts` (and avoid landing\n // Airtable's `Period start` on a different calendar day than the operator\n // expects on late-night runs near a month boundary). See morning brief\n // 2026-05-29 (M1) for context.\n const out = new Date(today);\n out.setUTCDate(out.getUTCDate() - n);\n return out;\n}\n\n/**\n * Render and create an Airtable draft for one site.\n *\n * No idempotency guard here — the recurrence guard lives in draftDueReports\n * (cli/commands/report.ts), keyed on reportPeriodKey(dueDate). The manual\n * single-site path intentionally always drafts (an operator asking for a draft\n * gets one). findReportByPeriod (airtable/reports.ts) is the real-Airtable\n * point lookup available to dashboard/digest callers that need the same\n * idempotency guarantee outside the CLI batch loop.\n */\nexport async function draftReportForSite(\n base: AirtableBase | null,\n siteRow: WebsiteRow,\n reportType: ReportType,\n options: DraftOptions = {},\n): Promise<DraftResult> {\n const scores = scoresFromWebsite(siteRow);\n\n const today = new Date();\n const slug = siteSlug(siteRow.name);\n\n const periodStart =\n base !== null ? await derivePeriodStart(base, siteRow, reportType, today) : daysAgo(today, 30);\n\n const periodEnd = today;\n const completedOn = today;\n const lastTestedDate =\n reportType === \"Maintenance\" && siteRow.testingDay ? new Date(siteRow.testingDay) : null;\n\n // GA enrichment (real path only). Soft-fail: any GA problem leaves the numbers null so\n // the draft still proceeds (operator fills them manually) — GA is an enhancement, not a\n // gate. Rendered with the fetched numbers so the review HTML matches the Airtable fields.\n // An *error* (vs a legitimate not-configured skip) is recorded in softFailures so the\n // caller can surface a fleet-wide outage in the batch summary.\n const gaResult =\n base !== null ? await fetchGaUsers(siteRow, periodStart, periodEnd) : NO_ENRICHMENT;\n const searchResult =\n base !== null ? await fetchSearch(siteRow, periodStart, periodEnd) : NO_ENRICHMENT;\n const gaUsers = gaResult.value;\n const search = searchResult.value;\n const softFailures: SoftFailure[] = [\n ...(gaResult.softFailed ? ([\"ga\"] as const) : []),\n ...(searchResult.softFailed ? ([\"search\"] as const) : []),\n ];\n\n const cidName = `${slug}-header`;\n const { html } = await renderReportHtml({\n siteName: siteRow.name,\n siteUrl: siteRow.url,\n reportType,\n completedOn,\n lighthouse: scores,\n gaUsersCurrent: gaUsers?.current,\n gaUsersPrevious: gaUsers?.previous,\n searchPosition: search?.foundOnPage1 ? (search.position ?? undefined) : undefined,\n lastTestedDate,\n commentary: null,\n copy: resolveCopy(siteRow),\n headerImageCid: cidName,\n });\n\n if (options.previewOnly) {\n const path = options.previewPath ?? `reports/${slug}/draft.html`;\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, html, \"utf-8\");\n return { reportRow: null, htmlPath: path, html, softFailures };\n }\n\n if (base === null) throw new Error(\"base required when previewOnly=false\");\n\n // \"Finish an existing row\" path (the --due re-draft wedge fix). When the caller\n // hands us a row that was created but never made Draft-ready — a crash between\n // createDraft and setDraftReady leaves exactly this — we DON'T createDraft again\n // (that would duplicate the period). We re-attach the rendered HTML and flip the\n // ready flag against the EXISTING row, completing the half-made draft in place.\n // The row's other fields (scores, period, dates) were already written at create\n // time; the only pieces a crash drops are the attachment + the ready flag.\n if (options.completeRowId) {\n await finishDraftRow(base, options.completeRowId, slug, periodEnd, html);\n return { reportRow: options.existingRow ?? null, htmlPath: null, html, softFailures };\n }\n\n const reportId = `${siteRow.name} — ${reportType} — ${periodEnd.toISOString().slice(0, 10)}`;\n const created = await createDraft(base, {\n reportId,\n siteId: siteRow.id,\n reportType,\n period: options.period ?? periodEnd.toISOString().slice(0, 7),\n periodStart,\n periodEnd,\n completedOn,\n lighthouse: scores,\n lastTestedDate,\n ...(gaUsers ? { gaUsersCurrent: gaUsers.current, gaUsersPrevious: gaUsers.previous } : {}),\n ...(search ? { searchFoundPage1: search.foundOnPage1 } : {}),\n ...(search?.foundOnPage1 && search.position !== null\n ? { searchPosition: search.position }\n : {}),\n });\n\n await finishDraftRow(base, created.id, slug, periodEnd, html);\n\n return { reportRow: created, htmlPath: null, html, softFailures };\n}\n\n/** Attach the rendered HTML and flip Draft ready=true on an existing Reports row.\n * Shared by both the create path and the \"complete a half-made row\" path so the\n * upload + ready-flag steps are identical (and re-runnable) either way. */\nasync function finishDraftRow(\n base: AirtableBase,\n rowId: string,\n slug: string,\n periodEnd: Date,\n html: string,\n): Promise<void> {\n const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;\n await uploadAttachment(rowId, \"Rendered HTML\", html, htmlFilename, \"text/html\");\n await setDraftReady(base, rowId, true);\n}\n\n/** Result of an enrichment fetch: the value (null if unavailable) plus whether\n * it errored (`softFailed`) as opposed to being legitimately not-configured. */\ntype Enrichment<T> = { value: T | null; softFailed: boolean };\n/** A not-configured / skipped enrichment — null value, not a soft-failure. */\nconst NO_ENRICHMENT: Enrichment<never> = { value: null, softFailed: false };\n\n/**\n * Fetch GA \"Users\" for the period, soft-failing to null. Returns a null value (no enrichment)\n * when GA isn't configured (`GA_SUBJECT` unset) or the site has no GA4 property ID — those are\n * legitimate skips, `softFailed: false`. When the GA API errors it logs a one-line warning and\n * returns `softFailed: true`. Never throws, so a GA problem can never block a draft; the\n * operator can always enter the numbers by hand.\n */\nasync function fetchGaUsers(\n siteRow: WebsiteRow,\n periodStart: Date,\n periodEnd: Date,\n): Promise<Enrichment<{ current: number; previous: number }>> {\n const cfg = readGaConfig();\n if (!cfg || !siteRow.ga4PropertyId) return NO_ENRICHMENT;\n try {\n const value = await fetchPeriodUsers(\n { propertyId: siteRow.ga4PropertyId, subject: cfg.subject, keyPath: cfg.keyPath },\n periodStart,\n periodEnd,\n );\n return { value, softFailed: false };\n } catch (e) {\n console.warn(`⚠ GA skipped for ${siteRow.name}: ${(e as Error).message}`);\n return { value: null, softFailed: true };\n }\n}\n\n/**\n * Fetch the site's Google search presence for the period, soft-failing to null. Returns a null\n * value when GA/SA isn't configured (`readGaConfig()` null — search shares the SA credentials)\n * or the site has no `searchQuery` (legitimate skips, `softFailed: false`). When the Search\n * Console API errors it logs a one-line warning and returns `softFailed: true`. Never throws,\n * so a search problem can never block a draft.\n */\nasync function fetchSearch(\n siteRow: WebsiteRow,\n periodStart: Date,\n periodEnd: Date,\n): Promise<Enrichment<SearchPresence>> {\n const cfg = readGaConfig();\n if (!cfg || !siteRow.searchQuery) return NO_ENRICHMENT;\n try {\n const value = await fetchSearchPresence(\n {\n keyPath: cfg.keyPath,\n subject: cfg.subject,\n property: siteRow.searchConsoleProperty ?? undefined,\n host: siteRow.url,\n query: siteRow.searchQuery,\n },\n periodStart,\n periodEnd,\n );\n return { value, softFailed: false };\n } catch (e) {\n console.warn(`⚠ Search presence skipped for ${siteRow.name}: ${(e as Error).message}`);\n return { value: null, softFailed: true };\n }\n}\n\nasync function derivePeriodStart(\n base: AirtableBase,\n siteRow: WebsiteRow,\n reportType: ReportType,\n today: Date,\n): Promise<Date> {\n const prior = await listReportsForSite(base, siteRow.id);\n const sameType = prior\n .filter((r) => r.reportType === reportType && r.periodEnd)\n .map((r) => r.periodEnd!)\n .sort();\n const latest = sameType[sameType.length - 1];\n if (!latest) return daysAgo(today, 30);\n // Half-open periods. The prior report's GA/Search windows are inclusive of its\n // periodEnd, so starting this report on the *same* day double-counts that\n // boundary day across two consecutive reports (and inflates the headline Users\n // count). Start the next day instead. UTC to stay TZ-consistent with daysAgo.\n const start = new Date(latest);\n start.setUTCDate(start.getUTCDate() + 1);\n return start;\n}\n","import mjml2html from \"mjml\";\nimport type { ReportData } from \"./types.js\";\nimport { buildMjml } from \"./maintenance-email/template.js\";\nimport { buildLaunchMjml } from \"./launch-email/template.js\";\n\nexport type RenderResult = {\n html: string;\n warnings: Array<{ line: number; message: string }>;\n};\n\nexport async function renderReportHtml(data: ReportData): Promise<RenderResult> {\n const mjml = data.reportType === \"Launch\" ? buildLaunchMjml(data) : buildMjml(data);\n const out = await mjml2html(mjml, { validationLevel: \"strict\" });\n return { html: out.html, warnings: out.errors ?? [] };\n}\n","import type { WebsiteRow } from \"./airtable/websites.js\";\n\nexport type ResolvedCopy = {\n maintenanceIntro: string;\n maintenanceChecks: string[]; // 6; index 3 is the Google row's no-position default\n testingIntro: string;\n testingChecklist: string[]; // 6\n notesHeader: string;\n seoCta: string;\n contact: string[]; // closing invitation lines\n footerOrg: string;\n footerAddress: string[];\n launchHeading: string;\n launchBody: string;\n launchSetupItems: string[];\n};\n\nexport const DEFAULT_COPY: ResolvedCopy = {\n maintenanceIntro:\n \"Includes checking the hosting, DNS, Content Management System (CMS, if applicable), search indexing and security of the site for major flaws and updating as necessary.\",\n maintenanceChecks: [\n \"Reviewed Logs\",\n \"CMS Checked\",\n \"DNS Checked\",\n \"Google Indexed\",\n \"Reviewed Certificate\",\n \"Security Updates\",\n ],\n testingIntro:\n \"Testing includes checks similar to those at launch: testing on common browsers and operating systems, at different screen sizes, and checking every function, and updating all packages for performance rather than just those needed for security.\",\n testingChecklist: [\n \"Desktop Browsers\",\n \"Mobile Browsers\",\n \"Package Updates\",\n \"Bottlenecks\",\n \"Form Functionality\",\n \"Animation Functionality\",\n ],\n notesHeader: \"NOTES\",\n seoCta: \"Contact us if you are interested in more in-depth data or have questions about SEO.\",\n contact: [\"Just hit reply.\", \"We're here to help in any way we can.\"],\n footerOrg: \"Reddoor Creative, LLC\",\n footerAddress: [\"29027 Dapper Dan\", \"Fair Oaks Ranch, TX 78015\"],\n launchHeading: \"LAUNCHED\",\n launchBody:\n \"Your site is live. We've set it up on the Reddoor stack with hosting, security, and automatic maintenance so it stays fast and healthy. Here's what's in place:\",\n launchSetupItems: [\n \"Hosting, DNS, and SSL configured\",\n \"Continuous integration + automatic dependency updates\",\n \"Analytics and uptime monitoring\",\n ],\n};\n\n/** Trim an override to null when blank (mirrors the trim-to-null handling). */\nfunction override(v: string | null): string | null {\n if (typeof v !== \"string\") return null;\n const t = v.trim();\n return t.length > 0 ? t : null;\n}\n\n/**\n * Resolve a site's effective copy: DEFAULT_COPY with the three per-site narrative\n * overrides applied. Only maintenanceIntro/contact/footer are per-site (M6a §2);\n * everything else is the shared default. PURE.\n */\n/** Split an operator override into lines: tolerate CRLF, drop blank lines (a stray\n * blank in the Airtable cell shouldn't render an empty address row). */\nfunction splitLines(s: string): string[] {\n return s.split(/\\r?\\n/).filter((l) => l.trim().length > 0);\n}\n\nexport function resolveCopy(site: WebsiteRow): ResolvedCopy {\n const intro = override(site.copyIntro);\n const contact = override(site.copyContact);\n const footer = override(site.copyFooter);\n const footerLines = footer ? splitLines(footer) : null;\n return {\n ...DEFAULT_COPY,\n maintenanceIntro: intro ?? DEFAULT_COPY.maintenanceIntro,\n contact: contact ? splitLines(contact) : DEFAULT_COPY.contact,\n footerOrg: footerLines?.[0] ?? DEFAULT_COPY.footerOrg,\n footerAddress: footerLines ? footerLines.slice(1) : DEFAULT_COPY.footerAddress,\n };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const CHECK_CID = \"rd-check-png\";\nexport const BLURRED_CID = \"rd-blurred-tests-jpg\";\n\nexport type BundledImage = {\n bytes: Uint8Array;\n contentType: string;\n cid: string;\n filename: string;\n};\n\n// Walk up from the current module's URL looking for the assets dir in either\n// the dev layout (src/reports/maintenance-email/assets/) or the published\n// layout (dist/reports/maintenance-email/assets/). REQUIRED because tsup\n// inlines this module into dist/cli/bin.js — so `import.meta.url`-based\n// sibling resolution looks in dist/cli/ for the PNGs and fails with ENOENT.\n// Regression that shipped in 0.10.0–0.10.1; tests passed in dev because\n// vitest evaluates the source file where import.meta.url is already correct.\nlet cachedAssetsDir: string | null = null;\nfunction resolveAssetsDir(): string {\n if (cachedAssetsDir) return cachedAssetsDir;\n let dir = dirname(fileURLToPath(import.meta.url));\n while (true) {\n // Source layout preferred — single source of truth in the workspace\n // and the only one present in dev/test environments.\n const srcCandidate = join(dir, \"src\", \"reports\", \"maintenance-email\", \"assets\", \"check.png\");\n if (existsSync(srcCandidate)) {\n cachedAssetsDir = dirname(srcCandidate);\n return cachedAssetsDir;\n }\n // Published layout — only `dist/` ships per package.json#files, so\n // consumers fall through to here.\n const distCandidate = join(dir, \"dist\", \"reports\", \"maintenance-email\", \"assets\", \"check.png\");\n if (existsSync(distCandidate)) {\n cachedAssetsDir = dirname(distCandidate);\n return cachedAssetsDir;\n }\n const parent = dirname(dir);\n if (parent === dir) {\n throw new Error(\n `loadBundledImages: could not locate maintenance-email assets dir by walking up from ${fileURLToPath(import.meta.url)}. Checked both src/ and dist/ layouts.`,\n );\n }\n dir = parent;\n }\n}\n\n/**\n * Read the bundled image bytes from disk. Both Maintenance and Testing\n * variants reference `check.png`; only the Maintenance variant references\n * `blurredTests.jpg`.\n */\nexport async function loadBundledImages(): Promise<{\n check: BundledImage;\n blurred: BundledImage;\n}> {\n const assetsDir = resolveAssetsDir();\n const [check, blurred] = await Promise.all([\n readFile(join(assetsDir, \"check.png\")),\n readFile(join(assetsDir, \"blurredTests.jpg\")),\n ]);\n return {\n check: {\n bytes: new Uint8Array(check),\n contentType: \"image/png\",\n cid: CHECK_CID,\n filename: \"check.png\",\n },\n blurred: {\n bytes: new Uint8Array(blurred),\n contentType: \"image/jpeg\",\n cid: BLURRED_CID,\n filename: \"blurredTests.jpg\",\n },\n };\n}\n","/**\n * Shared HTML/XML escape. One implementation behind the dashboard renderers\n * (`src/dashboard/render.ts`, `fleet-render.ts`), the daily digest\n * (`src/reports/digest.ts`), and the MJML email templates\n * (`src/reports/*-email/template.ts`).\n *\n * The set is the strict-XML set (`& < > \" '`), which is exactly what MJML's\n * `validationLevel: \"strict\"` parser needs and a superset of what plain HTML text\n * interpolation needs — so the SAME function serves both sinks. Site names\n * (e.g. \"Brown & Co\"), URLs, and operator commentary must not break the markup or\n * inject. The MJML templates re-export this as `escapeXml` for their callers.\n */\nexport function escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/** Allow only http(s) URLs in an href context; everything else collapses to \"#\". */\nexport function safeUrl(raw: string): string {\n try {\n const u = new URL(raw);\n if (u.protocol === \"http:\" || u.protocol === \"https:\") return raw;\n } catch {\n // fall through\n }\n return \"#\";\n}\n","import type { ReportData } from \"../types.js\";\nimport { DEFAULT_COPY, type ResolvedCopy } from \"../copy.js\";\nimport { CHECK_CID, BLURRED_CID } from \"./assets/index.js\";\nimport { escapeHtml } from \"../../util/html.js\";\nimport { isHttpUrl } from \"../../util/url.js\";\n\n/**\n * Escape operator/site-controlled strings before interpolating into the MJML markup.\n * MJML parses as XML with `validationLevel: \"strict\"`. Under mjml@4.18 a raw `&`, `<`,\n * or `>` does NOT throw — it passes straight through into the rendered output, so an\n * unescaped value (e.g. a site name \"Brown & Co\", a URL, or commentary) silently\n * injects HTML/markup into the email. A raw `\"` inside an ATTRIBUTE value (e.g. the\n * image `href`/`alt`) is the one that throws — it terminates the attribute and trips a\n * parse error that blocks the send. So we escape for two reasons: prevent\n * HTML/markup injection in text, and prevent the attribute-quote parse error. Apply\n * to every interpolation of siteName / siteUrl / commentary / copy.\n *\n * This IS `src/util/html.ts`'s `escapeHtml` (the strict-XML set is identical),\n * re-exported under the name the email templates import (the launch template imports\n * `escapeXml` from here).\n */\nexport const escapeXml = escapeHtml;\n\n// Bundled images: shipped in dist/ via tsup onSuccess copy, attached inline via\n// CID by orchestrate.ts at send time. No external CDN dependency.\nconst CHECK_PNG = `cid:${CHECK_CID}`;\nconst BLURRED_TESTS = `cid:${BLURRED_CID}`;\n\nexport function fmtDate(d: Date | null): string {\n // Guard BOTH null AND an Invalid Date — `new Date(\"not-a-date\")` (a malformed\n // Airtable date string) is a truthy Date whose getUTC* accessors all return\n // NaN, which would render \"NaN.NaN.NaN\" into a real client email. `!d` alone\n // misses it; `Number.isNaN(d.getTime())` catches it.\n if (!d || Number.isNaN(d.getTime())) return \"\";\n // Airtable date fields are wall-clock YYYY-MM-DD strings parsed as UTC midnight.\n // Use UTC accessors so the rendered date matches what the operator entered.\n // US format: MM.DD.YYYY (Reddoor is Texas-based, clients are US).\n const mm = String(d.getUTCMonth() + 1).padStart(2, \"0\");\n const dd = String(d.getUTCDate()).padStart(2, \"0\");\n const yyyy = d.getUTCFullYear();\n return `${mm}.${dd}.${yyyy}`;\n}\n\nfunction fmtUsers(n: number): string {\n return n.toLocaleString(\"en-US\");\n}\n\nconst TREND_UP = \"#2E7D32\"; // positive green — growth reads as good\nconst TREND_NEUTRAL = \"#757575\"; // muted grey — dips/flat aren't failures (and brand red is reserved)\n\nfunction trendText(color: string, text: string): string {\n return `<mj-text color=\"${color}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${text}</mj-text>`;\n}\n\n/**\n * The line under \"{N} Users\": a directional trend vs the previous period when both numbers\n * are real, else a graceful fallback. `undefined` means GA was unavailable (distinct from a\n * real 0). Up = green; down/flat = muted grey (a traffic dip isn't a failure).\n */\nfunction analyticsTrendLine(cur: number | undefined, prev: number | undefined): string {\n if (cur === undefined || prev === undefined) {\n // GA unavailable for one/both — show the prior count if we have it, else an em dash.\n return trendText(TREND_NEUTRAL, `Last Period: ${prev !== undefined ? fmtUsers(prev) : \"—\"}`);\n }\n if (prev === 0) {\n return cur > 0\n ? trendText(TREND_UP, \"▲ New this period (0 last period)\")\n : trendText(TREND_NEUTRAL, \"Last Period: 0\");\n }\n const pct = Math.round(((cur - prev) / prev) * 100);\n const range = `(${fmtUsers(prev)} → ${fmtUsers(cur)})`;\n if (pct > 0) return trendText(TREND_UP, `▲ ${pct}% vs last period ${range}`);\n if (pct < 0) return trendText(TREND_NEUTRAL, `▼ ${Math.abs(pct)}% vs last period ${range}`);\n return trendText(TREND_NEUTRAL, `No change vs last period (${fmtUsers(prev)})`);\n}\n\nfunction maintenanceChecksSection(copy: ResolvedCopy, searchPosition?: number): string {\n const googleLabel =\n searchPosition !== undefined\n ? `Page 1 Google Result (#${searchPosition})`\n : (copy.maintenanceChecks[3] ?? \"\");\n const rows = copy.maintenanceChecks.map((label, i) => (i === 3 ? googleLabel : label));\n return rows\n .map(\n (label, i) => `\n <mj-section background-color=\"white\" padding=\"0px\"${i === rows.length - 1 ? ' padding-bottom=\"36px\"' : \"\"}>\n <mj-group>\n <mj-column padding-left=\"0px\" width=\"90%\"${i < rows.length - 1 ? ' border-bottom=\"solid #CCCCCC 1px\"' : \"\"}>\n <mj-text height=\"25px\" padding-left=\"0px\" color=\"#757575\" padding-top=\"20px\" padding-bottom=\"7.5px\" font-size=\"16px\">${escapeXml(label)}</mj-text>\n </mj-column>\n <mj-column width=\"10%\"${i < rows.length - 1 ? ' border-bottom=\"solid #CCCCCC 1px\"' : \"\"} padding-top=\"15px\">\n <mj-image align=\"right\" padding-right=\"0px\" width=\"20px\" height=\"20px\" padding-top=\"2.5px\" padding-bottom=\"15px\" src=\"${CHECK_PNG}\" />\n </mj-column>\n </mj-group>\n </mj-section>`,\n )\n .join(\"\");\n}\n\nfunction testingChecklistSection(copy: ResolvedCopy): string {\n const rows = copy.testingChecklist;\n return rows\n .map(\n (label, i) => `\n <mj-section background-color=\"#F4F4F4\" padding=\"0px\"${i === rows.length - 1 ? ' padding-bottom=\"60px\"' : \"\"}>\n <mj-group>\n <mj-column width=\"90%\" padding-left=\"0px\"${i < rows.length - 1 ? ' border-bottom=\"solid #CCCCCC 1px\"' : \"\"}>\n <mj-text height=\"25px\" padding-left=\"0px\" color=\"#757575\" padding-top=\"20px\" padding-bottom=\"7.5px\" font-size=\"16px\">${escapeXml(label)}</mj-text>\n </mj-column>\n <mj-column width=\"10%\"${i < rows.length - 1 ? ' border-bottom=\"solid #CCCCCC 1px\"' : \"\"} padding-top=\"15px\">\n <mj-image align=\"right\" padding-right=\"0px\" width=\"20px\" height=\"20px\" padding-top=\"2.5px\" padding-bottom=\"15px\" src=\"${CHECK_PNG}\" />\n </mj-column>\n </mj-group>\n </mj-section>`,\n )\n .join(\"\");\n}\n\nfunction maintenanceTestingPlaceholder(lastTested: Date | null): string {\n return `\n <mj-section background-color=\"#F4F4F4\">\n <mj-column>\n <mj-image href=\"mailto:info@reddoorla.com\" src=\"${BLURRED_TESTS}\" />\n </mj-column>\n </mj-section>\n <mj-section background-color=\"#F4F4F4\" padding-top=\"0px\">\n <mj-column>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">Last Tested: ${fmtDate(lastTested)}</mj-text>\n </mj-column>\n </mj-section>`;\n}\n\nfunction testingIntroSection(copy: ResolvedCopy): string {\n return `\n <mj-section background-color=\"#F4F4F4\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">TESTING</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${escapeXml(copy.testingIntro)}</mj-text>\n </mj-column>\n </mj-section>`;\n}\n\nfunction commentarySection(text: string, copy: ResolvedCopy): string {\n return `\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"55px\">${escapeXml(copy.notesHeader)}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${escapeXml(text).replace(/\\r\\n?|\\n/g, \"<br/>\")}</mj-text>\n </mj-column>\n </mj-section>`;\n}\n\nfunction hasHeaderDims(\n data: ReportData,\n): data is ReportData & { headerWidth: number; headerHeight: number; headerBgColor: string } {\n return Boolean(data.headerWidth && data.headerHeight && data.headerBgColor);\n}\n\nexport function headerImageTag(data: ReportData): string {\n const src = `cid:${data.headerImageCid}`;\n const alt = `${escapeXml(data.siteName)} maintenance report`;\n // escapeXml only escapes markup chars — it does NOT neutralize a dangerous URL\n // scheme. A `javascript:`/`data:` siteUrl would survive escaping and become a live\n // header href. Gate on isHttpUrl (the same http(s) allowlist the audit path uses)\n // and DROP a non-http(s) href entirely (fall back to \"#\") rather than linking it.\n const href = isHttpUrl(data.siteUrl) ? escapeXml(data.siteUrl) : \"#\";\n // Reserve the box and show a matched placeholder while the image loads / if blocked.\n // Critically, we do NOT set an mj-image `height` — MJML would emit `height:<px>` while\n // keeping `width:100%`, locking the height while the width scales and distorting the\n // image at any rendered width != the design width (mobile, narrow panes). Instead the\n // image stays `height:auto` (proportional) and the box is reserved via `aspect-ratio`\n // in the head <mj-style> below (see headerStyleBlock). `container-background-color` is\n // the placeholder; the bare fallback (no dims, e.g. local preview) keeps today's behavior.\n if (hasHeaderDims(data)) {\n return `<mj-image href=\"${href}\" src=\"${src}\" alt=\"${alt}\" width=\"${data.headerWidth}px\" css-class=\"rd-header\" container-background-color=\"${data.headerBgColor}\" />`;\n }\n return `<mj-image href=\"${href}\" src=\"${src}\" alt=\"${alt}\" />`;\n}\n\nexport function headerStyleBlock(data: ReportData): string {\n if (!hasHeaderDims(data)) return \"\";\n // Reserve the header's vertical space by aspect ratio so it scales proportionally with\n // its fluid (width:100%) width — no fixed pixel height, so it never squishes.\n // `height:auto !important` defends against any client honoring MJML's inline height.\n return `<mj-style>.rd-header img { height: auto !important; aspect-ratio: ${data.headerWidth} / ${data.headerHeight}; }</mj-style>`;\n}\n\nexport function buildMjml(data: ReportData): string {\n const copy = data.copy ?? DEFAULT_COPY;\n const isTesting = data.reportType === \"Testing\";\n const previewText = `Checked up on ${escapeXml(data.siteName)}`;\n\n return `<mjml>\n <mj-head>\n <mj-attributes>\n <mj-text font-family=\"helvetica, sans-serif\" padding-left=\"5px\" padding-right=\"5px\" />\n <mj-section padding-left=\"11%\" padding-right=\"11%\"/>\n <mj-image padding=\"0px\" />\n </mj-attributes>\n <mj-preview>${previewText}</mj-preview>\n ${headerStyleBlock(data)}\n </mj-head>\n <mj-body background-color=\"white\">\n <mj-section background-color=\"#F4F4F4\" padding-top=\"0px\" padding-bottom=\"0px\" padding-left=\"0px\" padding-right=\"0px\">\n <mj-column>\n ${headerImageTag(data)}\n </mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">COMPLETED ON</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\">${fmtDate(data.completedOn)}</mj-text>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">MAINTENANCE CHECKS</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${escapeXml(copy.maintenanceIntro)}</mj-text>\n </mj-column>\n </mj-section>\n ${maintenanceChecksSection(copy, data.searchPosition)}\n <mj-section background-color=\"#F4F4F4\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"55px\">LIGHTHOUSE SCORES*</mj-text>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"300\" padding-top=\"25px\">Performance</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\" padding-top=\"0px\">${data.lighthouse.performance}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"0px\" padding-bottom=\"36px\">Acceptable 50–89 // Ideal 90–100</mj-text>\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"300\" padding-top=\"25px\">Readability</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\" padding-top=\"0px\">${data.lighthouse.accessibility}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"0px\" padding-bottom=\"36px\">Acceptable 80–99 // Ideal 100</mj-text>\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"300\" padding-top=\"25px\">Best Practices</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\" padding-top=\"0px\">${data.lighthouse.bestPractices}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"0px\" padding-bottom=\"36px\">Acceptable 60–79 // Ideal 80–92</mj-text>\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"300\" padding-top=\"25px\">Site Structure</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\" padding-top=\"0px\">${data.lighthouse.seo}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"0px\" padding-bottom=\"36px\">Acceptable 50–89 // Ideal 90–100</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" padding-bottom=\"36px\" line-height=\"20px\">*A Lighthouse score is a numerical measure provided by Google's Lighthouse tool, which evaluates various aspects of a web page's quality.</mj-text>\n </mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">ANALYTICS</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\">${data.gaUsersCurrent !== undefined ? fmtUsers(data.gaUsersCurrent) : \"—\"} Users</mj-text>\n ${analyticsTrendLine(data.gaUsersCurrent, data.gaUsersPrevious)}\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" padding-bottom=\"36px\" line-height=\"20px\">${escapeXml(copy.seoCta)}</mj-text>\n </mj-column>\n </mj-section>\n ${isTesting ? testingIntroSection(copy) + testingChecklistSection(copy) : maintenanceTestingPlaceholder(data.lastTestedDate)}\n ${data.commentary ? commentarySection(data.commentary, copy) : \"\"}\n <mj-section background-color=\"white\">\n <mj-column padding-top=\"36px\">\n <mj-text color=\"#C00\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"700\" padding-top=\"36px\" line-height=\"36px\">Any questions, concerns or requests?</mj-text>\n ${copy.contact\n .map((line, i) =>\n i === copy.contact.length - 1\n ? `<mj-text font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"300\" padding-top=\"0px\" line-height=\"30px\" padding-bottom=\"36px\">${escapeXml(line)}</mj-text>`\n : `<mj-text font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"300\" line-height=\"30px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\\n \")}\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" line-height=\"20px\" font-style=\"italic\">Copyright ${new Date().getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"700\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">Our mailing address is:</mj-text>\n ${[copy.footerOrg, ...copy.footerAddress]\n .map(\n (line) =>\n `<mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\\n \")}\n </mj-column>\n </mj-section>\n </mj-body>\n</mjml>`;\n}\n","import type { ReportData } from \"../types.js\";\nimport { DEFAULT_COPY } from \"../copy.js\";\nimport {\n escapeXml,\n fmtDate,\n headerImageTag,\n headerStyleBlock,\n} from \"../maintenance-email/template.js\";\n\nconst RED = \"#C00\";\nconst GREY = \"#757575\";\n\n/** Purpose-built go-live email: header · LAUNCHED + date · message · what-we-set-up\n * · contact · footer. Reuses the M6a copy layer (contact/footer honor per-site\n * overrides). No maintenance checklist / Lighthouse / analytics. */\nexport function buildLaunchMjml(data: ReportData): string {\n const copy = data.copy ?? DEFAULT_COPY;\n const previewText = `${escapeXml(data.siteName)} is live`;\n // All copy — launchHeading/launchBody/launchSetupItems included — is escaped\n // (spec §3.3: all copy escaped). It keeps strict MJML from choking on a stray\n // `&`/`<` if the default copy ever gains one, matching contact/footer below.\n const setupRows = copy.launchSetupItems\n .map(\n (item) => `\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\" padding-top=\"4px\" padding-bottom=\"4px\">• ${escapeXml(item)}</mj-text>`,\n )\n .join(\"\");\n const contactRows = copy.contact\n .map(\n (line) => `\n <mj-text font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"300\" line-height=\"30px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\");\n const footerAddressRows = copy.footerAddress\n .map(\n (line) => `\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\");\n\n return `<mjml>\n <mj-head>\n <mj-attributes>\n <mj-text font-family=\"helvetica, sans-serif\" padding-left=\"5px\" padding-right=\"5px\" />\n <mj-section padding-left=\"11%\" padding-right=\"11%\"/>\n <mj-image padding=\"0px\" />\n </mj-attributes>\n <mj-preview>${previewText}</mj-preview>\n ${headerStyleBlock(data)}\n </mj-head>\n <mj-body background-color=\"white\">\n <mj-section background-color=\"#F4F4F4\" padding-top=\"0px\" padding-bottom=\"0px\" padding-left=\"0px\" padding-right=\"0px\">\n <mj-column>${headerImageTag(data)}</mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"${RED}\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">${escapeXml(copy.launchHeading)}</mj-text>\n <mj-text color=\"${RED}\" font-size=\"44px\" font-weight=\"400\">${fmtDate(data.completedOn)}</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\" padding-top=\"20px\">${escapeXml(copy.launchBody)}</mj-text>\n ${setupRows}\n </mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column padding-top=\"36px\">\n <mj-text color=\"${RED}\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"700\" padding-top=\"36px\" line-height=\"36px\">Any questions, concerns or requests?</mj-text>\n ${contactRows}\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" line-height=\"20px\" font-style=\"italic\">Copyright ${new Date().getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"700\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">Our mailing address is:</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(copy.footerOrg)}</mj-text>\n ${footerAddressRows}\n </mj-column>\n </mj-section>\n </mj-body>\n</mjml>`;\n}\n","import type { FieldSet, Records } from \"airtable\";\nimport type { AirtableBase } from \"./client.js\";\nimport type { ReportType, LighthouseScores } from \"../types.js\";\n\nexport const REPORTS_TABLE = \"Reports\";\n\nconst REPORT_TYPES: readonly ReportType[] = [\"Maintenance\", \"Testing\", \"Launch\"];\n\n/** Coerce the Airtable `Report type` (a single-select string) to a known\n * ReportType. A bare `as ReportType` cast is a compile-time lie: if the\n * single-select gains an unexpected option, the bad value flows to render.ts,\n * where `reportType === \"Launch\"` silently falls through to the Maintenance\n * template. Validate at the boundary; warn + default to \"Maintenance\" so an\n * unknown type is VISIBLE in the logs rather than silently mis-templated. */\nfunction toReportType(raw: string | undefined): ReportType {\n if (raw && (REPORT_TYPES as readonly string[]).includes(raw)) return raw as ReportType;\n if (raw)\n console.warn(`[reports] unknown Report type ${JSON.stringify(raw)} — treating as Maintenance`);\n return \"Maintenance\";\n}\n\nexport type DeliveryStatus = \"pending\" | \"delivered\" | \"bounced\" | \"complained\";\n\nexport type ReportRow = {\n id: string;\n reportId: string;\n siteId: string;\n reportType: ReportType;\n /** UTC `YYYY-MM` recurrence key (idempotency for search-before-create). Null on legacy rows. */\n period: string | null;\n periodStart: string | null;\n periodEnd: string | null;\n completedOn: string | null;\n lighthouse: LighthouseScores | null;\n gaUsersCurrent: number | null;\n gaUsersPrevious: number | null;\n searchFoundPage1: boolean | null;\n searchPosition: number | null;\n lastTestedDate: string | null;\n commentary: string | null;\n subjectOverride: string | null;\n draftReady: boolean;\n approvedToSend: boolean;\n sentAt: string | null;\n approvedAt: string | null;\n approvedBy: string | null;\n deliveryStatus: DeliveryStatus;\n renderedHtmlAttachment: { url: string; filename: string } | null;\n /** Read out of the Resend response and stored in a hidden field; needed for webhook reconciliation. */\n resendMessageId: string | null;\n};\n\n/**\n * The \"Ready for your yes\" gate: Draft ready ∧ ¬Approved to send ∧ Sent at BLANK.\n * The single source of truth for \"pending the operator's approval\" — `listPendingApproval`,\n * `runDigest`'s ready-list, the per-site dashboard, and the fleet cockpit all key off this\n * one predicate so the surfaces can't drift.\n */\nexport function isPendingApproval(r: ReportRow): boolean {\n return r.draftReady && !r.approvedToSend && r.sentAt === null;\n}\n\nfunction mapRow(rec: { id: string; fields: Record<string, unknown> }): ReportRow {\n const f = rec.fields;\n const linkSites = (f[\"Site\"] as string[] | undefined) ?? [];\n const html =\n ((f[\"Rendered HTML\"] as Array<{ url: string; filename: string }> | undefined) ?? [])[0] ?? null;\n return {\n id: rec.id,\n reportId: String(f[\"Report ID\"] ?? \"\"),\n siteId: linkSites[0] ?? \"\",\n reportType: toReportType(f[\"Report type\"] as string | undefined),\n period: (f[\"Period\"] as string | undefined) ?? null,\n periodStart: (f[\"Period start\"] as string | undefined) ?? null,\n periodEnd: (f[\"Period end\"] as string | undefined) ?? null,\n completedOn: (f[\"Completed on\"] as string | undefined) ?? null,\n lighthouse: lighthouseFromFields(f),\n gaUsersCurrent: (f[\"GA users (period)\"] as number | undefined) ?? null,\n gaUsersPrevious: (f[\"GA users (prev period)\"] as number | undefined) ?? null,\n searchFoundPage1:\n typeof f[\"Search found page 1\"] === \"boolean\" ? (f[\"Search found page 1\"] as boolean) : null,\n searchPosition: (f[\"Search position\"] as number | undefined) ?? null,\n lastTestedDate: (f[\"Last tested date\"] as string | undefined) ?? null,\n commentary: (f[\"Commentary\"] as string | undefined) ?? null,\n subjectOverride: (f[\"Subject override\"] as string | undefined) ?? null,\n draftReady: Boolean(f[\"Draft ready\"]),\n approvedToSend: Boolean(f[\"Approved to send\"]),\n sentAt: (f[\"Sent at\"] as string | undefined) ?? null,\n approvedAt: (f[\"Approved At\"] as string | undefined) ?? null,\n approvedBy: (f[\"Approved By\"] as string | undefined) ?? null,\n deliveryStatus: ((f[\"Delivery status\"] as string | undefined) ?? \"pending\") as DeliveryStatus,\n renderedHtmlAttachment: html,\n resendMessageId: (f[\"Resend message ID\"] as string | undefined) ?? null,\n };\n}\n\nfunction lighthouseFromFields(f: Record<string, unknown>): LighthouseScores | null {\n const p = f[\"Lighthouse — Performance\"];\n const a = f[\"Lighthouse — Accessibility\"];\n const b = f[\"Lighthouse — Best Practices\"];\n const s = f[\"Lighthouse — SEO\"];\n if (\n typeof p !== \"number\" ||\n typeof a !== \"number\" ||\n typeof b !== \"number\" ||\n typeof s !== \"number\"\n )\n return null;\n return { performance: p, accessibility: a, bestPractices: b, seo: s };\n}\n\nexport type DraftInput = {\n reportId: string;\n siteId: string;\n reportType: ReportType;\n /** UTC `YYYY-MM` recurrence key. Omitted on legacy callers; written only when supplied. */\n period?: string;\n periodStart: Date;\n periodEnd: Date;\n completedOn: Date;\n lighthouse: LighthouseScores;\n lastTestedDate: Date | null;\n /** GA \"Users\" for the period / previous period. Omitted when GA is not configured\n * for the site or the fetch failed — the operator fills the fields manually. */\n gaUsersCurrent?: number;\n gaUsersPrevious?: number;\n /** Search-presence result. `searchFoundPage1` is written whenever the check ran (true or\n * false — false is the operator-only negative signal). `searchPosition` only when found. */\n searchFoundPage1?: boolean;\n searchPosition?: number;\n};\n\nfunction ymd(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Escape a string for safe interpolation into an Airtable filterByFormula.\n * Airtable formulas use SQL-like string literals; we escape backslash and\n * double quote. Used wherever an externally-supplied string flows into a\n * formula (e.g. Resend message ids on the webhook path).\n */\nexport function escapeFormulaString(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nexport async function createDraft(base: AirtableBase, input: DraftInput): Promise<ReportRow> {\n // Set Delivery status to \"pending\" at creation time, NOT at send time. This\n // matters for H4: if stampSent wrote \"pending\" after the webhook had already\n // written \"delivered\" (race), the operator would see a regressed status.\n const fields: FieldSet = {\n \"Report ID\": input.reportId,\n Site: [input.siteId],\n \"Report type\": input.reportType,\n \"Period start\": ymd(input.periodStart),\n \"Period end\": ymd(input.periodEnd),\n \"Completed on\": ymd(input.completedOn),\n \"Lighthouse — Performance\": input.lighthouse.performance,\n \"Lighthouse — Accessibility\": input.lighthouse.accessibility,\n \"Lighthouse — Best Practices\": input.lighthouse.bestPractices,\n \"Lighthouse — SEO\": input.lighthouse.seo,\n \"Delivery status\": \"pending\",\n };\n if (input.lastTestedDate) fields[\"Last tested date\"] = ymd(input.lastTestedDate);\n // GA fields are written only when supplied (GA configured + fetch succeeded). When\n // omitted the row keeps them blank for manual entry — the pre-GA behavior.\n if (input.gaUsersCurrent !== undefined) fields[\"GA users (period)\"] = input.gaUsersCurrent;\n if (input.gaUsersPrevious !== undefined) fields[\"GA users (prev period)\"] = input.gaUsersPrevious;\n if (input.searchFoundPage1 !== undefined) fields[\"Search found page 1\"] = input.searchFoundPage1;\n if (input.searchPosition !== undefined) fields[\"Search position\"] = input.searchPosition;\n if (input.period !== undefined) fields[\"Period\"] = input.period;\n const created = (await base(REPORTS_TABLE).create([{ fields }])) as Records<FieldSet>;\n const rec = created[0];\n if (!rec) throw new Error(\"Airtable create returned no records\");\n return mapRow({ id: rec.id, fields: rec.fields });\n}\n\nexport async function setDraftReady(\n base: AirtableBase,\n recordId: string,\n ready: boolean,\n): Promise<void> {\n await base(REPORTS_TABLE).update([{ id: recordId, fields: { \"Draft ready\": ready } }]);\n}\n\n/**\n * Overwrite the four `Lighthouse — *` score cells (and, when supplied, `Completed\n * on`) on an EXISTING Reports row. The launch re-run path uses this: it reuses the\n * already-created Launch row but must refresh its scores to match the freshly-run\n * audit — otherwise the re-rendered preview shows new scores while the row (and the\n * eventually-sent email, which reads the row) keeps the stale ones. The create path\n * already writes fresh scores via `createDraft`; this is its update-side mirror, using\n * the same exact field names so the two stay in lockstep.\n */\nexport async function updateReportScores(\n base: AirtableBase,\n recordId: string,\n scores: LighthouseScores,\n completedOn?: Date,\n): Promise<void> {\n const fields: FieldSet = {\n \"Lighthouse — Performance\": scores.performance,\n \"Lighthouse — Accessibility\": scores.accessibility,\n \"Lighthouse — Best Practices\": scores.bestPractices,\n \"Lighthouse — SEO\": scores.seo,\n };\n if (completedOn) fields[\"Completed on\"] = ymd(completedOn);\n await base(REPORTS_TABLE).update([{ id: recordId, fields }]);\n}\n\nexport async function listSendableReports(base: AirtableBase): Promise<ReportRow[]> {\n const out: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({\n filterByFormula:\n \"AND({Draft ready} = TRUE(), {Approved to send} = TRUE(), {Sent at} = BLANK())\",\n pageSize: 100,\n })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\n/**\n * Fetch every Reports row, unfiltered. Site-scoped callers filter the result in\n * memory: the `Site` linked-record field CANNOT be formula-filtered by record id\n * (see findReportByPeriod's doc for why), and the fleet's Reports table is small\n * enough that one paged fetch-all beats N broken-or-per-site queries.\n */\nexport async function listAllReports(base: AirtableBase): Promise<ReportRow[]> {\n const out: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({ pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\nexport async function listReportsForSite(base: AirtableBase, siteId: string): Promise<ReportRow[]> {\n // Client-side match on the mapped siteId (mapRow reads the record id from the\n // REST response, where it IS present) — record ids can't appear in formulas.\n return (await listAllReports(base)).filter((r) => r.siteId === siteId);\n}\n\n/**\n * Mark a row as sent: write `Sent at` and (when known) `Resend message ID`.\n * Crucially does NOT touch `Delivery status` — that's set to \"pending\" in\n * createDraft and updated by the webhook from there. If we wrote \"pending\" here\n * we could clobber a \"delivered\" that the webhook raced ahead and wrote first (H4).\n *\n * `messageId` may be `null`: the 409 idempotency-conflict path has no recoverable\n * id (the original send's id was lost when the prior run's stamp failed), so it\n * passes null and we write ONLY `Sent at`. Writing a sentinel string like\n * \"idempotent-conflict\" into the id column would masquerade as a real Resend id\n * and silently orphan `findReportByMessageId` lookups; leaving it null is honest —\n * the row still stops replaying (Sent at is set), delivery tracking for that one\n * report is simply degraded (the id is genuinely unknowable on that path).\n */\nexport async function stampSent(\n base: AirtableBase,\n recordId: string,\n sentAt: Date,\n messageId: string | null,\n): Promise<void> {\n const fields: Record<string, string> = { \"Sent at\": sentAt.toISOString() };\n if (messageId !== null) fields[\"Resend message ID\"] = messageId;\n await base(REPORTS_TABLE).update([\n {\n id: recordId,\n fields,\n },\n ]);\n}\n\nexport async function setDeliveryStatus(\n base: AirtableBase,\n recordId: string,\n status: DeliveryStatus,\n): Promise<void> {\n await base(REPORTS_TABLE).update([{ id: recordId, fields: { \"Delivery status\": status } }]);\n}\n\n/**\n * Stamp the approval on a Reports row: flips `Approved to send` TRUE and records\n * who/when for the audit trail. The caller (approveReport handler) is responsible\n * for idempotency — this is the raw write. Never touches `Sent at`.\n */\nexport async function approveReportRow(\n base: AirtableBase,\n recordId: string,\n approvedAt: Date,\n approvedBy: string,\n): Promise<void> {\n await base(REPORTS_TABLE).update([\n {\n id: recordId,\n fields: {\n \"Approved to send\": true,\n \"Approved At\": approvedAt.toISOString(),\n \"Approved By\": approvedBy,\n },\n },\n ]);\n}\n\n/**\n * True when an `.find` rejection is a GENUINE not-found, not a transient failure.\n * The Airtable SDK stamps `.statusCode` (404) and/or `.error` (\"NOT_FOUND\") on\n * its errors. Anything else (429 rate-limit, 500 outage, bad-PAT 401, network\n * error) must NOT be masked as a 404 — see getReportById.\n */\nfunction isNotFoundError(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) return false;\n const e = err as { statusCode?: unknown; error?: unknown; name?: unknown; message?: unknown };\n if (e.statusCode === 404) return true;\n const tag = String(e.error ?? e.name ?? e.message ?? \"\");\n return tag === \"NOT_FOUND\" || /not found/i.test(tag);\n}\n\n/**\n * Fetch one Reports row by its Airtable record id, or null if it doesn't exist.\n * Only a GENUINE not-found (404 / NOT_FOUND) collapses to null; every other\n * failure (outage, 429, bad PAT, network error) is rethrown so the adapter\n * surfaces a 500 instead of a misleading 404. Swallowing all throws previously\n * turned an Airtable outage into a \"no such report\".\n */\nexport async function getReportById(\n base: AirtableBase,\n recordId: string,\n): Promise<ReportRow | null> {\n try {\n const rec = await base(REPORTS_TABLE).find(recordId);\n return mapRow({ id: rec.id, fields: rec.fields as Record<string, unknown> });\n } catch (err) {\n if (isNotFoundError(err)) return null;\n throw err;\n }\n}\n\nexport async function findReportByMessageId(\n base: AirtableBase,\n messageId: string,\n): Promise<ReportRow | null> {\n const rows: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({\n filterByFormula: `{Resend message ID} = \"${escapeFormulaString(messageId)}\"`,\n maxRecords: 1,\n })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return rows[0] ?? null;\n}\n\n/**\n * Find the Reports row for a `(site, reportType, period)` triple, or null. The\n * idempotency lookup behind search-before-create drafting.\n *\n * The site is matched CLIENT-side, never in the formula: Airtable's formula layer\n * renders linked-record fields ({Site}) as the linked rows' PRIMARY-FIELD NAMES,\n * not record ids, so any formula comparing {Site} or ARRAYJOIN({Site}) against a\n * `recXXX` id matches NOTHING (live-proven against the real base — do not\n * reintroduce that idiom). Record ids exist only in the REST response, where\n * mapRow reads them. So the formula filters on the real scalar fields (Report\n * type + Period — escaped, keeping it injection-safe if their source ever\n * changes), and the first mapped row whose siteId matches wins. The candidate\n * set is at most one row per site for the (type, period), so this stays small.\n */\nexport async function findReportByPeriod(\n base: AirtableBase,\n siteId: string,\n reportType: ReportType,\n period: string,\n): Promise<ReportRow | null> {\n const safeType = escapeFormulaString(reportType);\n const safePeriod = escapeFormulaString(period);\n const formula = `AND({Report type} = \"${safeType}\", {Period} = \"${safePeriod}\")`;\n const rows: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({ filterByFormula: formula, pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return rows.find((r) => r.siteId === siteId) ?? null;\n}\n","/** Cheap HTML sniff: an Airtable signed-URL \"200\" that is really a login/error page\n * starts with `<!doctype html`, `<html`, or `<head` after an optional UTF-8 BOM /\n * leading whitespace. We only need to catch the common error-page case, not parse\n * HTML. */\nfunction looksLikeHtml(bytes: Uint8Array): boolean {\n // Inspect the first ~64 bytes as ASCII (1 byte → 1 char; enough for a doctype /\n // opening tag). Skip a leading UTF-8 BOM (bytes EF BB BF) by index, then strip any\n // leading ASCII whitespace, and match the common HTML openers case-insensitively.\n const start = bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ? 3 : 0;\n const head = Buffer.from(bytes.slice(start, start + 64))\n .toString(\"ascii\")\n .replace(/^[\\s]+/, \"\")\n .toLowerCase();\n return head.startsWith(\"<!doctype html\") || head.startsWith(\"<html\") || head.startsWith(\"<head\");\n}\n\nexport async function fetchAttachmentBytes(\n url: string,\n): Promise<{ bytes: Uint8Array; contentType: string }> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch Airtable attachment ${res.status} ${res.statusText} (url=${url})`,\n );\n }\n const contentType = res.headers.get(\"content-type\") ?? \"application/octet-stream\";\n const ab = await res.arrayBuffer();\n const bytes = new Uint8Array(ab);\n // Sanity-gate the body: a 200 that is actually an HTML error/login page (expired\n // signed URL, auth wall) would otherwise be attached as the \"image\" and ship a\n // broken header. Accept an explicit image/* content-type; otherwise reject anything\n // that sniffs as HTML — so the send fails loudly rather than emailing a broken image.\n const isImageType = contentType.toLowerCase().startsWith(\"image/\");\n if (!isImageType && looksLikeHtml(bytes)) {\n throw new Error(\n `Airtable attachment did not return image data (content-type=\"${contentType}\", ` +\n `body looks like an HTML page — the signed URL may have expired) (url=${url})`,\n );\n }\n return { bytes, contentType };\n}\n\n/**\n * Upload bytes (or a string) as an attachment to a specific record + field.\n * Uses Airtable's content.airtable.com upload endpoint (base64 body) because\n * the standard SDK only accepts public URLs for attachments, and we don't\n * host the generated content anywhere public.\n *\n * Docs: https://airtable.com/developers/web/api/upload-attachment\n *\n * Requires AIRTABLE_PAT + AIRTABLE_BASE_ID in env (same as the rest of the\n * reports module). The fieldName is URL-encoded for the request path.\n */\nexport async function uploadAttachment(\n recordId: string,\n fieldName: string,\n body: Uint8Array | string,\n filename: string,\n contentType: string,\n): Promise<void> {\n const apiKey = process.env.AIRTABLE_PAT;\n const baseId = process.env.AIRTABLE_BASE_ID;\n if (!apiKey || !baseId) {\n throw new Error(\"AIRTABLE_PAT and AIRTABLE_BASE_ID must be set\");\n }\n const base64 =\n typeof body === \"string\"\n ? Buffer.from(body, \"utf-8\").toString(\"base64\")\n : Buffer.from(body).toString(\"base64\");\n const payload = { contentType, file: base64, filename };\n const url = `https://content.airtable.com/v0/${baseId}/${recordId}/${encodeURIComponent(fieldName)}/uploadAttachment`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n if (!res.ok) {\n throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);\n }\n}\n","import { dirname, join } from \"node:path\";\nimport { defaultCredentialsPath } from \"../../util/credentials.js\";\n\nexport type GaConfig = {\n /** Workspace user the service account impersonates (domain-wide delegation). */\n subject: string;\n /** Absolute path to the service-account JSON key file. */\n keyPath: string;\n};\n\n/**\n * Read GA configuration from the environment (credentials.env is already loaded into\n * process.env by the CLI entrypoint). Returns null when `GA_SUBJECT` is unset — the\n * signal that GA enrichment is simply not configured, so drafting skips it silently.\n *\n * `GA_SA_KEY_PATH` is optional; it defaults to `ga-service-account.json` alongside the\n * credentials file (e.g. ~/.config/reddoor-maint/), keeping the key out of the repo.\n */\nexport function readGaConfig(): GaConfig | null {\n const subject = process.env.GA_SUBJECT?.trim();\n if (!subject) return null;\n const keyPath =\n process.env.GA_SA_KEY_PATH?.trim() ||\n join(dirname(defaultCredentialsPath()), \"ga-service-account.json\");\n return { subject, keyPath };\n}\n","import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** Resolve the canonical credentials file path. Respects $XDG_CONFIG_HOME\n * (Linux/macOS convention) and falls back to ~/.config/reddoor-maint/. */\nexport function defaultCredentialsPath(): string {\n const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\");\n return join(base, \"reddoor-maint\", \"credentials.env\");\n}\n\n/** Parse a tiny subset of dotenv: `KEY=value` per line, `# comments`,\n * blank lines. A leading `export ` token is stripped (dotenv does this),\n * so a hand-edited `export AIRTABLE_PAT=…` parses instead of being dropped.\n * Quoted values strip the surrounding quotes. A non-blank, non-comment line\n * that still doesn't parse (no `=`, bad key) is skipped with a one-line\n * stderr warning naming the line number — this is a credentials file, so a\n * silent drop turns into a confusing \"missing credential\" downstream. */\nexport function parseEnvFile(contents: string): Record<string, string> {\n const out: Record<string, string> = {};\n const lines = contents.split(/\\r?\\n/);\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n // Strip a leading `export ` so `export KEY=value` (a common hand-edit)\n // parses the same as `KEY=value`.\n const line = trimmed.replace(/^export\\s+/, \"\");\n const eq = line.indexOf(\"=\");\n const key = eq > 0 ? line.slice(0, eq).trim() : \"\";\n if (eq <= 0 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n console.warn(`credentials: skipping unparseable line ${i + 1}: ${trimmed}`);\n continue;\n }\n let value = line.slice(eq + 1).trim();\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n out[key] = value;\n }\n return out;\n}\n\n/** Load credentials from `path` (default: canonical file) into `process.env`.\n * `process.env` values win — file-defined keys are only applied when the\n * env var is currently undefined. Missing/unreadable file is a silent\n * no-op; commands that need the credentials will fail downstream with\n * their own clear error. Returns the keys actually applied (diagnostics). */\nexport function loadCredentialsIntoEnv(path: string = defaultCredentialsPath()): string[] {\n let contents: string;\n try {\n contents = readFileSync(path, \"utf-8\");\n } catch {\n return [];\n }\n const parsed = parseEnvFile(contents);\n const applied: string[] = [];\n for (const [k, v] of Object.entries(parsed)) {\n if (process.env[k] === undefined) {\n process.env[k] = v;\n applied.push(k);\n }\n }\n return applied;\n}\n","import { readFileSync } from \"node:fs\";\nimport { JWT } from \"google-auth-library\";\nimport { BetaAnalyticsDataClient } from \"@google-analytics/data\";\n\nconst ANALYTICS_READONLY = \"https://www.googleapis.com/auth/analytics.readonly\";\nconst MS_PER_DAY = 86_400_000;\n\nexport type GaQuery = {\n /** GA4 numeric property ID (e.g. \"471880366\"). */\n propertyId: string;\n /** Workspace user to impersonate via domain-wide delegation. */\n subject: string;\n /** Path to the service-account JSON key. */\n keyPath: string;\n};\n\n/** UTC YYYY-MM-DD — matches the rest of the reports pipeline's date handling. */\nfunction ymd(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Fetch GA4 `activeUsers` (\"Users\") for a report period and the equal-length window\n * immediately before it, via a domain-wide-delegation service account that impersonates\n * `subject`. Throws on any auth/API error — the caller (draftReportForSite) soft-fails.\n *\n * Previous window: same length as the current period, ending the day before `periodStart`.\n */\nexport async function fetchPeriodUsers(\n query: GaQuery,\n periodStart: Date,\n periodEnd: Date,\n): Promise<{ current: number; previous: number }> {\n const key = JSON.parse(readFileSync(query.keyPath, \"utf8\")) as {\n client_email: string;\n private_key: string;\n };\n const authClient = new JWT({\n email: key.client_email,\n key: key.private_key,\n scopes: [ANALYTICS_READONLY],\n subject: query.subject,\n });\n const client = new BetaAnalyticsDataClient({ authClient });\n\n const lengthDays = Math.round((periodEnd.getTime() - periodStart.getTime()) / MS_PER_DAY);\n const prevEnd = new Date(periodStart.getTime() - MS_PER_DAY);\n const prevStart = new Date(prevEnd.getTime() - lengthDays * MS_PER_DAY);\n\n const property = `properties/${query.propertyId}`;\n const run = async (start: Date, end: Date): Promise<number> => {\n const [resp] = await client.runReport({\n property,\n dateRanges: [{ startDate: ymd(start), endDate: ymd(end) }],\n metrics: [{ name: \"activeUsers\" }],\n });\n const raw = resp.rows?.[0]?.metricValues?.[0]?.value ?? \"0\";\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) ? n : 0;\n };\n\n const current = await run(periodStart, periodEnd);\n const previous = await run(prevStart, prevEnd);\n return { current, previous };\n}\n","import { readFileSync } from \"node:fs\";\nimport { JWT } from \"google-auth-library\";\n\nconst WEBMASTERS_READONLY = \"https://www.googleapis.com/auth/webmasters.readonly\";\nconst SC_BASE = \"https://searchconsole.googleapis.com/webmasters/v3\";\n/** Average-position threshold for \"on page 1\" (10 organic results per page). */\nconst PAGE_1_MAX_POSITION = 10;\n\nexport type SearchPresenceQuery = {\n /** Path to the service-account JSON key (same one GA uses). */\n keyPath: string;\n /** Workspace user to impersonate via domain-wide delegation. */\n subject: string;\n /** Explicit Search Console property (`sc-domain:...` or `https://.../`). Overrides auto-resolution. */\n property?: string | undefined;\n /** Site host, used to auto-resolve the property from `sites.list` when `property` is absent. */\n host: string;\n /** Operator-supplied query string (e.g. the business name). */\n query: string;\n};\n\nexport type SearchPresence = {\n /** True when the average position for the query is on page 1 (<= 10). */\n foundOnPage1: boolean;\n /** Rounded average position, or null when not found / no data. */\n position: number | null;\n};\n\ntype SiteEntry = { siteUrl: string };\n\n/** Reduce any property string or URL to a bare host: no `sc-domain:`, scheme, `www.`, path, lowercased. */\nexport function bareHost(s: string): string {\n return s\n .trim()\n .replace(/^sc-domain:/i, \"\")\n .replace(/^https?:\\/\\//i, \"\")\n .split(\"/\")[0]!\n .replace(/^www\\./i, \"\")\n .toLowerCase();\n}\n\n/**\n * All Search Console properties matching `host`, ordered for query fallback: Domain\n * (`sc-domain:`) forms first (broadest coverage), then URL-prefix forms. A site can be verified\n * as both; a freshly-created Domain property has no backfilled history, so its data can be empty\n * even while a long-lived URL-prefix property has data — hence we return every match and let the\n * caller try them in order until one returns data. Empty list = nothing matches.\n */\nexport function resolvePropertyCandidates(entries: SiteEntry[], host: string): string[] {\n const target = bareHost(host);\n const matches = entries.filter((e) => bareHost(e.siteUrl) === target).map((e) => e.siteUrl);\n const domains = matches.filter((s) => s.toLowerCase().startsWith(\"sc-domain:\"));\n const prefixes = matches.filter((s) => !s.toLowerCase().startsWith(\"sc-domain:\"));\n return [...domains, ...prefixes];\n}\n\n/** UTC YYYY-MM-DD — matches the rest of the reports pipeline. */\nfunction ymd(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Query Google Search Console for the average position of `query` on the site over the report\n * period, via a domain-wide-delegation service account impersonating `subject`. Uses `property`\n * verbatim when given (operator's choice is final — no fallback); otherwise auto-discovers all\n * matching properties via `sites.list` and tries them in order (Domain first) until one returns\n * data. Throws on any auth/API error — the caller (draftReportForSite) soft-fails.\n */\nexport async function fetchSearchPresence(\n q: SearchPresenceQuery,\n periodStart: Date,\n periodEnd: Date,\n): Promise<SearchPresence> {\n const key = JSON.parse(readFileSync(q.keyPath, \"utf8\")) as {\n client_email: string;\n private_key: string;\n };\n const jwt = new JWT({\n email: key.client_email,\n key: key.private_key,\n scopes: [WEBMASTERS_READONLY],\n subject: q.subject,\n });\n\n const explicit = q.property?.trim();\n let candidates: string[];\n if (explicit) {\n candidates = [explicit];\n } else {\n const list = await jwt.request<{ siteEntry?: SiteEntry[] }>({\n url: `${SC_BASE}/sites`,\n method: \"GET\",\n });\n candidates = resolvePropertyCandidates(list.data.siteEntry ?? [], q.host);\n if (candidates.length === 0) return { foundOnPage1: false, position: null };\n }\n\n for (const property of candidates) {\n const res = await jwt.request<{ rows?: Array<{ position?: number }> }>({\n url: `${SC_BASE}/sites/${encodeURIComponent(property)}/searchAnalytics/query`,\n method: \"POST\",\n data: {\n startDate: ymd(periodStart),\n endDate: ymd(periodEnd),\n dimensions: [\"query\"],\n dimensionFilterGroups: [\n {\n filters: [\n { dimension: \"query\", operator: \"equals\", expression: q.query.toLowerCase() },\n ],\n },\n ],\n rowLimit: 1,\n },\n });\n const pos = res.data.rows?.[0]?.position;\n if (typeof pos === \"number\") {\n // Search Console can average below 1; floor to 1 so the template never\n // renders a nonsensical \"#0\" (positions are 1-indexed).\n return { foundOnPage1: pos <= PAGE_1_MAX_POSITION, position: Math.max(1, Math.round(pos)) };\n }\n }\n return { foundOnPage1: false, position: null };\n}\n","import Airtable from \"airtable\";\nimport { defaultCredentialsPath } from \"../../util/credentials.js\";\n\nexport type AirtableConfig = {\n apiKey: string;\n baseId: string;\n};\n\nfunction missing(name: string): Error {\n return Object.assign(\n new Error(\n `${name} not set. Export it in your shell or put it in ${defaultCredentialsPath()} as ${name}=...`,\n ),\n { exitCode: 2 },\n );\n}\n\nexport function readAirtableConfig(): AirtableConfig {\n const apiKey = process.env.AIRTABLE_PAT;\n const baseId = process.env.AIRTABLE_BASE_ID;\n if (!apiKey) throw missing(\"AIRTABLE_PAT\");\n if (!baseId) throw missing(\"AIRTABLE_BASE_ID\");\n return { apiKey, baseId };\n}\n\nexport type AirtableBase = ReturnType<typeof openBase>;\n\nexport function openBase(cfg: AirtableConfig) {\n return new Airtable({ apiKey: cfg.apiKey }).base(cfg.baseId);\n}\n","import sharp from \"sharp\";\n\nexport type PreparedHeaderImage = {\n /** Resized JPEG bytes to attach inline (CID) in place of the Airtable original. */\n bytes: Uint8Array;\n /** Always \"image/jpeg\" — we re-encode for predictable size and a flat white background. */\n contentType: string;\n /** CSS display width in px (≤ requested, never wider than the source has pixels for). */\n displayWidth: number;\n /** CSS display height in px, source aspect ratio preserved (no distortion). */\n displayHeight: number;\n /** Dominant-color hex (e.g. \"#cfc3a8\"), used as the loading/blocked placeholder box. */\n placeholderColor: string;\n};\n\nexport type PrepareHeaderImageOptions = {\n /** Intended CSS display width. The email body is 600px, so that's the default. */\n displayWidth?: number;\n};\n\nconst DEFAULT_DISPLAY_WIDTH = 600;\n/** Encode the source at 2× display width so it stays crisp on retina screens. */\nconst RETINA_SCALE = 2;\n/** Quality is for *resized* pixels — at 1200px the texture/text read as sharp; bytes are tiny. */\nconst JPEG_QUALITY = 82;\n\nfunction channelToHex(value: number): string {\n return Math.max(0, Math.min(255, Math.round(value)))\n .toString(16)\n .padStart(2, \"0\");\n}\n\n/**\n * Downscale an oversized header image for email: 2× the display width (retina) at most,\n * never upscaled, re-encoded as JPEG on a flat white background. Also reports the display\n * dimensions (so the template can reserve the box and stop reflow) and a dominant color\n * (so the reserved box shows a matched placeholder while the image loads).\n *\n * Root cause this addresses: Airtable headers can be multi-MB / 2400px+ while the email\n * renders them at ~600px — shipping ~16× more pixels than the display can use.\n */\nexport async function prepareHeaderImage(\n bytes: Uint8Array,\n options: PrepareHeaderImageOptions = {},\n): Promise<PreparedHeaderImage> {\n const requestedDisplayWidth = options.displayWidth ?? DEFAULT_DISPLAY_WIDTH;\n const input = Buffer.from(bytes);\n\n const meta = await sharp(input).metadata();\n const origWidth = meta.width;\n const origHeight = meta.height;\n if (!origWidth || !origHeight) {\n throw new Error(\"prepareHeaderImage: could not read source image dimensions\");\n }\n\n // Never claim a wider display than the source can fill at 1×.\n const displayWidth = Math.min(requestedDisplayWidth, origWidth);\n const displayHeight = Math.round((displayWidth * origHeight) / origWidth);\n\n // Encode at 2× display for retina, but never enlarge a smaller original.\n const targetSourceWidth = Math.min(origWidth, displayWidth * RETINA_SCALE);\n\n const out = await sharp(input)\n .resize({ width: targetSourceWidth, withoutEnlargement: true })\n .flatten({ background: \"#ffffff\" })\n .jpeg({ quality: JPEG_QUALITY })\n .toBuffer();\n\n const { dominant } = await sharp(out).stats();\n const placeholderColor = `#${channelToHex(dominant.r)}${channelToHex(dominant.g)}${channelToHex(dominant.b)}`;\n\n return {\n bytes: new Uint8Array(out),\n contentType: \"image/jpeg\",\n displayWidth,\n displayHeight,\n placeholderColor,\n };\n}\n","import { Resend } from \"resend\";\n\nexport type ResendSendInput = {\n from: string;\n to: string[];\n cc?: string[];\n replyTo?: string;\n subject: string;\n html: string;\n attachments?: Array<{\n filename: string;\n content: string; // base64\n contentType?: string;\n /** Setting this attaches the file as inline; reference it from HTML as `src=\"cid:<id>\"`. */\n inlineContentId?: string;\n }>;\n /**\n * Stable key forwarded as the `Idempotency-Key` header. Resend dedupes calls\n * with the same key for 24 hours, returning the original message id. Use a\n * key that's stable across retries of the same logical send (e.g. the\n * Reports row id), so a network blip during stamping doesn't cause a\n * duplicate email to the client.\n */\n idempotencyKey?: string;\n};\n\nexport type ResendSendResult = {\n messageId: string;\n};\n\nexport type ResendClient = {\n send: (input: ResendSendInput) => Promise<ResendSendResult>;\n};\n\nexport function defaultResendClient(): ResendClient {\n const key = process.env.RESEND_API_KEY;\n if (!key) throw Object.assign(new Error(\"RESEND_API_KEY not set\"), { exitCode: 2 });\n const resend = new Resend(key);\n return {\n async send(input) {\n const payload: Parameters<typeof resend.emails.send>[0] = {\n from: input.from,\n to: input.to,\n subject: input.subject,\n html: input.html,\n };\n if (input.cc) payload.cc = input.cc;\n if (input.replyTo) payload.replyTo = input.replyTo;\n if (input.attachments) payload.attachments = input.attachments;\n const options: Parameters<typeof resend.emails.send>[1] = {};\n if (input.idempotencyKey) options.idempotencyKey = input.idempotencyKey;\n const { data, error } = await resend.emails.send(payload, options);\n if (error) throw new Error(`Resend error: ${error.message}`);\n if (!data?.id) throw new Error(\"Resend returned no message id\");\n return { messageId: data.id };\n },\n };\n}\n","/**\n * True when a thrown send error is Resend's same-key + DIFFERENT-body 409\n * (`invalid_idempotent_request`). The ResendClient (send/resend.ts) discards the\n * status/name and only surfaces the message string, so we match defensively on the\n * stable message substring \"idempotency key has been used\" (case-insensitive); a\n * `name`/`statusCode` of 409/`invalid_idempotent_request` is also accepted if a\n * future client happens to preserve it. A same-key + SAME-body re-send is deduped\n * by Resend (returns the original id) and never reaches here.\n *\n * Shared by both send surfaces that key into Resend's idempotency window:\n * `runDigest` (digest.ts, `digest-<date>` key) and `sendOne` (orchestrate.ts,\n * `report:<id>` key). Both treat a 409 as \"the email already went out under this\n * key on a prior run\" — a no-op for the digest, an already-done success for sendOne.\n */\nexport function isIdempotencyConflict(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n if (/idempotency key has been used/i.test(message)) return true;\n const e = err as { name?: unknown; statusCode?: unknown };\n if (e.name === \"invalid_idempotent_request\") return true;\n if (e.statusCode === 409) return true;\n return false;\n}\n","import { openBase, readAirtableConfig } from \"../airtable/client.js\";\nimport { listSendableReports, stampSent } from \"../airtable/reports.js\";\nimport { listWebsites, siteSlug, updateLaunched } from \"../airtable/websites.js\";\nimport type { WebsiteRow } from \"../airtable/websites.js\";\nimport type { ReportRow } from \"../airtable/reports.js\";\nimport { fetchAttachmentBytes } from \"../airtable/attachments.js\";\nimport { renderReportHtml } from \"../render.js\";\nimport { resolveCopy } from \"../copy.js\";\nimport { loadBundledImages } from \"../maintenance-email/assets/index.js\";\nimport { prepareHeaderImage } from \"../maintenance-email/header-image.js\";\nimport { defaultResendClient, type ResendClient, type ResendSendInput } from \"./resend.js\";\nimport { isIdempotencyConflict } from \"./idempotency.js\";\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\nconst REPLY_TO = \"info@reddoorla.com\";\n\nconst MONTHS = [\n \"January\",\n \"February\",\n \"March\",\n \"April\",\n \"May\",\n \"June\",\n \"July\",\n \"August\",\n \"September\",\n \"October\",\n \"November\",\n \"December\",\n];\n\n/** \"May 2026\" — UTC month/year, consistent with the rest of the reports pipeline's dates. */\nfunction monthYear(d: Date): string {\n return `${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}`;\n}\n\ntype InlineAttachment = NonNullable<ResendSendInput[\"attachments\"]>[number];\n\n/** Build a Resend inline (CID-referenced) attachment from raw bytes — the header\n * image and both bundled images share this exact shape. */\nfunction toInlineAttachment(a: {\n bytes: Uint8Array;\n filename: string;\n contentType: string;\n cid: string;\n}): InlineAttachment {\n return {\n filename: a.filename,\n content: Buffer.from(a.bytes).toString(\"base64\"),\n contentType: a.contentType,\n inlineContentId: a.cid,\n };\n}\n\nexport type OrchestrateOptions = {\n resend?: ResendClient;\n};\n\nexport async function sendApprovedReports(\n options: OrchestrateOptions = {},\n): Promise<{ output: string; code: number }> {\n const base = openBase(readAirtableConfig());\n const client = options.resend ?? defaultResendClient();\n\n const sendable = await listSendableReports(base);\n if (sendable.length === 0) return { output: \"No reports ready to send.\", code: 0 };\n\n const websites = await listWebsites(base);\n const sites = new Map(websites.map((w) => [w.id, w]));\n\n const lines: string[] = [];\n let anyFailed = false;\n for (const report of sendable) {\n const site = sites.get(report.siteId);\n if (!site) {\n lines.push(`✗ ${report.reportId} — Site row not found for id=${report.siteId}`);\n anyFailed = true;\n continue;\n }\n try {\n const messageId = await sendOne(client, base, site, report);\n lines.push(`✓ sent: ${report.reportId} (${messageId})`);\n if (report.reportType === \"Launch\") {\n try {\n await updateLaunched(base, site.id, new Date().toISOString());\n lines.push(` ↳ launched: ${site.name} flipped to maintenance`);\n } catch (e) {\n lines.push(` ⚠ launch flip failed for ${site.name}: ${(e as Error).message}`);\n }\n }\n } catch (e) {\n lines.push(`✗ ${report.reportId} — ${(e as Error).message}`);\n anyFailed = true;\n }\n }\n return { output: lines.join(\"\\n\"), code: anyFailed ? 1 : 0 };\n}\n\nasync function sendOne(\n client: ResendClient,\n base: ReturnType<typeof openBase>,\n site: WebsiteRow,\n report: ReportRow,\n): Promise<string> {\n if (!site.headerImage) {\n throw new Error(`Site '${site.name}' has no Header image set on the Websites row`);\n }\n if (!report.lighthouse) {\n throw new Error(\n `Report ${report.reportId} has no Lighthouse scores — all four cells ` +\n `(Lighthouse — Performance / Accessibility / Best Practices / SEO) must be numeric ` +\n `on the Reports row; one non-numeric or blank cell nulls all four`,\n );\n }\n\n // Resolve + validate recipients BEFORE the expensive work (header fetch + sharp\n // downscale + full MJML render). A misconfigured-recipients site is a guaranteed\n // failure, so fail fast here rather than after burning that work. Same checks +\n // messages as before — only the position moved.\n const explicitTo = parseAddresses(site.reportRecipientsTo);\n // Run pointOfContact through the parser too — operators sometimes paste\n // \"a@x, b@y\" into that single-line field.\n const fallbackTo = parseAddresses(site.pointOfContact);\n const to = explicitTo ?? fallbackTo ?? [];\n if (to.length === 0) {\n throw new Error(\n `Site '${site.name}' has no recipients (Report recipients (To) AND point of contact are both empty)`,\n );\n }\n for (const addr of to) {\n if (!isProbablyEmail(addr)) {\n throw new Error(\n `Site '${site.name}' recipient is malformed: ${addr} — use a bare address only ` +\n `(no \\`Name <addr>\\` display-name syntax); fix Report recipients (To) or point of contact in Airtable`,\n );\n }\n }\n const cc = parseAddresses(site.reportRecipientsCc);\n if (cc) {\n for (const addr of cc) {\n if (!isProbablyEmail(addr)) {\n throw new Error(\n `Site '${site.name}' CC is malformed: ${addr} — fix Report recipients (CC) in Airtable`,\n );\n }\n }\n }\n\n const original = await fetchAttachmentBytes(site.headerImage.url);\n // Downscale the (often multi-MB / 2400px+) Airtable header to email display size, and get\n // back display dims + a placeholder color so the template can reserve the box.\n const header = await prepareHeaderImage(original.bytes);\n const bundled = await loadBundledImages();\n\n const slug = siteSlug(site.name);\n const cidName = `${slug}-header`;\n const { html } = await renderReportHtml({\n siteName: site.name,\n siteUrl: site.url,\n reportType: report.reportType,\n completedOn: report.completedOn ? new Date(report.completedOn) : new Date(),\n lighthouse: report.lighthouse,\n gaUsersCurrent: report.gaUsersCurrent ?? undefined,\n gaUsersPrevious: report.gaUsersPrevious ?? undefined,\n searchPosition:\n report.searchFoundPage1 && report.searchPosition !== null ? report.searchPosition : undefined,\n lastTestedDate: report.lastTestedDate ? new Date(report.lastTestedDate) : null,\n commentary: report.commentary,\n copy: resolveCopy(site),\n headerImageCid: cidName,\n headerWidth: header.displayWidth,\n headerHeight: header.displayHeight,\n headerBgColor: header.placeholderColor,\n });\n\n const reportDate = report.completedOn ? new Date(report.completedOn) : new Date();\n const subject =\n report.subjectOverride ?? `${site.name} — ${monthYear(reportDate)} ${report.reportType} Report`;\n\n const payload: Parameters<ResendClient[\"send\"]>[0] = {\n from: FROM_ADDRESS,\n to,\n replyTo: REPLY_TO,\n subject,\n html,\n attachments: [\n toInlineAttachment({\n bytes: header.bytes,\n filename: `${cidName}.jpg`,\n contentType: header.contentType,\n cid: cidName,\n }),\n // Bundled images referenced via cid:rd-check-png / cid:rd-blurred-tests-jpg\n // in the template. Attached inline so the email is self-contained — no\n // external CDN dependency, no image-blocked broken icons in webmail.\n toInlineAttachment({\n bytes: bundled.check.bytes,\n filename: bundled.check.filename,\n contentType: bundled.check.contentType,\n cid: bundled.check.cid,\n }),\n toInlineAttachment({\n bytes: bundled.blurred.bytes,\n filename: bundled.blurred.filename,\n contentType: bundled.blurred.contentType,\n cid: bundled.blurred.cid,\n }),\n ],\n // Stable across retries of the same row — if Airtable stamping fails after a\n // successful Resend, the next --send-ready replays with the same key and\n // Resend returns the original message id rather than sending a duplicate.\n idempotencyKey: `report:${report.id}`,\n };\n if (cc) payload.cc = cc;\n\n let result: Awaited<ReturnType<ResendClient[\"send\"]>>;\n try {\n result = await client.send(payload);\n } catch (err) {\n // The send path is at-least-once: client.send succeeds → stampSent writes\n // `Sent at` (the ONLY thing that removes the row from listSendableReports). If\n // stampSent threw on a PRIOR run (an Airtable blip), `Sent at` stayed null and\n // the row replays here. By replay time the rendered body has usually changed\n // (operator Commentary edit, `report --due` rewrote scores, or the header\n // re-encodes non-deterministically), so Resend rejects the same-key\n // (`report:<id>`) / different-body re-send with a 409 (`invalid_idempotent_request`).\n //\n // That 409 means the email ALREADY WENT OUT under this key on the prior run.\n // Do NOT re-throw and do NOT re-send (re-throwing leaves the row unstamped, and\n // after the 24h key TTL a SECOND real email would go out). Instead stamp the row\n // so it stops replaying, then return success so the caller runs the Launch flip —\n // which self-heals a launch that sent-but-never-flipped on the prior run.\n //\n // Any OTHER error (real network/Resend failure) re-throws, exactly as before, so\n // a genuine failure still fails loudly and the row replays next run.\n if (isIdempotencyConflict(err)) {\n // Stamp `Sent at` ONLY — the original send's messageId is unrecoverable on\n // the 409 path, so we leave `Resend message ID` null rather than writing a\n // sentinel that would masquerade as a real id and orphan webhook lookups.\n // Still return the sentinel string so the caller logs the already-sent path\n // and runs the Launch flip.\n await stampSent(base, report.id, new Date(), null);\n console.log(`↻ already sent (idempotency conflict), stamped: ${report.reportId}`);\n return \"idempotent-conflict\";\n }\n throw err;\n }\n await stampSent(base, report.id, new Date(), result.messageId);\n return result.messageId;\n}\n\n/**\n * Split a comma/newline-separated address field into a clean array.\n * Lowercases (case-insensitive dedupe) and removes empty entries. Returns\n * null if nothing survives. Does NOT understand `Display Name <email>` —\n * operators should put a bare address in the Airtable field, or use multiple\n * lines if needing multiple recipients.\n */\nexport function parseAddresses(field: string | null): string[] | null {\n if (!field) return null;\n const seen = new Set<string>();\n const list: string[] = [];\n for (const raw of field.split(/[,\\n]/)) {\n const trimmed = raw.trim().toLowerCase();\n if (!trimmed) continue;\n if (seen.has(trimmed)) continue;\n seen.add(trimmed);\n list.push(trimmed);\n }\n return list.length > 0 ? list : null;\n}\n\n/**\n * Cheap email shape check — must contain exactly one @, with non-empty\n * local and domain parts and at least one dot in the domain. We're not\n * trying to be a full RFC validator; we're trying to catch operator\n * mistakes like \"ops at acme dot com\" or a missing @ before they 422\n * at Resend.\n */\nexport function isProbablyEmail(s: string): boolean {\n const at = s.indexOf(\"@\");\n if (at < 1 || at !== s.lastIndexOf(\"@\")) return false;\n const local = s.slice(0, at);\n const domain = s.slice(at + 1);\n if (!local || !domain) return false;\n if (!domain.includes(\".\")) return false;\n if (/\\s/.test(s)) return false;\n return true;\n}\n","import type { WebsiteRow, Frequency, Status } from \"./airtable/websites.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport type { ReportType } from \"./types.js\";\n\n/** Statuses where reports are appropriate. Drops \"deprecated\" and\n * \"probably not our problem\" — even if the operator left a freq set, we don't\n * want to surface those sites in --due output. Sites with status=null pass\n * through (existing data is partial; better to surface than silently skip). */\nconst ELIGIBLE_STATUSES: ReadonlySet<Status> = new Set<Status>([\n \"in development\",\n \"launch period\",\n \"maintenance\",\n \"hosting\",\n]);\n\nexport type DueItem = {\n site: WebsiteRow;\n reportType: ReportType;\n /** Inclusive: the day the next report became due. */\n dueDate: Date;\n /** ISO date of the last `Sent at` for this (site, type), or null if there's never been one. */\n lastSent: string | null;\n};\n\nconst MONTHS: Record<Exclude<Frequency, \"None\">, number> = {\n Monthly: 1,\n Quarterly: 3,\n Yearly: 12,\n};\n\n/**\n * Add `n` calendar months in UTC, clamped to the last day of the target month.\n * Jan 31 + 1 month = Feb 28 (not Mar 3, which is what naive setMonth produces).\n * All-UTC accessors mean the result is timezone-independent.\n */\nfunction addMonths(d: Date, n: number): Date {\n const out = new Date(d);\n const day = out.getUTCDate();\n out.setUTCDate(1);\n out.setUTCMonth(out.getUTCMonth() + n);\n const lastDayOfTargetMonth = new Date(\n Date.UTC(out.getUTCFullYear(), out.getUTCMonth() + 1, 0),\n ).getUTCDate();\n out.setUTCDate(Math.min(day, lastDayOfTargetMonth));\n return out;\n}\n\n/** Truncate to UTC midnight. Avoids local-TZ skew when comparing Airtable date-only fields. */\nfunction startOfDay(d: Date): Date {\n const out = new Date(d);\n out.setUTCHours(0, 0, 0, 0);\n return out;\n}\n\nfunction lastSentForType(reports: ReportRow[], siteId: string, type: ReportType): string | null {\n const candidates = reports\n .filter((r) => r.siteId === siteId && r.reportType === type && r.sentAt !== null)\n .map((r) => r.sentAt!)\n .sort();\n return candidates[candidates.length - 1] ?? null;\n}\n\n/**\n * Computes which (site, type) pairs are due as of `today`.\n *\n * Algorithm per (site, type):\n * 1. If freq === \"None\", skip.\n * 2. baseDate = max(last Sent at for this type, site's `maintenance/testing day` fallback).\n * 3. If no baseDate exists at all, the site is due now.\n * 4. dueDate = baseDate + frequency months.\n * 5. Due iff startOfDay(today) >= startOfDay(dueDate).\n */\nexport function findDueReports(\n websites: WebsiteRow[],\n reports: ReportRow[],\n today: Date,\n): DueItem[] {\n const out: DueItem[] = [];\n const todayStart = startOfDay(today);\n\n for (const site of websites) {\n // Skip explicitly-non-active statuses (deprecated, \"probably not our problem\").\n // Null status is treated as active for backwards compat with rows that pre-date\n // the Status convention.\n if (site.status !== null && !ELIGIBLE_STATUSES.has(site.status)) continue;\n\n for (const type of [\"Maintenance\", \"Testing\"] as const) {\n const rawFreq = type === \"Maintenance\" ? site.maintenanceFreq : site.testingFreq;\n // Normalize obvious whitespace so a trailing-space typo (\"Quarterly \") still\n // schedules. The LOUD warning below is the real safety net for genuine\n // casing/spelling mistakes (\"monthly\", \"Quaterly\").\n const freq = (typeof rawFreq === \"string\" ? rawFreq.trim() : rawFreq) as Frequency;\n // Intentional silent skip — \"None\" (and the empty/blank default) means \"no\n // schedule\", not a mistake.\n if (freq === \"None\" || freq === (\"\" as Frequency)) continue;\n // A non-empty, non-None value that doesn't match a known schedule used to\n // silently produce no due date — the site just vanished from the loop. Warn\n // LOUDLY so a casing/typo Airtable value is fixable instead of invisible.\n if (!(freq in MONTHS)) {\n console.warn(\n `⚠ ${site.name}: unrecognized ${type === \"Maintenance\" ? \"maintenance\" : \"testing\"} frequency '${rawFreq}' — not scheduling; fix the Airtable value`,\n );\n continue;\n }\n\n const lastSent = lastSentForType(reports, site.id, type);\n const fallback = type === \"Maintenance\" ? site.maintenanceDay : site.testingDay;\n const baseIso = lastSent ?? fallback;\n\n if (!baseIso) {\n out.push({ site, reportType: type, dueDate: todayStart, lastSent });\n continue;\n }\n\n const dueDate = addMonths(new Date(baseIso), MONTHS[freq]);\n if (todayStart.getTime() >= startOfDay(dueDate).getTime()) {\n out.push({ site, reportType: type, dueDate, lastSent });\n }\n }\n }\n\n return out;\n}\n\n/**\n * The UTC `YYYY-MM` of a `dueDate` from {@link findDueReports} — the per-recurrence\n * idempotency key for drafting. Monthly recurrences land in distinct months; quarterly\n * and yearly land in distinct due-months too, so this uniquely names one draft per cycle.\n * UTC accessors keep it timezone-independent, consistent with the rest of this module.\n */\nexport function reportPeriodKey(dueDate: Date): string {\n if (Number.isNaN(dueDate.getTime())) throw new TypeError(\"reportPeriodKey: invalid Date\");\n const year = dueDate.getUTCFullYear();\n const month = String(dueDate.getUTCMonth() + 1).padStart(2, \"0\");\n return `${year}-${month}`;\n}\n","/** Render an absolute timestamp as a coarse \"Xd ago\" relative string for the\n * fleet card. Takes an explicit `now` for testability; defaults to wall clock\n * for callers (the Netlify function). Returns \"—\" for null / unparseable. */\nexport function relativeTimeFromNow(iso: string | null, now: Date = new Date()): string {\n if (!iso) return \"—\";\n const t = Date.parse(iso);\n if (Number.isNaN(t)) return \"—\";\n\n const seconds = Math.max(0, Math.floor((now.getTime() - t) / 1000));\n if (seconds < 60) return \"just now\";\n\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}d ago`;\n\n const weeks = Math.floor(days / 7);\n if (weeks < 4) return `${weeks}w ago`;\n\n const months = Math.floor(days / 30);\n return `${months}mo ago`;\n}\n","import type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport type { ReportRow } from \"../reports/airtable/reports.js\";\nimport { isPendingApproval } from \"../reports/airtable/reports.js\";\nimport type { SubmissionRow } from \"../reports/airtable/submissions.js\";\nimport { relativeTimeFromNow } from \"./relative-time.js\";\nimport { escapeHtml, safeUrl } from \"../util/html.js\";\n\nfunction scoreTile(label: string, value: number | null): string {\n const display = value === null ? \"—\" : String(value);\n return `<div class=\"tile\"><div class=\"tile-value\">${escapeHtml(display)}</div><div class=\"tile-label\">${escapeHtml(label)}</div></div>`;\n}\n\nfunction healthTile(label: string, value: number | null, sub: string | null): string {\n const display = value === null ? \"—\" : String(value);\n const subLine = sub ? `<div class=\"tile-sub\">${escapeHtml(sub)}</div>` : \"\";\n return `<div class=\"tile\"><div class=\"tile-value\">${escapeHtml(display)}</div><div class=\"tile-label\">${escapeHtml(label)}</div>${subLine}</div>`;\n}\n\nfunction depsSub(majorBehind: number | null): string | null {\n if (majorBehind === null || majorBehind === 0) return null;\n return `${majorBehind} major behind`;\n}\n\nfunction securityTotal(site: WebsiteRow): number | null {\n const parts = [\n site.securityVulnsCritical,\n site.securityVulnsHigh,\n site.securityVulnsModerate,\n site.securityVulnsLow,\n ];\n if (parts.every((p) => p === null)) return null;\n return parts.reduce<number>((sum, p) => sum + (p ?? 0), 0);\n}\n\nfunction securitySub(site: WebsiteRow): string | null {\n const total = securityTotal(site);\n if (total === null || total === 0) return null;\n const c = site.securityVulnsCritical ?? 0;\n const h = site.securityVulnsHigh ?? 0;\n const m = site.securityVulnsModerate ?? 0;\n const l = site.securityVulnsLow ?? 0;\n return `${c}C / ${h}H / ${m}M / ${l}L`;\n}\n\nfunction pendingRow(r: ReportRow): string {\n const type = escapeHtml(r.reportType);\n const period = r.period ? escapeHtml(r.period) : \"—\";\n return `<li><strong>${type}</strong> <span class=\"muted\">${period}</span> <button class=\"approve\" data-report-id=\"${escapeHtml(r.id)}\" data-approve-url=\"/api/reports/${encodeURIComponent(r.id)}/approve\">Approve</button></li>`;\n}\n\nfunction pendingSection(reports: ReportRow[]): string {\n const pending = reports.filter(isPendingApproval);\n if (pending.length === 0) return \"\";\n return `<div class=\"section pending\">\n <h2>Pending your yes (${pending.length})</h2>\n <ul class=\"pending-list\">${pending.map(pendingRow).join(\"\")}</ul>\n </div>`;\n}\n\nfunction reportRow(r: ReportRow): string {\n const date = r.completedOn ? escapeHtml(r.completedOn) : \"—\";\n const type = escapeHtml(r.reportType);\n const id = escapeHtml(r.reportId);\n const link = r.renderedHtmlAttachment\n ? `<a href=\"${escapeHtml(safeUrl(r.renderedHtmlAttachment.url))}\">view</a>`\n : `<span class=\"muted\">no attachment</span>`;\n const action = isPendingApproval(r)\n ? `<button class=\"approve\" data-report-id=\"${escapeHtml(r.id)}\" data-approve-url=\"/api/reports/${encodeURIComponent(r.id)}/approve\">Approve</button>`\n : \"\";\n return `<tr><td>${date}</td><td>${type}</td><td><code>${id}</code></td><td>${link}</td><td>${action}</td></tr>`;\n}\n\nfunction submissionRow(s: SubmissionRow): string {\n const when = s.submittedAt ? escapeHtml(relativeTimeFromNow(s.submittedAt)) : \"—\";\n const type = escapeHtml(s.formType);\n const who = escapeHtml(s.name || \"(no name)\");\n const email = escapeHtml(s.email || \"\");\n const message = escapeHtml(s.message ?? \"\");\n const status = escapeHtml(s.status);\n const id = escapeHtml(s.id);\n const url = `/api/submissions/${encodeURIComponent(s.id)}/status`;\n const btn = (label: string, action: string) =>\n `<button class=\"subm-status\" data-id=\"${id}\" data-status=\"${action}\" data-url=\"${url}\">${label}</button>`;\n return `<li class=\"subm-item\">\n <div class=\"subm-head\"><strong>${type}</strong> · ${who} <span class=\"muted\">${email}</span> <span class=\"pill subm-${status}\">${status}</span> <span class=\"muted\">${when}</span></div>\n ${message ? `<div class=\"subm-msg\">${message}</div>` : \"\"}\n <div class=\"subm-actions\">${btn(\"Read\", \"read\")}${btn(\"Archive\", \"archived\")}${btn(\"Spam\", \"spam\")}</div>\n </li>`;\n}\n\nfunction submissionsSection(submissions: SubmissionRow[]): string {\n if (submissions.length === 0) return \"\";\n const recent = [...submissions]\n .sort((a, b) => (b.submittedAt ?? \"\").localeCompare(a.submittedAt ?? \"\"))\n .slice(0, 25);\n return `<div class=\"section submissions\">\n <h2>Form submissions (${submissions.length})</h2>\n <ul class=\"subm-list\">${recent.map(submissionRow).join(\"\")}</ul>\n </div>`;\n}\n\nconst STYLES = `\n:root { color-scheme: light dark; }\nbody { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }\n@media (prefers-color-scheme: dark) { body { color: #e8e8e8; background: #111; } a { color: #6cb6ff; } }\nh1 { margin: 0 0 0.25rem; font-size: 1.75rem; }\n.meta { color: #666; margin-bottom: 2rem; }\n.meta a { color: inherit; }\n.audited { color: #999; font-size: 0.85rem; margin-bottom: 1.5rem; }\n.section { margin: 2rem 0; }\n.section h2 { font-size: 1.1rem; margin: 0 0 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #666; }\n.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.75rem; }\n.tile { padding: 1rem; border: 1px solid #ddd; border-radius: 6px; text-align: center; }\n@media (prefers-color-scheme: dark) { .tile { border-color: #333; } }\n.tile-value { font-size: 2rem; font-weight: 600; }\n.tile-label { font-size: 0.85rem; color: #666; margin-top: 0.25rem; }\n.tile-sub { font-size: 0.75rem; color: #999; margin-top: 0.15rem; }\ntable { width: 100%; border-collapse: collapse; }\nth, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid #eee; }\n@media (prefers-color-scheme: dark) { th, td { border-color: #2a2a2a; } }\n.muted { color: #999; }\n.empty { color: #999; padding: 1rem; border: 1px dashed #ccc; border-radius: 6px; text-align: center; }\nbutton.approve { font: inherit; padding: 0.35rem 0.85rem; border: 1px solid #2c7; border-radius: 6px; background: #2c7; color: #fff; cursor: pointer; }\nbutton.approve:disabled { opacity: 0.6; cursor: default; }\n.pending-list { list-style: none; padding: 0; margin: 0; }\n.pending-list li { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-bottom: 1px solid #eee; }\n.pill { font-size: 0.75rem; padding: 0.1rem 0.5rem; border-radius: 999px; font-weight: 700; }\n.subm-list { list-style: none; padding: 0; margin: 0; }\n.subm-item { padding: 0.6rem 0; border-bottom: 1px solid #eee; }\n@media (prefers-color-scheme: dark) { .subm-item { border-color: #2a2a2a; } }\n.subm-head { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }\n.subm-msg { margin: 0.35rem 0; white-space: pre-wrap; }\n.subm-actions { display: flex; gap: 0.4rem; }\nbutton.subm-status { font: inherit; padding: 0.25rem 0.7rem; border: 1px solid #888; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; }\nbutton.subm-status:disabled { opacity: 0.6; cursor: default; }\n.pill.subm-new { background: #e8f0fe; color: #1a56db; }\n.pill.subm-read { background: #f0f0f0; color: #555; }\n.pill.subm-archived { background: #eee; color: #888; }\n.pill.subm-spam { background: #fdecea; color: #b00; }\n`;\n\n/**\n * Render the per-site dashboard as a single HTML document. Pure function:\n * no Airtable access, no env reads, no I/O. The Netlify function handler\n * fetches data, then hands it here. Easier to unit-test, easier to render\n * a static preview from CLI later.\n */\nexport function renderSiteDashboardHtml(\n site: WebsiteRow,\n reports: ReportRow[],\n submissions: SubmissionRow[] = [],\n): string {\n const name = escapeHtml(site.name);\n const urlSafe = safeUrl(site.url);\n const allScoresNull =\n site.pScore === null && site.rScore === null && site.bpScore === null && site.seoScore === null;\n\n const scoresSection = allScoresNull\n ? `<div class=\"empty\">No lighthouse data yet — run <code>reddoor-maint audit --write-airtable</code> from the site checkout.</div>`\n : `<div class=\"tiles\">\n ${scoreTile(\"Performance\", site.pScore)}\n ${scoreTile(\"Accessibility\", site.rScore)}\n ${scoreTile(\"Best Practices\", site.bpScore)}\n ${scoreTile(\"SEO\", site.seoScore)}\n </div>`;\n\n const secTotal = securityTotal(site);\n const allHealthNull =\n site.a11yViolations === null && site.depsDrifted === null && secTotal === null;\n const healthSection = allHealthNull\n ? `<div class=\"empty\">No health data yet — run <code>reddoor-maint audit --write-airtable</code> from the site checkout.</div>`\n : `<div class=\"tiles\">\n ${healthTile(\"Accessibility issues\", site.a11yViolations, null)}\n ${healthTile(\"Dependency updates\", site.depsDrifted, depsSub(site.depsMajorBehind))}\n ${healthTile(\"Security alerts\", secTotal, securitySub(site))}\n </div>`;\n\n const auditedLine = site.lastLighthouseAuditAt\n ? `<div class=\"audited\">Last audited ${escapeHtml(relativeTimeFromNow(site.lastLighthouseAuditAt))}</div>`\n : \"\";\n\n // The report-history TABLE is the only place the \"recent 6\" slice belongs:\n // long enough to show a quarter of monthly reports plus the latest testing\n // report, short enough to keep the page a single scroll. The pending list +\n // approve buttons above intentionally see the FULL `reports` set — an OLD\n // pending report that falls outside this slice must still be approvable\n // (and must not disagree with the fleet banner, which counts ALL reports).\n const recentReports = [...reports]\n .sort((a, b) => (b.completedOn ?? \"\").localeCompare(a.completedOn ?? \"\"))\n .slice(0, 6);\n const reportsSection =\n recentReports.length === 0\n ? `<div class=\"empty\">No reports yet.</div>`\n : `<table>\n <thead><tr><th>Completed</th><th>Type</th><th>ID</th><th>Report</th><th></th></tr></thead>\n <tbody>${recentReports.map(reportRow).join(\"\")}</tbody>\n </table>`;\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${name} — Reddoor maintenance</title>\n <style>${STYLES}</style>\n</head>\n<body>\n <h1>${name}</h1>\n <div class=\"meta\"><a href=\"${escapeHtml(urlSafe)}\">${escapeHtml(site.url)}</a></div>\n ${auditedLine}\n ${pendingSection(reports)}\n ${submissionsSection(submissions)}\n\n <div class=\"section\">\n <h2>Lighthouse</h2>\n ${scoresSection}\n </div>\n\n <div class=\"section\">\n <h2>Site Health</h2>\n ${healthSection}\n </div>\n\n <div class=\"section\">\n <h2>Reports</h2>\n ${reportsSection}\n </div>\n <script>\n document.querySelectorAll(\"button.approve\").forEach((b) => {\n b.addEventListener(\"click\", async () => {\n b.disabled = true;\n try {\n const res = await fetch(b.dataset.approveUrl, { method: \"POST\" });\n b.textContent = res.ok ? \"Approved\" : \"Failed\";\n if (!res.ok) b.disabled = false;\n } catch {\n // Network rejection (offline, DNS, abort): mirror the !res.ok path so\n // the button doesn't sit permanently disabled reading \"Approve\".\n b.textContent = \"Failed\";\n b.disabled = false;\n }\n });\n });\n document.querySelectorAll(\"button.subm-status\").forEach((b) => {\n b.addEventListener(\"click\", async () => {\n b.disabled = true;\n try {\n const res = await fetch(b.dataset.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ status: b.dataset.status }),\n });\n b.textContent = res.ok ? \"✓\" : \"Failed\";\n if (!res.ok) b.disabled = false;\n } catch {\n b.textContent = \"Failed\";\n b.disabled = false;\n }\n });\n });\n </script>\n</body>\n</html>`;\n}\n","import type { WebsiteRow } from \"../reports/airtable/websites.js\";\n\nexport type OnboardingStatus = {\n score: number;\n total: 4;\n checks: {\n firstAudit: boolean;\n recipients: boolean;\n schedule: boolean;\n poc: boolean;\n };\n};\n\nfunction isNonEmpty(s: string | null | undefined): boolean {\n return typeof s === \"string\" && s.trim().length > 0;\n}\n\n/** Four-point onboarding signal for the fleet card. A site is \"fully onboarded\"\n * when it has been audited at least once, has a To-recipient for monthly\n * reports, has a maintenance schedule that isn't \"None\", and has a named POC. */\nexport function onboardingStatus(row: WebsiteRow): OnboardingStatus {\n const checks = {\n firstAudit: isNonEmpty(row.lastLighthouseAuditAt),\n recipients: isNonEmpty(row.reportRecipientsTo),\n schedule: row.maintenanceFreq !== \"None\",\n poc: isNonEmpty(row.pointOfContact),\n };\n const score = Object.values(checks).filter(Boolean).length;\n return { score, total: 4, checks };\n}\n","import type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { siteSlug } from \"../reports/airtable/websites.js\";\nimport type { CockpitModel, SiteCard, Tier, SubmissionEntry } from \"./fleet-cockpit.js\";\nimport { onboardingStatus } from \"./onboarding.js\";\nimport { relativeTimeFromNow } from \"./relative-time.js\";\nimport { escapeHtml, safeUrl } from \"../util/html.js\";\n\nconst DASH = \"—\";\n\nfunction scoreSpan(category: \"perf\" | \"a11y-lh\" | \"bp\" | \"seo\", value: number | null): string {\n const display = value === null ? DASH : String(value);\n return `<span class=\"score ${category}\">${escapeHtml(display)}</span>`;\n}\n\nfunction a11ySpan(value: number | null): string {\n const display = value === null ? DASH : String(value);\n return `<span class=\"metric a11y\">${escapeHtml(display)}</span>`;\n}\n\nfunction depsSpan(\n drifted: number | null,\n majorBehind: number | null,\n outdated: number | null,\n): string {\n if (drifted === null || majorBehind === null) {\n return `<span class=\"metric deps\">${DASH}</span>`;\n }\n // Declared-range drift vs baseline, plus the real outdated-install count when\n // it was determined (null = not checked this run → omit, don't imply clean).\n const driftPart = drifted === 0 ? \"0\" : `${drifted} drifted (${majorBehind} major)`;\n const display = outdated === null ? driftPart : `${driftPart} · ${outdated} outdated`;\n return `<span class=\"metric deps\">${escapeHtml(display)}</span>`;\n}\n\nfunction securitySpan(\n critical: number | null,\n high: number | null,\n moderate: number | null,\n low: number | null,\n): string {\n if (critical === null || high === null || moderate === null || low === null) {\n return `<span class=\"metric sec\">${DASH}</span>`;\n }\n const total = critical + high + moderate + low;\n const display = total === 0 ? \"0\" : `${critical}C/${high}H/${moderate}M/${low}L`;\n return `<span class=\"metric sec\">${escapeHtml(display)}</span>`;\n}\n\nfunction card(site: WebsiteRow): string {\n const name = escapeHtml(site.name);\n // The per-site dashboard at /s/<slug> is operator-only, gated by the shared\n // dashboard password (no per-site token). Cockpit visibility is Status-based;\n // the caller filters the fleet view.\n const href = `/s/${escapeHtml(siteSlug(site.name))}`;\n const onboarding = onboardingStatus(site);\n const audited = relativeTimeFromNow(site.lastLighthouseAuditAt);\n const safeSiteUrl = escapeHtml(safeUrl(site.url));\n const visibleUrl = escapeHtml(site.url);\n\n return `<article class=\"card\">\n <header class=\"card-head\">\n <a class=\"site\" href=\"${href}\">${name}</a>\n <a class=\"url\" href=\"${safeSiteUrl}\" target=\"_blank\" rel=\"noopener\">${visibleUrl}</a>\n <span class=\"setup\">Setup: <strong>${onboarding.score}/${onboarding.total}</strong></span>\n <span class=\"audited\">Audited: <strong>${escapeHtml(audited)}</strong></span>\n </header>\n <div class=\"card-metrics\">\n <span class=\"cluster lighthouse\">\n ${scoreSpan(\"perf\", site.pScore)}\n ${scoreSpan(\"a11y-lh\", site.rScore)}\n ${scoreSpan(\"bp\", site.bpScore)}\n ${scoreSpan(\"seo\", site.seoScore)}\n </span>\n <span class=\"cluster health\">\n <span class=\"metric-label\">a11y</span> ${a11ySpan(site.a11yViolations)}\n <span class=\"metric-label\">deps</span> ${depsSpan(site.depsDrifted, site.depsMajorBehind, site.depsOutdated)}\n <span class=\"metric-label\">sec</span> ${securitySpan(\n site.securityVulnsCritical,\n site.securityVulnsHigh,\n site.securityVulnsModerate,\n site.securityVulnsLow,\n )}\n </span>\n </div>\n </article>`;\n}\n\nconst STYLES = `\n:root { color-scheme: light dark; }\nbody { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 1100px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }\n@media (prefers-color-scheme: dark) { body { color: #e8e8e8; background: #111; } a { color: #6cb6ff; } }\nh1 { margin: 0 0 0.25rem; font-size: 1.75rem; }\n.meta { color: #666; margin-bottom: 1.5rem; }\n.empty { color: #999; padding: 2rem; text-align: center; border: 1px dashed #ccc; border-radius: 6px; }\n.cards { display: flex; flex-direction: column; gap: 0.75rem; }\n.card { border: 1px solid #e5e5e5; border-radius: 8px; padding: 0.9rem 1.1rem; }\n@media (prefers-color-scheme: dark) { .card { border-color: #2a2a2a; background: #181818; } }\n.card-head { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; align-items: baseline; }\n.card-head .site { font-weight: 600; font-size: 1.05rem; }\n.card-head .url { color: #666; font-size: 0.85rem; }\n.card-head .setup, .card-head .audited { color: #666; font-size: 0.85rem; }\n.card-head .setup { margin-left: auto; }\n.card-metrics { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; margin-top: 0.5rem; font-variant-numeric: tabular-nums; }\n.cluster { display: inline-flex; gap: 0.5rem; align-items: baseline; }\n.cluster.lighthouse .score { display: inline-block; min-width: 2.25rem; text-align: right; }\n.metric-label { color: #999; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }\n.metric { font-feature-settings: \"tnum\"; }\n.summary { display:flex; flex-wrap:wrap; gap:0.5rem 1.25rem; align-items:baseline; margin-bottom:0.5rem; }\n.summary .tier { font-weight:700; }\n.summary .heads { color:#666; font-size:0.9rem; }\n.filters { display:flex; flex-wrap:wrap; gap:0.4rem; margin-bottom:1.25rem; }\n.filters button { font:inherit; font-size:0.85rem; padding:0.25rem 0.7rem; border:1px solid #ccc; border-radius:999px; background:transparent; color:inherit; cursor:pointer; }\n.filters button[aria-pressed=\"true\"] { background:#1a1a1a; color:#fff; border-color:#1a1a1a; }\n@media (prefers-color-scheme: dark) { .filters button[aria-pressed=\"true\"] { background:#e8e8e8; color:#111; } }\ndetails.tier { margin:0.75rem 0; }\ndetails.tier > summary { cursor:pointer; font-weight:700; font-size:1.05rem; padding:0.35rem 0; list-style:none; }\n.approve-strip { border:1px solid #ffe08a; background:#fff8e1; border-radius:8px; padding:0.75rem 1rem; margin-bottom:1.25rem; }\n@media (prefers-color-scheme: dark) { .approve-strip { background:#241f00; border-color:#5a4d00; } }\n.approve-strip h2 { font-size:1rem; margin:0 0 0.5rem; }\n.approve-row { display:flex; flex-wrap:wrap; gap:0.5rem 1rem; align-items:center; padding:0.25rem 0; }\n.pill { font-size:0.75rem; padding:0.1rem 0.5rem; border-radius:999px; font-weight:700; }\n.pill.attention { background:#fdecea; color:#b00; }\n.pill.watch { background:#fff4e5; color:#a65a00; }\n.pill.healthy { background:#e8f5e9; color:#1b7a2f; }\n.chips { display:flex; flex-wrap:wrap; gap:0.4rem; margin-top:0.5rem; }\n.chip { font-size:0.8rem; padding:0.1rem 0.5rem; border-radius:6px; background:#f0f0f0; }\n@media (prefers-color-scheme: dark) { .chip { background:#222; } }\n.chip.critical { background:#fdecea; color:#b00; }\n.badge { font-weight:700; color:#C00; font-size:0.72rem; margin-right:0.25rem; }\n.all-clear { background:#e8f5e9; color:#1b7a2f; padding:0.6rem 1rem; border-radius:8px; margin-bottom:1.25rem; font-weight:600; }\n@media (prefers-color-scheme: dark) { .all-clear { background:#10240f; color:#7fce85; } }\n`;\n\nconst TIER_META: Record<Tier, { emoji: string; label: string; open: boolean }> = {\n attention: { emoji: \"🔴\", label: \"Needs attention\", open: true },\n watch: { emoji: \"🟡\", label: \"Watch\", open: false },\n healthy: { emoji: \"🟢\", label: \"Healthy\", open: false },\n};\n\nconst FILTERS = [\n \"all\",\n \"vulns\",\n \"lighthouse\",\n \"delivery\",\n \"prs\",\n \"ci\",\n \"stale\",\n \"pending\",\n \"submissions\",\n] as const;\n\nfunction summaryBar(model: CockpitModel): string {\n const s = model.summary;\n const heads = [\n `${s.criticalHighVulns} critical/high vuln${s.criticalHighVulns === 1 ? \"\" : \"s\"}`,\n `${s.lighthouseBelowFloor} Lighthouse<75`,\n `${s.deliveryFailures} delivery`,\n `${s.renovateFailing} PRs failing`,\n `${s.ciRed} CI red`,\n `${s.pending} pending`,\n `${s.newSubmissions ?? 0} new`,\n ].join(\" · \");\n const chips = FILTERS.map(\n (f) =>\n `<button type=\"button\" data-filter=\"${f}\" aria-pressed=\"${f === \"all\" ? \"true\" : \"false\"}\">${f}</button>`,\n ).join(\"\");\n return `<div class=\"summary\">\n <span class=\"tier\">🔴 ${s.attention} needs attention</span>\n <span class=\"tier\">🟡 ${s.watch} watch</span>\n <span class=\"tier\">🟢 ${s.healthy} healthy</span>\n </div>\n <div class=\"summary heads\">${escapeHtml(heads)}</div>\n <div class=\"filters\">${chips}</div>`;\n}\n\n/** Affirmative all-clear when nothing is on the 🔴 tier (spec §5.2/§12) — so a\n * healthy or empty fleet reads as \"all clear\", not three bare \"None.\" rows. */\nfunction allClearBanner(model: CockpitModel): string {\n if (model.summary.attention > 0) return \"\";\n const msg =\n model.cards.length === 0\n ? \"No sites on the fleet view yet.\"\n : \"All clear — nothing needs your attention.\";\n return `<div class=\"all-clear\">✓ ${escapeHtml(msg)}</div>`;\n}\n\nfunction approveStrip(model: CockpitModel): string {\n if (model.pending.length === 0) return \"\";\n const rows = model.pending\n .map((p) => {\n const href = `/s/${escapeHtml(p.slug)}`;\n const url = `/api/reports/${encodeURIComponent(p.reportId)}/approve`;\n return `<div class=\"approve-row\" data-signal=\"pending\">\n <strong>${escapeHtml(p.siteName)}</strong>\n <span class=\"muted\">${escapeHtml(p.reportType)} ${escapeHtml(p.period)}</span>\n <button class=\"approve\" data-report-id=\"${escapeHtml(p.reportId)}\" data-approve-url=\"${url}\">Approve</button>\n <a href=\"${href}\">open ▸</a>\n </div>`;\n })\n .join(\"\");\n return `<section class=\"approve-strip\" data-tier=\"pending\">\n <h2>Approve (${model.pending.length}) — your daily yes</h2>\n ${rows}\n </section>`;\n}\n\nfunction submissionsStrip(model: CockpitModel): string {\n const subs: SubmissionEntry[] = model.submissions ?? [];\n if (subs.length === 0) return \"\";\n const rows = subs\n .map((sub) => {\n const href = `/s/${escapeHtml(sub.slug)}`;\n const when = sub.submittedAt ? escapeHtml(relativeTimeFromNow(sub.submittedAt)) : \"\";\n const who = escapeHtml(sub.name || sub.email);\n return `<div class=\"approve-row\" data-signal=\"submissions\">\n <strong>${escapeHtml(sub.siteName)}</strong>\n <span class=\"muted\">${escapeHtml(sub.formType)} — ${who}</span>\n <span class=\"muted\">${when}</span>\n <a href=\"${href}\">open ▸</a>\n </div>`;\n })\n .join(\"\");\n return `<section class=\"approve-strip subm-strip\" data-tier=\"submissions\">\n <h2>📥 New submissions (${subs.length})</h2>\n ${rows}\n </section>`;\n}\n\nfunction submBadge(c: SiteCard): string {\n const n = c.newSubmissions ?? 0;\n return n > 0 ? `<span class=\"chip\">📥 ${n} new</span>` : \"\";\n}\n\nconst PILL_LABEL: Record<Tier, string> = { attention: \"failing\", watch: \"watch\", healthy: \"ok\" };\n\nfunction attentionBadge(status?: string): string {\n if (status === \"new\") return `<span class=\"badge\">NEW</span>`;\n if (status === \"worse\") return `<span class=\"badge\">WORSE</span>`;\n return \"\";\n}\n\nfunction chips(c: SiteCard): string {\n const items = c.items.map((it) => {\n const cls = it.severity === \"critical\" ? \"chip critical\" : \"chip\";\n return `<span class=\"${cls}\">${attentionBadge(it.status)}${escapeHtml(it.title)}</span>`;\n });\n for (const reason of c.watchReasons)\n items.push(`<span class=\"chip\">${escapeHtml(reason)}</span>`);\n return items.length ? `<div class=\"chips\">${items.join(\"\")}</div>` : \"\";\n}\n\n/** Space-separated signal tags for the client filter. Attention-item kinds\n * (\"vulns\"/\"lighthouse\"/\"delivery\"/\"prs\" from renovate/\"ci\") plus the structured\n * watch signals (\"lighthouse\" for a sub-floor-band score, \"stale\" for an old\n * commit) — so a watch-band Lighthouse card still matches the \"lighthouse\" filter. */\nfunction signalsAttr(c: SiteCard): string {\n const kinds = new Set<string>();\n for (const it of c.items) {\n kinds.add(it.kind === \"vuln\" ? \"vulns\" : it.kind === \"renovate\" ? \"prs\" : it.kind);\n }\n for (const sig of c.watchSignals) kinds.add(sig);\n return [...kinds].join(\" \");\n}\n\nfunction cockpitCard(c: SiteCard): string {\n const base = card(c.site); // existing header + metrics markup\n const pill = `<span class=\"pill ${c.tier}\">${PILL_LABEL[c.tier]}</span>`;\n const extra = `${pill}${chips(c)}${submBadge(c)}`;\n const opening = `<article class=\"card\" data-signals=\"${signalsAttr(c)}\">`;\n // Inject the pill + chips before the article's closing tag, and add the filter\n // hook. Function replacers so a `$` in escaped chip text can't be read as a\n // String.replace special ($&, $1, …).\n return base\n .replace('<article class=\"card\">', () => opening)\n .replace(\"</article>\", () => `${extra}</article>`);\n}\n\nconst FILTER_SCRIPT = `<script>\n(function(){\n var btns = document.querySelectorAll('.filters button');\n var cards = document.querySelectorAll('.cards .card');\n var details = document.querySelectorAll('details.tier');\n btns.forEach(function(b){\n b.addEventListener('click', function(){\n var f = b.getAttribute('data-filter');\n btns.forEach(function(x){ x.setAttribute('aria-pressed', x===b ? 'true':'false'); });\n // \"pending\" lives on the approve strip, not on tier cards — just jump to it,\n // never hide the triage cards (else the whole board blanks).\n if (f === 'pending') { var s = document.querySelector('.approve-strip'); if (s) s.scrollIntoView({behavior:'smooth'}); return; }\n if (f === 'submissions') { var ss = document.querySelector('[data-tier=\"submissions\"]'); if (ss) ss.scrollIntoView({behavior:'smooth'}); return; }\n if (f !== 'all') details.forEach(function(d){ d.open = true; });\n cards.forEach(function(c){\n var sig = (c.getAttribute('data-signals')||'').split(' ');\n c.style.display = (f==='all' || sig.indexOf(f)!==-1) ? '' : 'none';\n });\n });\n });\n // approve buttons: mirror the per-site dashboard's inline POST.\n document.querySelectorAll('button.approve').forEach(function(b){\n b.addEventListener('click', async function(){\n b.disabled = true; b.textContent = 'Approving…';\n try { var res = await fetch(b.dataset.approveUrl, { method: 'POST' });\n b.textContent = res.ok ? 'Approved ✓' : 'Failed'; }\n catch(e){ b.textContent = 'Failed'; b.disabled = false; }\n });\n });\n})();\n</script>`;\n\n/**\n * Render the fleet cockpit as a single HTML document. Pure function: no Airtable\n * access, no env reads, no I/O. The Netlify function handler builds the\n * CockpitModel (visible-site filter, tiering, NEW/WORSE badging, pending list)\n * and hands it here. Renders the doc shell + summary bar + filter chips + pinned\n * approve strip + three <details> tier sections of cards.\n */\nexport function renderCockpitHtml(model: CockpitModel): string {\n const total = model.cards.length;\n const tiers: Tier[] = [\"attention\", \"watch\", \"healthy\"];\n const sections = tiers\n .map((tier) => {\n const cards = model.cards.filter((c) => c.tier === tier);\n const meta = TIER_META[tier];\n const body =\n cards.length === 0\n ? `<div class=\"empty\">None.</div>`\n : `<div class=\"cards\">${cards.map(cockpitCard).join(\"\")}</div>`;\n return `<details class=\"tier\" data-tier=\"${tier}\"${meta.open ? \" open\" : \"\"}>\n <summary>${meta.emoji} ${meta.label} (${cards.length})</summary>\n ${body}\n </details>`;\n })\n .join(\"\");\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Reddoor maintenance — fleet cockpit</title>\n <style>${STYLES}</style>\n</head>\n<body>\n <h1>Reddoor fleet cockpit</h1>\n <div class=\"meta\">${total} site${total === 1 ? \"\" : \"s\"} on the Reddoor stack.</div>\n ${summaryBar(model)}\n ${allClearBanner(model)}\n ${approveStrip(model)}\n ${submissionsStrip(model)}\n ${sections}\n ${FILTER_SCRIPT}\n</body>\n</html>`;\n}\n","import { timingSafeEqual } from \"node:crypto\";\n\n/**\n * Verify an `Authorization: Basic <base64>` header against the configured\n * dashboard password. Username is intentionally ignored — operators may\n * type anything when the browser prompts; only the password gates entry.\n *\n * Returns false for any of:\n * - missing/empty Authorization header\n * - non-Basic auth scheme\n * - malformed base64 or payload (no colon to split user:password)\n * - wrong password\n * - expected password missing (DASHBOARD_PASSWORD not configured)\n *\n * Wrong-password compare is constant-time; BYTE lengths are checked first\n * (timingSafeEqual throws a RangeError on a buffer-length mismatch, and the\n * length itself doesn't leak — operator's password length is fixed per deploy).\n * Comparing JS-string lengths instead of byte lengths could let an equal-char\n * but unequal-byte password (a multibyte char) reach timingSafeEqual and throw,\n * turning a wrong password into an uncaught 500.\n */\nexport function verifyBasicAuth(\n authHeader: string | null | undefined,\n expectedPassword: string | null,\n): boolean {\n if (!authHeader || !expectedPassword) return false;\n // RFC 7235: scheme is case-insensitive.\n const match = /^basic\\s+(.+)$/i.exec(authHeader.trim());\n if (!match) return false;\n let decoded: string;\n try {\n decoded = Buffer.from(match[1]!, \"base64\").toString(\"utf-8\");\n } catch {\n return false;\n }\n // Base64-decoding never throws in Node, but a payload of garbage may\n // produce a string with no colon. user:password form is required.\n const colonIdx = decoded.indexOf(\":\");\n if (colonIdx === -1) return false;\n const provided = decoded.slice(colonIdx + 1);\n // Compare BYTE lengths, not JS-string lengths: timingSafeEqual compares the\n // underlying buffers and throws a RangeError if they differ in byte length.\n // Two strings can share a JS length but differ in UTF-8 byte length (e.g. a\n // multibyte char), so a JS-length guard would let mismatched buffers through\n // and crash the handler with a 500.\n const a = Buffer.from(provided, \"utf-8\");\n const b = Buffer.from(expectedPassword, \"utf-8\");\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n"],"mappings":"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","names":["join","spawn","join","readFile","join","join","eslint","readFile","spawn","readFile","join","readFile","join","readFile","join","spawn","readFile","writeFile","mkdtemp","rm","join","readJsonMaybe","readFile","spawn","mkdtemp","join","writeFile","rm","spawn","readFile","writeFile","join","readFile","join","writeFile","commit","stat","join","exists","stat","spawn","join","commit","join","readFile","writeFile","join","join","readFile","writeFile","join","spawn","join","spawn","join","readFile","writeFile","join","glob","SCRIPT_BLOCK","SCRIPT_BLOCK","SCRIPT_BLOCK","SCRIPT_BLOCK","IGNORE","glob","join","readFile","writeFile","spawn","writeFile","join","join","spawn","commit","writeFile","join","commit","writeFile","join","rm","stat","join","exists","stat","spawn","join","commit","rm","stat","join","existsSync","dirname","join","exists","stat","spawn","join","commit","mkdir","writeFile","dirname","join","join","commit","mkdir","dirname","writeFile","readFile","readFile","mkdir","writeFile","dirname","readFile","existsSync","dirname","join","fileURLToPath","mapRow","mapRow","mapRow","dirname","join","readFileSync","join","join","dirname","readFileSync","ymd","readFileSync","JWT","ymd","mkdir","dirname","writeFile","MONTHS","STYLES","chips"]}
|
|
1
|
+
{"version":3,"sources":["../src/audits/util/spawn.ts","../src/audits/deps.ts","../src/util/site.ts","../src/configs/baseline-versions.ts","../src/audits/deps-outdated.ts","../src/audits/lint.ts","../src/audits/security.ts","../src/audits/lighthouse.ts","../src/configs/lighthouse.ts","../src/audits/util/site-config.ts","../src/util/free-port.ts","../src/audits/a11y.ts","../src/configs/playwright-a11y.ts","../src/audits/index.ts","../src/recipes/sync-configs.ts","../src/recipes/sync-configs/templates.ts","../src/recipes/sync-configs/gitignore.ts","../src/util/git.ts","../src/recipes/_with-recipe.ts","../src/recipes/bump-deps.ts","../src/recipes/svelte-5/index.ts","../src/util/pkg.ts","../src/recipes/svelte-5/step-bump-versions.ts","../src/recipes/svelte-5/step-svelte-config.ts","../src/recipes/svelte-5/step-svelte-migrate.ts","../src/recipes/svelte-5/step-tailwind-upgrade.ts","../src/recipes/svelte-5/step-gotchas.ts","../src/recipes/svelte-5/codemods/on-event-to-handler.ts","../src/recipes/svelte-5/codemods/dollar-props.ts","../src/util/svelte-source.ts","../src/recipes/svelte-5/codemods/dollar-restprops.ts","../src/recipes/svelte-5/codemods/state-effect-sync.ts","../src/recipes/svelte-5/codemods/dollar-props-class.ts","../src/recipes/svelte-5/codemods/legacy-reactive.ts","../src/recipes/svelte-5/step-verify.ts","../src/recipes/svelte-5/step-summary.ts","../src/recipes/svelte-codemods.ts","../src/recipes/convert-to-pnpm.ts","../src/recipes/convert-to-pnpm/script-rewrites.ts","../src/recipes/onboard.ts","../src/util/self-version.ts","../src/recipes/a11y-fixtures-page/index.ts","../src/recipes/a11y-fixtures-page/template.ts","../src/recipes/init.ts","../src/recipes/index.ts","../src/inventory/local.ts","../src/inventory/json.ts","../src/util/url.ts","../src/reports/airtable/websites.ts","../src/inventory/airtable.ts","../src/reports/draft.ts","../src/reports/render.ts","../src/reports/copy.ts","../src/reports/maintenance-email/assets/index.ts","../src/util/html.ts","../src/reports/maintenance-email/template.ts","../src/reports/launch-email/template.ts","../src/reports/airtable/reports.ts","../src/reports/airtable/attachments.ts","../src/reports/ga/config.ts","../src/util/credentials.ts","../src/reports/ga/client.ts","../src/reports/search/client.ts","../src/reports/airtable/client.ts","../src/reports/maintenance-email/header-image.ts","../src/reports/send/resend.ts","../src/reports/send/idempotency.ts","../src/reports/send/orchestrate.ts","../src/reports/due.ts","../src/dashboard/relative-time.ts","../src/dashboard/favicon.ts","../src/dashboard/render.ts","../src/dashboard/onboarding.ts","../src/dashboard/fleet-render.ts","../src/dashboard/basic-auth.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { StringDecoder } from \"node:string_decoder\";\n\nexport type SpawnResult = { code: number; stdout: string; stderr: string };\n\nexport type SpawnOptions = {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n timeoutMs?: number;\n /** When true, the child inherits stdout/stderr so the user sees live\n * progress (useful for long-running `pnpm up` / `npm install`). The\n * returned `stdout` and `stderr` will be empty strings in that case. */\n streaming?: boolean;\n};\n\nexport type SpawnFn = (\n cmd: string,\n args: readonly string[],\n opts?: SpawnOptions,\n) => Promise<SpawnResult>;\n\ntype KillFn = (pid: number, signal: NodeJS.Signals | number) => void;\n\n/** Construction-time knobs, separated from per-call {@link SpawnOptions} mainly\n * so tests can inject deterministic `spawnImpl`/`killImpl` and a tiny grace. */\nexport type SpawnInternals = {\n spawnImpl?: typeof spawn;\n killImpl?: KillFn;\n /** Delay after SIGTERM before escalating to SIGKILL on a timeout (default 5s). */\n killGraceMs?: number;\n /** Cap on captured stdout/stderr length so a runaway child can't OOM the CLI. */\n maxOutputBytes?: number;\n};\n\nconst TRUNCATION_MARKER = \"\\n…[output truncated]\";\n\nexport function makeSpawn(internals: SpawnInternals = {}): SpawnFn {\n const spawnImpl = internals.spawnImpl ?? spawn;\n const killImpl: KillFn = internals.killImpl ?? ((pid, sig) => process.kill(pid, sig));\n const killGraceMs = internals.killGraceMs ?? 5000;\n const maxOutputBytes = internals.maxOutputBytes ?? 10 * 1024 * 1024;\n\n return (cmd, args, opts = {}) =>\n new Promise((resolve, reject) => {\n const streaming = opts.streaming === true;\n const child = spawnImpl(cmd, [...args], {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n stdio: streaming ? [\"ignore\", \"inherit\", \"inherit\"] : [\"ignore\", \"pipe\", \"pipe\"],\n // Detach ONLY when a timeout can fire: the child then leads its own\n // process group, so the timeout can kill the WHOLE tree (vite, and\n // Chromium under lhci/playwright) via process.kill(-pid), not just the\n // npx/pnpm wrapper. Without it, killing the wrapper orphaned the\n // grandchildren — a zombie vite squatting its port, Chrome left running.\n // We do NOT detach timeout-less streaming calls (pnpm install/up):\n // detaching gains nothing there (no timeout → no group-kill) and would\n // break terminal Ctrl-C, which only reaches the foreground group — i.e.\n // it would re-orphan the very children this guards. We never unref() the\n // child since we still await it.\n detached: opts.timeoutMs !== undefined,\n });\n\n // Cap appended output so an unbounded stream can't exhaust memory.\n const cap = (acc: string, chunk: string): string => {\n if (acc.length >= maxOutputBytes) return acc;\n const next = acc + chunk;\n return next.length > maxOutputBytes\n ? next.slice(0, maxOutputBytes) + TRUNCATION_MARKER\n : next;\n };\n\n let stdout = \"\";\n let stderr = \"\";\n // Decode each stream through a StringDecoder so a multibyte UTF-8 char\n // split across two `data` chunks isn't corrupted: the decoder holds the\n // partial trailing bytes until the rest arrives, instead of the old\n // `String(chunk)` which decoded each chunk in isolation (and replaced the\n // split char with U+FFFD). Flushed via `.end()` on close.\n const outDecoder = new StringDecoder(\"utf-8\");\n const errDecoder = new StringDecoder(\"utf-8\");\n if (!streaming) {\n child.stdout?.on(\n \"data\",\n (chunk: Buffer) => (stdout = cap(stdout, outDecoder.write(chunk))),\n );\n child.stderr?.on(\n \"data\",\n (chunk: Buffer) => (stderr = cap(stderr, errDecoder.write(chunk))),\n );\n }\n\n /** Signal the child's whole process group; ignore if it's already gone.\n * POSIX-only: a negative pid signals the group (the project targets\n * macOS/Linux; this is only reached when detached, i.e. on a timeout). */\n const killGroup = (sig: NodeJS.Signals): void => {\n if (child.pid === undefined) return;\n try {\n killImpl(-child.pid, sig);\n } catch {\n // ESRCH: the group already exited between the timeout and the kill.\n }\n };\n\n let killTimer: ReturnType<typeof setTimeout> | undefined;\n const timer = opts.timeoutMs\n ? setTimeout(() => {\n killGroup(\"SIGTERM\");\n // Escalate if SIGTERM is ignored (a wedged Chrome can swallow it).\n killTimer = setTimeout(() => killGroup(\"SIGKILL\"), killGraceMs);\n // Best-effort cleanup AFTER we've already rejected — it must never\n // hold the CLI open past its real work.\n killTimer.unref();\n reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));\n }, opts.timeoutMs)\n : undefined;\n\n const clearTimers = (): void => {\n if (timer) clearTimeout(timer);\n if (killTimer) clearTimeout(killTimer);\n };\n\n child.on(\"error\", (err) => {\n clearTimers();\n reject(err);\n });\n child.on(\"close\", (code) => {\n clearTimers();\n if (!streaming) {\n // Flush any bytes the decoder buffered mid-character (e.g. a truncated\n // final UTF-8 sequence). `.end()` returns \"\" when nothing is pending.\n stdout = cap(stdout, outDecoder.end());\n stderr = cap(stderr, errDecoder.end());\n }\n resolve({ code: code ?? -1, stdout, stderr });\n });\n });\n}\n\nexport const defaultSpawn: SpawnFn = makeSpawn();\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { baselineVersions } from \"../configs/baseline-versions.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport { scanOutdated, type OutdatedCounts } from \"./deps-outdated.js\";\n\nexport type Drift = \"same\" | \"patch\" | \"minor\" | \"major\" | \"newer\";\n\nexport type DepsDriftEntry = {\n pkg: string;\n baseline: string;\n actual: string;\n drift: Drift;\n};\n\n/** The deps audit reports TWO signals:\n * - `entries`: declared-range drift vs the canonical baseline (what the\n * package.json *asks for*, caret-stripped) — the long-standing signal.\n * - `outdated`: real installed-version drift vs the registry's latest, from\n * the committed lockfile (null when it can't be determined). Added so the\n * \"Deps Drifted\" dashboard number stops being the only — and misleading —\n * deps signal. */\nexport type DepsDetails = {\n entries: DepsDriftEntry[];\n outdated: OutdatedCounts | null;\n};\n\nfunction stripCaret(range: string): string {\n return range.replace(/^[\\^~]/, \"\");\n}\n\n/** A spec we can drift-compare against a semver baseline: a plain version or\n * caret/tilde range like \"5.55.10\", \"^5.55.10\", \"~5.0.0\". Excludes \"*\",\n * \"latest\", \"workspace:*\", \"npm:\"-aliases, and git/URL/file specs — those used\n * to parse to NaN and produce bogus drift, so they're skipped instead. */\nfunction isComparableRange(spec: string): boolean {\n return /^[\\^~]?\\d/.test(spec.trim());\n}\n\nfunction parseSemver(v: string): [number, number, number] {\n const cleaned = stripCaret(v).split(\"-\")[0] ?? \"0.0.0\";\n const parts = cleaned.split(\".\").map((n) => Number.parseInt(n, 10));\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n}\n\nfunction compareSemver(actual: string, baseline: string): Drift {\n const [aMajor, aMinor, aPatch] = parseSemver(actual);\n const [bMajor, bMinor, bPatch] = parseSemver(baseline);\n if (aMajor > bMajor) return \"newer\";\n if (aMajor < bMajor) return \"major\";\n if (aMinor > bMinor) return \"newer\";\n if (aMinor < bMinor) return \"minor\";\n if (aPatch > bPatch) return \"newer\";\n if (aPatch < bPatch) return \"patch\";\n return \"same\";\n}\n\nexport async function depsAudit(ctx: AuditContext): Promise<AuditResult> {\n const pkgPath = join(ctx.site.path, \"package.json\");\n let pkgRaw: string;\n try {\n pkgRaw = await readFile(pkgPath, \"utf-8\");\n } catch (err) {\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status: \"skip\",\n summary: `no package.json at ${pkgPath}`,\n details: { error: String(err) },\n };\n }\n\n let pkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };\n try {\n pkg = JSON.parse(pkgRaw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n } catch (err) {\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status: \"fail\",\n summary: `package.json is not valid JSON: ${(err as Error).message}`,\n details: { error: String(err) },\n };\n }\n const installed: Record<string, string> = {\n ...(pkg.dependencies ?? {}),\n ...(pkg.devDependencies ?? {}),\n };\n\n const entries: DepsDriftEntry[] = [];\n for (const [name, baseline] of Object.entries(baselineVersions)) {\n const actual = installed[name];\n if (!actual) continue;\n // Skip non-semver specs (\"*\", \"workspace:*\", \"npm:\"-aliases, git/URL): they\n // can't be drift-compared and used to yield NaN-driven bogus drift (LOW-3).\n if (!isComparableRange(actual)) continue;\n entries.push({\n pkg: name,\n baseline,\n actual,\n drift: compareSemver(actual, baseline),\n });\n }\n\n const anyMajor = entries.some((d) => d.drift === \"major\");\n const anyMinor = entries.some((d) => d.drift === \"minor\");\n const anyNewer = entries.some((d) => d.drift === \"newer\");\n\n // Status stays driven by the declared-range baseline drift (unchanged\n // behavior). The outdated count is an independent, informational signal.\n const status: AuditResult[\"status\"] = anyMajor ? \"fail\" : anyMinor || anyNewer ? \"warn\" : \"pass\";\n\n const driftSummary =\n status === \"pass\"\n ? `all ${entries.length} tracked deps in line with baseline`\n : status === \"warn\"\n ? `${entries.filter((d) => d.drift !== \"same\").length} of ${entries.length} tracked deps drifted`\n : `${entries.filter((d) => d.drift === \"major\").length} deps lagging by a major version`;\n\n const outdated = await scanOutdated(ctx.site.path, ctx.spawn ?? defaultSpawn);\n const summary = outdated\n ? `${driftSummary}; ${outdated.outdated} outdated install(s) (${outdated.major} major)`\n : driftSummary;\n\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status,\n summary,\n details: { entries, outdated } satisfies DepsDetails,\n };\n}\n","import type { Site } from \"../types.js\";\n\n/** Human-friendly label for log/output formatting. Prefer the inventory's\n * `name` when present (e.g. \"caltex-landing\") and fall back to the\n * filesystem `path` when unnamed. Every audit + recipe uses this.\n *\n * Uses `||` (not `??`) deliberately: an Airtable Name that slugs to the EMPTY\n * string (`siteSlug(\"!!!\")` → \"\") is `\"\"`, not null/undefined, so `??` would let\n * it through and render a blank label. `||` falls back to the path. */\nexport function siteLabel(site: Site): string {\n return site.name || site.path;\n}\n","// Curated map of the framework deps reddoor sites should stay close to.\n// Refreshed at each package release from reddoor-starter's package.json.\n// Versions are caret ranges to mirror what `pnpm add` would produce.\n\nexport const baselineVersions: Record<string, string> = {\n // SvelteKit core\n svelte: \"^5.55.10\",\n \"@sveltejs/kit\": \"^2.61.1\",\n \"@sveltejs/adapter-netlify\": \"^6.0.4\",\n \"@sveltejs/adapter-auto\": \"^7.0.1\",\n \"@sveltejs/vite-plugin-svelte\": \"^7.1.2\",\n \"svelte-check\": \"^4.4.8\",\n\n // Build tooling\n vite: \"^8.0.14\",\n vitest: \"^4.1.7\",\n typescript: \"^6.0.3\",\n\n // Tailwind 4\n tailwindcss: \"^4.3.0\",\n \"@tailwindcss/vite\": \"^4.3.0\",\n\n // Prismic\n \"@prismicio/client\": \"^7.21.8\",\n \"@prismicio/svelte\": \"^2.2.1\",\n \"@slicemachine/adapter-sveltekit\": \"^0.3.96\",\n \"slice-machine-ui\": \"^2.21.3\",\n\n // Test tooling\n \"@playwright/test\": \"^1.60.0\",\n \"@axe-core/playwright\": \"^4.11.3\",\n \"@lhci/cli\": \"^0.15.1\",\n\n // Lint\n eslint: \"^10.4.0\",\n \"eslint-plugin-svelte\": \"^3.18.0\",\n \"eslint-config-prettier\": \"^10.1.8\",\n prettier: \"^3.8.3\",\n \"prettier-plugin-svelte\": \"^4.0.1\",\n \"typescript-eslint\": \"^8.60.0\",\n \"@eslint/js\": \"^10.0.1\",\n globals: \"^17.6.0\",\n\n // Misc\n \"@lucide/svelte\": \"^1.17.0\",\n \"@zerodevx/svelte-img\": \"^2.1.2\",\n};\n\nexport default baselineVersions;\n","import { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { SpawnFn } from \"./util/spawn.js\";\n\n/** Real installed-version drift, distinct from the declared-range \"drift\" the\n * deps audit computes against the baseline: how many dependencies are behind\n * the registry's latest, per the committed lockfile. */\nexport type OutdatedCounts = { outdated: number; major: number };\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction majorOf(version: string): number {\n const head = version.replace(/^[\\^~]/, \"\").split(\".\")[0] ?? \"0\";\n const n = Number.parseInt(head, 10);\n return Number.isNaN(n) ? 0 : n;\n}\n\n/**\n * Count outdated dependencies for a site, using its committed lockfile as the\n * source of truth for \"what's installed/deployed\". Returns `null` (skip — the\n * caller degrades gracefully) when it can't determine this:\n * - no `pnpm-lock.yaml` (not a pnpm site, or never installed)\n * - the lockfile is stale vs package.json (`--frozen-lockfile` install fails)\n * - `pnpm outdated` output isn't parseable\n *\n * `pnpm outdated` exits non-zero precisely WHEN there are outdated packages, so\n * its exit code is ignored and only its JSON is parsed. `--frozen-lockfile`\n * never mutates the lockfile, so this stays read-only with respect to the repo.\n */\nexport async function scanOutdated(\n sitePath: string,\n spawn: SpawnFn,\n): Promise<OutdatedCounts | null> {\n if (!(await exists(join(sitePath, \"pnpm-lock.yaml\")))) return null;\n\n // Everything below is best-effort: a thrown spawn (timeout, `pnpm` not on\n // PATH, spawn error) must degrade to a skip (null), NOT bubble up and flip the\n // whole deps audit to a hard fail — the declared-range drift is independent of\n // pnpm and must still report. (Mirrors securityAudit's try/catch.)\n try {\n // Materialize node_modules from the lockfile, but only when it's missing —\n // an already-installed checkout skips the cold install. `--frozen-lockfile`\n // never rewrites the lockfile (read-only wrt the repo) and fails fast on a\n // lockfile out of sync with package.json → skip.\n if (!(await exists(join(sitePath, \"node_modules\")))) {\n const install = await spawn(\"pnpm\", [\"install\", \"--frozen-lockfile\"], {\n cwd: sitePath,\n timeoutMs: 180_000,\n });\n if (install.code !== 0) return null;\n }\n\n // `pnpm outdated` exits non-zero precisely WHEN there are outdated packages,\n // so its exit code is ignored and only its JSON is parsed.\n const res = await spawn(\"pnpm\", [\"outdated\", \"--json\"], {\n cwd: sitePath,\n timeoutMs: 60_000,\n });\n const parsed = JSON.parse(res.stdout || \"{}\") as Record<\n string,\n { current?: string; latest?: string }\n >;\n const entries = Object.values(parsed);\n return {\n outdated: entries.length,\n major: entries.filter((e) => e.current && e.latest && majorOf(e.latest) > majorOf(e.current))\n .length,\n };\n } catch {\n return null;\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { ESLint } from \"eslint\";\nimport { check as prettierCheck, resolveConfig as prettierResolveConfig } from \"prettier\";\nimport { glob } from \"tinyglobby\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport type { AuditContext } from \"./util/inject.js\";\n\nconst TARGET_GLOBS = [\"**/*.{ts,js,svelte}\"];\nconst IGNORE = [\"node_modules/**\", \"dist/**\", \".svelte-kit/**\", \"build/**\", \".netlify/**\"];\n\nasync function listFiles(cwd: string): Promise<string[]> {\n return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });\n}\n\nexport async function lintAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const configPath = join(site.path, \"eslint.config.js\");\n\n if (!existsSync(configPath)) {\n return {\n audit: \"lint\",\n site: siteLabel(site),\n status: \"skip\",\n summary: \"no eslint config at site root\",\n };\n }\n\n const eslint = new ESLint({\n cwd: site.path,\n overrideConfigFile: configPath,\n errorOnUnmatchedPattern: false,\n });\n\n const relFiles = await listFiles(site.path);\n\n // Pass relative paths to ESLint; its cwd is already site.path. Avoids\n // dereferencing symlinks on pnpm workspaces.\n const eslintResults = await eslint.lintFiles(relFiles);\n const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);\n const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);\n\n const prettierUnformatted: string[] = [];\n for (const rel of relFiles) {\n const absForResolve = join(site.path, rel);\n const source = await readFile(absForResolve, \"utf-8\");\n const options = (await prettierResolveConfig(absForResolve)) ?? {};\n const ok = await prettierCheck(source, { ...options, filepath: absForResolve });\n if (!ok) prettierUnformatted.push(rel);\n }\n\n const status: AuditResult[\"status\"] =\n eslintErrors > 0 || prettierUnformatted.length > 0\n ? \"fail\"\n : eslintWarnings > 0\n ? \"warn\"\n : \"pass\";\n\n const summary =\n status === \"pass\"\n ? `lint clean across ${relFiles.length} files`\n : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;\n\n return {\n audit: \"lint\",\n site: siteLabel(site),\n status,\n summary,\n details: {\n eslintErrors,\n eslintWarnings,\n prettierUnformatted,\n files: relFiles.length,\n },\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { defaultSpawn, type SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\n\ntype Severity = \"low\" | \"moderate\" | \"high\" | \"critical\";\n\ntype Counts = { low: number; moderate: number; high: number; critical: number };\n\ntype AdvisoryEntry = {\n module: string;\n severity: Severity;\n title: string;\n cves?: string[];\n url?: string;\n};\n\n// pnpm audit output (npm-compat with extra advisories map keyed by ID).\ntype PnpmAuditJson = {\n metadata?: { vulnerabilities?: Partial<Counts> };\n advisories?: Record<\n string,\n {\n id?: number;\n title?: string;\n module_name?: string;\n severity?: string;\n cves?: string[];\n url?: string;\n }\n >;\n};\n\n// npm v7+ shape (vulnerabilities keyed by package name).\ntype NpmAuditJson = {\n metadata?: { vulnerabilities?: Partial<Counts> };\n vulnerabilities?: Record<\n string,\n {\n name?: string;\n severity?: string;\n via?: unknown;\n url?: string;\n }\n >;\n};\n\nfunction classify(v: Counts) {\n if (v.critical > 0 || v.high > 0) return \"fail\" as const;\n if (v.moderate > 0 || v.low > 0) return \"warn\" as const;\n return \"pass\" as const;\n}\n\nfunction normalizeSeverity(s: unknown): Severity {\n if (s === \"low\" || s === \"moderate\" || s === \"high\" || s === \"critical\") return s;\n // npm/pnpm sometimes emit \"info\" for informational advisories. Map down\n // rather than defaulting to \"moderate\" (which would inflate severity).\n return \"low\";\n}\n\nfunction extractAdvisoriesFromPnpm(parsed: PnpmAuditJson): AdvisoryEntry[] {\n const out: AdvisoryEntry[] = [];\n for (const a of Object.values(parsed.advisories ?? {})) {\n if (!a) continue;\n out.push({\n module: a.module_name ?? \"unknown\",\n severity: normalizeSeverity(a.severity),\n title: a.title ?? \"(no title)\",\n ...(a.cves ? { cves: a.cves } : {}),\n ...(a.url ? { url: a.url } : {}),\n });\n }\n return out;\n}\n\n/** Walk an npm v7+ `via` chain to find the root entry whose `via` array\n * contains a real advisory object (rather than another package name string).\n * Returns the package name at the root and the advisory detail. */\nfunction resolveNpmAdvisoryRoot(\n startName: string,\n vulnerabilities: NonNullable<NpmAuditJson[\"vulnerabilities\"]>,\n): { rootName: string; detail?: { title?: string; url?: string } } {\n const seen = new Set<string>();\n let current = startName;\n while (!seen.has(current)) {\n seen.add(current);\n const entry = vulnerabilities[current];\n if (!entry || !Array.isArray(entry.via)) return { rootName: current };\n\n const detailed = entry.via.find(\n (e): e is { title?: string; url?: string } => typeof e === \"object\" && e !== null,\n );\n if (detailed) return { rootName: current, detail: detailed };\n\n const next = entry.via.find((e): e is string => typeof e === \"string\");\n if (!next || next === current) return { rootName: current };\n current = next;\n }\n return { rootName: current };\n}\n\nfunction extractAdvisoriesFromNpm(parsed: NpmAuditJson): AdvisoryEntry[] {\n const vulnerabilities = parsed.vulnerabilities ?? {};\n const roots = new Map<string, AdvisoryEntry>();\n\n for (const [name, v] of Object.entries(vulnerabilities)) {\n if (!v) continue;\n const { rootName, detail } = resolveNpmAdvisoryRoot(name, vulnerabilities);\n if (roots.has(rootName)) continue; // already surfaced via another transitive entry\n\n const rootEntry = vulnerabilities[rootName];\n const severity = normalizeSeverity(rootEntry?.severity ?? v.severity);\n const title = detail?.title ?? rootName;\n const url = detail?.url;\n\n roots.set(rootName, {\n module: rootEntry?.name ?? rootName,\n severity,\n title,\n ...(url ? { url } : {}),\n });\n }\n\n return [...roots.values()];\n}\n\ntype ToolResult =\n | { kind: \"missing\" }\n | { kind: \"error\"; reason: string }\n | { kind: \"ok\"; parsed: PnpmAuditJson & NpmAuditJson };\n\nasync function runAuditTool(\n spawn: (cmd: string, args: readonly string[], opts?: { cwd?: string }) => Promise<SpawnResult>,\n cmd: string,\n args: readonly string[],\n cwd: string,\n): Promise<ToolResult> {\n let raw: SpawnResult;\n try {\n raw = await spawn(cmd, args, { cwd });\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) return { kind: \"missing\" };\n return { kind: \"error\", reason: `spawn failed: ${String(err).slice(0, 200)}` };\n }\n\n // 0 = clean, 1 = vulns found. Anything else is a real error.\n if (raw.code !== 0 && raw.code !== 1) {\n return {\n kind: \"error\",\n reason: `exit ${raw.code}${raw.stderr ? `: ${raw.stderr.slice(0, 150)}` : \"\"}`,\n };\n }\n\n let parsed: PnpmAuditJson & NpmAuditJson;\n try {\n parsed = JSON.parse(raw.stdout || \"{}\") as PnpmAuditJson & NpmAuditJson;\n } catch (err) {\n return { kind: \"error\", reason: `unparseable JSON: ${String(err).slice(0, 100)}` };\n }\n\n // pnpm error envelope: { error: { code, message } }. npm sometimes emits\n // a top-level error too. Either means the audit didn't actually run.\n const errEnvelope = (parsed as unknown as { error?: { code?: string } }).error;\n if (errEnvelope && typeof errEnvelope === \"object\") {\n return { kind: \"error\", reason: errEnvelope.code ?? \"error envelope returned\" };\n }\n\n // Without metadata.vulnerabilities there are no counts to report and we\n // can't trust the result. An empty `{}` is just as suspect as a missing\n // key — counts default to 0 and we'd silently report \"pass\". Treat both\n // as a tool failure so the caller can fall through to the other audit.\n const vulnsMeta = parsed.metadata?.vulnerabilities;\n if (!vulnsMeta || Object.keys(vulnsMeta).length === 0) {\n return { kind: \"error\", reason: \"no metadata.vulnerabilities in output\" };\n }\n\n return { kind: \"ok\", parsed };\n}\n\nexport async function securityAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n let used: \"pnpm audit\" | \"npm audit\" = \"pnpm audit\";\n let result = await runAuditTool(spawn, \"pnpm\", [\"audit\", \"--json\", \"--prod\"], site.path);\n\n // Fall through to npm if pnpm is missing OR pnpm couldn't actually\n // audit the project (e.g., no pnpm-lock.yaml). Previously we only fell\n // through on ENOENT, which meant npm-using sites silently reported \"pass\"\n // because pnpm returned an error envelope with no metadata.\n if (result.kind !== \"ok\") {\n const pnpmReason = result.kind === \"missing\" ? \"not installed\" : result.reason;\n const npmResult = await runAuditTool(\n spawn,\n \"npm\",\n [\"audit\", \"--json\", \"--omit=dev\"],\n site.path,\n );\n if (npmResult.kind === \"ok\") {\n result = npmResult;\n used = \"npm audit\";\n } else {\n const npmReason = npmResult.kind === \"missing\" ? \"not installed\" : npmResult.reason;\n return {\n audit: \"security\",\n site: label,\n status: \"skip\",\n summary: `cannot run audit — pnpm: ${pnpmReason}; npm: ${npmReason}`,\n };\n }\n }\n\n const parsed = result.parsed;\n\n const counts: Counts = {\n low: parsed.metadata?.vulnerabilities?.low ?? 0,\n moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,\n high: parsed.metadata?.vulnerabilities?.high ?? 0,\n critical: parsed.metadata?.vulnerabilities?.critical ?? 0,\n };\n\n const advisories =\n used === \"pnpm audit\" ? extractAdvisoriesFromPnpm(parsed) : extractAdvisoriesFromNpm(parsed);\n\n const status = classify(counts);\n const total = counts.low + counts.moderate + counts.high + counts.critical;\n const summary =\n status === \"pass\"\n ? `${used}: 0 vulnerabilities`\n : `${used}: ${total} vulnerabilities (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;\n\n return {\n audit: \"security\",\n site: label,\n status,\n summary,\n details: { counts, advisories },\n };\n}\n","import { readFile, writeFile, mkdtemp, rm, readdir } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AuditResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { lighthouseConfig } from \"../configs/lighthouse.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn, SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { readSiteConfig } from \"./util/site-config.js\";\nimport { findFreePort, withFreePort } from \"../util/free-port.js\";\n\ntype ManifestEntry = {\n url: string;\n summary: Record<string, number>;\n htmlPath?: string;\n jsonPath?: string;\n};\n\ntype AssertionResult = {\n name: string;\n actual: number;\n expected: number;\n operator: string;\n passed: boolean;\n level: \"warn\" | \"error\";\n auditProperty?: string;\n auditId?: string;\n};\n\ntype NormalizedLhciResult = {\n summary: Record<string, number>;\n assertionsFailed: number;\n assertions: Array<{ category: string; level: \"warn\" | \"error\"; message: string }>;\n};\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\ntype LhrFile = {\n requestedUrl: string;\n finalUrl?: string;\n categories: Record<string, { score: number | null }>;\n};\n\n/**\n * Build manifest-equivalent entries by scanning the `.lighthouseci/` dir\n * for `lhr-*.json` files written by `lhci collect`. We used to read\n * `manifest.json` directly, but lhci 0.15+ no longer writes it — the\n * audit would silently return \"no manifest written\" against a perfectly\n * healthy run. Reproduced on caltex 2026-05-28 (0.10.5 dogfood).\n */\nasync function readLhrEntries(resultsDir: string): Promise<ManifestEntry[]> {\n const files = await readdir(resultsDir).catch(() => [] as string[]);\n const entries: ManifestEntry[] = [];\n for (const f of files) {\n if (!f.startsWith(\"lhr-\") || !f.endsWith(\".json\")) continue;\n const lhr = await readJsonMaybe<LhrFile>(join(resultsDir, f));\n if (!lhr || !lhr.categories) continue;\n const summary: Record<string, number> = {};\n for (const [k, v] of Object.entries(lhr.categories)) {\n if (typeof v?.score === \"number\") summary[k] = v.score;\n }\n entries.push({ url: lhr.requestedUrl, summary });\n }\n return entries;\n}\n\nfunction averageSummaries(entries: ManifestEntry[]): Record<string, number> {\n if (entries.length === 0) return {};\n const sums: Record<string, number> = {};\n const counts: Record<string, number> = {};\n for (const e of entries) {\n for (const [k, v] of Object.entries(e.summary ?? {})) {\n if (typeof v !== \"number\") continue;\n sums[k] = (sums[k] ?? 0) + v;\n counts[k] = (counts[k] ?? 0) + 1;\n }\n }\n const out: Record<string, number> = {};\n for (const k of Object.keys(sums)) {\n const total = sums[k] ?? 0;\n const count = counts[k] ?? 1;\n out[k] = total / count;\n }\n return out;\n}\n\nfunction categoryFromAssertion(a: AssertionResult): string {\n // `name` looks like \"categories:accessibility\" or \"audits:uses-http2\".\n const colonIdx = a.name.indexOf(\":\");\n return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;\n}\n\nfunction messageForAssertion(a: AssertionResult): string {\n // `a.actual` is parsed from external lhci/Lighthouse JSON; a malformed or\n // missing value (not a number) would make `.toFixed` throw and crash the whole\n // audit. Guard it and fall back to a readable string instead.\n const actual = typeof a.actual === \"number\" ? a.actual.toFixed(2) : \"n/a\";\n return `${a.name} ${a.operator} ${a.expected} (actual: ${actual})`;\n}\n\n/** Shared tail: scan `.lighthouseci/` for lhr-*.json + assertion-results.json and\n * build the AuditResult. Identical for the checkout and deployed paths. */\nasync function parseLhciResults(\n resultsDir: string,\n label: string,\n raw: SpawnResult,\n): Promise<AuditResult> {\n const manifest = await readLhrEntries(resultsDir);\n\n if (manifest.length === 0) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"fail\",\n summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const assertionResults =\n (await readJsonMaybe<AssertionResult[]>(join(resultsDir, \"assertion-results.json\"))) ?? [];\n\n const failed = assertionResults.filter((a) => !a.passed);\n const assertions = failed.map((a) => ({\n category: categoryFromAssertion(a),\n level: a.level,\n message: messageForAssertion(a),\n }));\n\n const anyError = assertions.some((a) => a.level === \"error\");\n const anyWarn = assertions.some((a) => a.level === \"warn\");\n const status: AuditResult[\"status\"] = anyError ? \"fail\" : anyWarn ? \"warn\" : \"pass\";\n\n const normalized: NormalizedLhciResult = {\n summary: averageSummaries(manifest),\n assertionsFailed: failed.length,\n assertions,\n };\n\n const summary =\n status === \"pass\"\n ? \"lighthouse: all categories passing\"\n : `lighthouse: ${failed.length} assertion(s) failed`;\n\n return { audit: \"lighthouse\", site: label, status, summary, details: normalized };\n}\n\n/** Checkout mode (unchanged behavior): boot the site's vite dev server on a\n * pinned free port and audit the local fixtures/override URL. */\nasync function checkoutLighthouse(spawn: SpawnFn, site: Site, label: string): Promise<AuditResult> {\n const siteCfg = await readSiteConfig(site.path);\n // Allocate a free port + force vite to `--strictPort` so the spawned dev\n // server either binds the port we picked or fails loudly (caltex 2026-05-28\n // zombie-vite incident).\n const port = await findFreePort();\n const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];\n const resolvedConfig = {\n ...lighthouseConfig,\n ci: {\n ...lighthouseConfig.ci,\n collect: {\n ...lighthouseConfig.ci.collect,\n url: [withFreePort(baseUrl, port)],\n startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`,\n },\n },\n };\n\n const configDir = await mkdtemp(join(tmpdir(), \"reddoor-lhci-\"));\n const configPath = join(configDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(site.path, \".lighthouseci\");\n await rm(resultsDir, { recursive: true, force: true });\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: site.path,\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(configDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n await rm(configDir, { recursive: true, force: true });\n\n return parseLhciResults(resultsDir, label, raw);\n}\n\n/** Deployed mode: audit a production URL directly — no checkout, no dev server.\n * Runs in a throwaway tmp cwd; uploads to the filesystem so fleet runs never\n * push 200 public reports to temporary-public-storage. */\nasync function deployedLighthouse(\n spawn: SpawnFn,\n deployedUrl: string,\n label: string,\n): Promise<AuditResult> {\n const workDir = await mkdtemp(join(tmpdir(), \"reddoor-lh-deployed-\"));\n const resolvedConfig = {\n ci: {\n // Deliberately NOT spread from lighthouseConfig.ci.collect: deployed mode\n // must omit startServerCommand and the dev-server settings entirely.\n collect: {\n url: [deployedUrl],\n // 3 runs to damp Lighthouse's run-to-run variance; parseLhciResults\n // averages the lhr files. (Median is a tracked future refinement.)\n numberOfRuns: 3,\n settings: { preset: \"desktop\", skipAudits: [\"uses-http2\"] },\n },\n assert: lighthouseConfig.ci.assert,\n upload: { target: \"filesystem\", outputDir: join(workDir, \"lhci-report\") },\n },\n };\n\n const configPath = join(workDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(workDir, \".lighthouseci\");\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: workDir,\n // 3 serial cold runs of a slow deployed site (lhci's own maxWaitForLoad\n // ~45-60s each) + first-use Chrome download can plausibly exceed 3 min →\n // SIGTERM → no lhr-*.json → spurious \"no scores\". Match the 5-min budget\n // the checkout path already gives (erp-industrials nightly flake,\n // morning-brief 2026-06-10 MEDIUM-F).\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(workDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n\n try {\n return await parseLhciResults(resultsDir, label, raw);\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\nexport async function lighthouseAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n return site.deployedUrl\n ? deployedLighthouse(spawn, site.deployedUrl, label)\n : checkoutLighthouse(spawn, site, label);\n}\n","export const lighthouseConfig = {\n ci: {\n collect: {\n url: [\"http://localhost:5173/dev/a11y-fixtures\"],\n // `npm run vite:dev` works on both pnpm and npm sites — pnpm respects\n // the `run` form too. Keeps this config portable across the fleet\n // while sites transition to pnpm.\n startServerCommand: \"npm run vite:dev\",\n startServerReadyPattern: \"ready in\",\n startServerReadyTimeout: 120_000,\n numberOfRuns: 1,\n settings: {\n preset: \"desktop\",\n skipAudits: [\"uses-http2\"],\n },\n },\n assert: {\n assertions: {\n \"categories:accessibility\": [\"error\", { minScore: 0.95 }],\n \"categories:best-practices\": [\"error\", { minScore: 0.9 }],\n \"categories:seo\": [\"error\", { minScore: 0.9 }],\n \"categories:performance\": [\"warn\", { minScore: 0.7 }],\n },\n },\n upload: {\n target: \"temporary-public-storage\",\n },\n },\n} as const;\n\nexport default lighthouseConfig;\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport type SiteConfig = {\n /** Override URL the lighthouse audit hits. Sites without the default\n * `/dev/a11y-fixtures` dev route set this to their homepage. */\n lighthouseUrl?: string;\n};\n\n/**\n * Read per-site overrides from `package.json#reddoor`. Returns `{}` on any\n * failure (missing file, malformed JSON, missing key, wrong type) so every\n * caller can safely fall back to its built-in default. Never throws.\n */\nexport async function readSiteConfig(sitePath: string): Promise<SiteConfig> {\n let raw: string;\n try {\n raw = await readFile(join(sitePath, \"package.json\"), \"utf-8\");\n } catch {\n return {};\n }\n let pkg: unknown;\n try {\n pkg = JSON.parse(raw);\n } catch {\n return {};\n }\n if (!pkg || typeof pkg !== \"object\") return {};\n const cfg = (pkg as { reddoor?: unknown }).reddoor;\n if (!cfg || typeof cfg !== \"object\") return {};\n\n const out: SiteConfig = {};\n const url = (cfg as { lighthouseUrl?: unknown }).lighthouseUrl;\n if (typeof url === \"string\" && url.length > 0) {\n out.lighthouseUrl = url;\n }\n return out;\n}\n","import { createServer } from \"node:net\";\n\n/**\n * Bind an ephemeral TCP port, capture it, release it, and return it. Used\n * by the lighthouse and a11y audits to pick a port the audit's own dev\n * server will then bind via `--strictPort`.\n *\n * Why: vite's default behavior on a busy port is to bump to the next free\n * one (5173 → 5174 → …). When zombie vite processes (or any squatter) are\n * already on 5173, the audit's spawned vite lands on a higher port, but\n * the audit tooling (lhci, playwright) still probes 5173 — hits the\n * zombie — gets stale 404s — fails with \"no manifest written\" / \"no\n * results written (exit 1)\". Reproduced on caltex 2026-05-28 with 10\n * orphaned vite processes accumulated across this repo, the reports repo,\n * and caltex itself. Allocating a free port up front + `--strictPort`\n * makes the audit immune to port collisions.\n *\n * TOCTOU note: the small window between close() and the spawned vite\n * binding is theoretically racy, but in practice we run one audit at a\n * time and the OS keeps the port free for re-use. If vite still fails to\n * bind under `--strictPort`, the audit fails loudly — that's the correct\n * outcome (vs. silently auditing the wrong server).\n */\nexport async function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = createServer();\n server.unref();\n server.on(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const addr = server.address();\n if (typeof addr === \"object\" && addr) {\n const port = addr.port;\n server.close(() => resolve(port));\n } else {\n server.close();\n reject(new Error(\"findFreePort: could not determine assigned port from socket\"));\n }\n });\n });\n}\n\n/**\n * Swap the port (and force `localhost` host) on a URL so it points at the\n * audit's freshly-allocated dev server. Preserves the path + any query.\n * Used to rewrite the lighthouse `url` so lhci probes the correct port.\n */\nexport function withFreePort(url: string, port: number): string {\n const u = new URL(url);\n u.hostname = \"localhost\";\n u.port = String(port);\n return u.toString();\n}\n","import { readFile, writeFile, mkdtemp, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { a11yRoutes, smokeRoutes } from \"../configs/playwright-a11y.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { findFreePort } from \"../util/free-port.js\";\n\ntype Impact = \"minor\" | \"moderate\" | \"serious\" | \"critical\";\n\ntype AxeViolation = {\n id: string;\n impact: Impact;\n route: string;\n help?: string;\n helpUrl?: string;\n nodes?: Array<{ html?: string; target?: string[] }>;\n};\n\ntype NormalizedA11y = {\n totalViolations: number;\n byImpact: Partial<Record<Impact, number>>;\n violations: AxeViolation[];\n};\n\nconst RESULTS_REL = \".reddoor-a11y/results.json\";\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n// The audit-controlled playwright config. We synthesize it (rather than\n// rely on the site's playwright.config.ts) so we can pin the dev server\n// port + force `--strictPort` — same fix as the lighthouse audit, same\n// reason (zombie vite processes squatting on 5173 would otherwise eat\n// the audit's request and return stale 404s).\nfunction buildPlaywrightConfig(port: number, sitePath: string): string {\n return `import { defineConfig } from \"@playwright/test\";\n\nexport default defineConfig({\n testDir: \".\",\n testMatch: /.*\\\\.spec\\\\.ts$/,\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n reporter: process.env.CI ? \"github\" : \"list\",\n use: {\n baseURL: \"http://localhost:${port}\",\n trace: \"on-first-retry\",\n },\n webServer: {\n // --strictPort: refuse to bump to a different port if ours is taken,\n // so the audit fails loudly instead of probing a zombie.\n // reuseExistingServer:false: never reuse — we control the lifecycle.\n // cwd: playwright's default webServer.cwd is the config file's\n // directory. Our config lives in /tmp so without this override,\n // \"npm run vite:dev\" tries to read /tmp/.../package.json and\n // ENOENTs before vite ever starts. Caltex 2026-05-28 (0.10.5).\n command: \"npm run vite:dev -- --port ${port} --strictPort\",\n url: \"http://localhost:${port}/dev/a11y-fixtures\",\n cwd: ${JSON.stringify(sitePath)},\n reuseExistingServer: false,\n timeout: 120_000,\n },\n});\n`;\n}\n\n// The spec the audit writes runs all configured routes through axe in a single\n// test (so worker isolation doesn't fragment the collected violations) and\n// writes the structured result to <cwd>/.reddoor-a11y/results.json before\n// asserting. That way, the audit can read real axe details even when the\n// expect(...).toEqual([]) assertion fails.\nfunction buildSpec(): string {\n return `import { test, expect } from \"@playwright/test\";\nimport AxeBuilder from \"@axe-core/playwright\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nconst pages = ${JSON.stringify(a11yRoutes)};\nconst smokePages = ${JSON.stringify(smokeRoutes)};\nconst OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;\n\n// Playwright's default per-test timeout is 30s. We loop through every\n// configured route in a single test, so the budget needs to scale.\ntest.setTimeout(5 * 60_000);\n\ntest(\"a11y + hydration across configured routes\", async ({ page }) => {\n const violations = [];\n\n // Capture uncaught client-side exceptions across every route we visit. A page\n // that builds + SSRs cleanly can still throw on hydrate and blank itself\n // (data-dynamiq: a Svelte 4->5 run() referenced a $state declared after it) --\n // axe never sees that, so we listen for it directly and tag the route in scope.\n let currentRoute = \"\";\n page.on(\"pageerror\", (err) => {\n violations.push({\n id: \"client-error\",\n impact: \"critical\",\n route: currentRoute,\n help: String(err && err.message ? err.message : err),\n });\n });\n\n for (const { path, name } of pages) {\n currentRoute = name;\n await page.goto(path);\n // Snap CSS transitions/animations to their resting state before axe runs.\n // AnimateIn-style fixtures transition opacity 0->1; sampling mid-transition\n // makes axe compute color-contrast against semi-transparent text, yielding a\n // flaky \"serious\" color-contrast violation (~1/3 of runs on /dev/animate-in).\n // Disabling transitions/animations forces the final, rendered state\n // deterministically -- which is also what users (and prefers-reduced-motion\n // users) actually see, so it's the correct thing to assert.\n await page.addStyleTag({\n content: \"*,*::before,*::after{transition:none!important;animation:none!important;}\",\n });\n const results = await new AxeBuilder({ page })\n .withTags([\"wcag2a\",\"wcag2aa\",\"wcag21a\",\"wcag21aa\",\"wcag22aa\"])\n .analyze();\n for (const v of results.violations) {\n violations.push({\n id: v.id,\n impact: v.impact ?? \"moderate\",\n route: name,\n help: v.help,\n helpUrl: v.helpUrl,\n nodes: v.nodes.map((n) => ({ html: n.html, target: n.target })),\n });\n }\n }\n\n // Hydration smoke check: load real routes (the homepage) and fail on any\n // uncaught client-side error. No axe here -- real routes carry pre-existing\n // a11y debt we don't gate on; we only assert they don't crash on hydrate.\n // HTTP/SSR errors don't fire 'pageerror', so a data-less CI homepage that\n // renders empty-but-valid won't false-fail -- only a real client crash does.\n for (const { path, name } of smokePages) {\n currentRoute = name;\n await page.goto(path);\n // Let hydration + first effects run so a TDZ/ReferenceError surfaces.\n await page.waitForTimeout(2000);\n }\n\n const byImpact = {};\n for (const v of violations) {\n byImpact[v.impact] = (byImpact[v.impact] ?? 0) + 1;\n }\n if (OUTPUT) {\n await mkdir(dirname(OUTPUT), { recursive: true });\n await writeFile(\n OUTPUT,\n JSON.stringify({ totalViolations: violations.length, byImpact, violations }, null, 2),\n );\n }\n expect(violations).toEqual([]);\n});\n`;\n}\n\nexport async function a11yAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n // specDir lives INSIDE site.path (not /tmp) so the spec's\n // `import AxeBuilder from \"@axe-core/playwright\"` resolves via Node's\n // walk-up — the site's node_modules is the nearest one. A spec written\n // to /tmp ENOENTs at module resolution before any test runs. Caltex\n // 2026-05-28 (0.10.6 dogfood), third layer of the same class as the\n // webServer.cwd bug.\n const specDir = await mkdtemp(join(site.path, \".reddoor-a11y-spec-\"));\n // Everything past mkdtemp is wrapped so the transient specDir is removed on\n // EVERY catchable exit — success, skip-return, or any throw (a failed\n // writeFile/findFreePort used to orphan it). A timeout-SIGKILL of the parent\n // can't be caught here; `.reddoor-a11y-spec-*/` is fleet-gitignored as the\n // backstop for that. (2026-06-10 MEDIUM-D; recurred from 06-05 M3.)\n try {\n const specPath = join(specDir, \"a11y.spec.ts\");\n await writeFile(specPath, buildSpec(), \"utf-8\");\n\n const port = await findFreePort();\n const configPath = join(specDir, \"playwright.config.ts\");\n await writeFile(configPath, buildPlaywrightConfig(port, site.path), \"utf-8\");\n\n const resultsPath = join(site.path, RESULTS_REL);\n // Clear stale artifacts so a failed spawn never reports old data.\n await rm(join(site.path, \".reddoor-a11y\"), { recursive: true, force: true });\n\n let raw;\n try {\n raw = await spawn(\n \"npx\",\n [\"--yes\", \"playwright\", \"test\", `--config=${configPath}`, \"--reporter=line\", specPath],\n {\n cwd: site.path,\n env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath },\n // playwright on a cold tree downloads Chrome, boots the site's dev\n // server, and runs axe over every configured route. The shared 30 s\n // default in runAudits is fine for deps/lint/security but starves\n // playwright (mirrors the lighthouse fix shipped earlier).\n timeoutMs: 5 * 60_000,\n },\n );\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"a11y\",\n site: label,\n status: \"skip\",\n summary: \"npx/playwright not available\",\n };\n }\n throw err;\n }\n\n const artifact = await readJsonMaybe<NormalizedA11y>(resultsPath);\n\n if (!artifact) {\n return {\n audit: \"a11y\",\n site: label,\n status: \"fail\",\n summary: `a11y: no results written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const hasSerious =\n (artifact.byImpact.serious ?? 0) > 0 || (artifact.byImpact.critical ?? 0) > 0;\n const hasAny = artifact.totalViolations > 0;\n\n const status: AuditResult[\"status\"] = hasSerious ? \"fail\" : hasAny ? \"warn\" : \"pass\";\n const summary =\n status === \"pass\"\n ? `a11y: 0 violations across ${a11yRoutes.length} routes (+${smokeRoutes.length} hydration smoke)`\n : `a11y: ${artifact.totalViolations} violations`;\n\n return {\n audit: \"a11y\",\n site: label,\n status,\n summary,\n details: artifact,\n };\n } finally {\n await rm(specDir, { recursive: true, force: true });\n }\n}\n","import { defineConfig, devices, type PlaywrightTestConfig } from \"@playwright/test\";\n\nexport type A11yRoute = { path: string; name: string };\n\nexport const a11yRoutes: A11yRoute[] = [\n { path: \"/dev/a11y-fixtures\", name: \"a11y fixtures\" },\n { path: \"/dev/animate-in\", name: \"animate-in demo\" },\n];\n\n// Routes smoke-loaded for client-side (hydration) errors only — NOT axe-scanned.\n// Catches the class of bug where build + SSR succeed but client hydration throws\n// and blanks the page (data-dynamiq 2026-06-09: a Svelte 4->5 `run()` referenced\n// a `$state` declared after it → TDZ ReferenceError on hydrate). `/` is the one\n// route every site has; real routes carry a11y debt we don't gate on here, so we\n// assert only that they don't crash on hydrate.\nexport const smokeRoutes: A11yRoute[] = [{ path: \"/\", name: \"home\" }];\n\nexport const playwrightA11yConfig: PlaywrightTestConfig = defineConfig({\n testDir: \"tests\",\n testMatch: /.*\\.spec\\.ts$/,\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n reporter: process.env.CI ? \"github\" : \"list\",\n use: {\n baseURL: \"http://localhost:5173\",\n trace: \"on-first-retry\",\n },\n projects: [\n {\n name: \"chromium\",\n use: { ...devices[\"Desktop Chrome\"] },\n },\n ],\n webServer: {\n // Portable across pnpm and npm sites — pnpm respects `npm run` too.\n command: \"npm run vite:dev\",\n url: \"http://localhost:5173/dev/a11y-fixtures\",\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n },\n});\n\nexport default playwrightA11yConfig;\n","import type { AuditName, AuditResult, Site } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn } from \"./util/spawn.js\";\nimport { depsAudit } from \"./deps.js\";\nimport { lintAudit } from \"./lint.js\";\nimport { securityAudit } from \"./security.js\";\nimport { lighthouseAudit } from \"./lighthouse.js\";\nimport { a11yAudit } from \"./a11y.js\";\n\nconst REGISTRY: Record<AuditName, (ctx: AuditContext) => Promise<AuditResult>> = {\n deps: depsAudit,\n lint: lintAudit,\n security: securityAudit,\n lighthouse: lighthouseAudit,\n a11y: a11yAudit,\n};\n\nexport const ALL_AUDIT_NAMES = Object.keys(REGISTRY) as AuditName[];\n\n/** Default per-audit spawn timeout when running via runAudits (30 s). */\nconst DEFAULT_AUDIT_TIMEOUT_MS = 30_000;\n\nfunction timedSpawn(timeoutMs: number): SpawnFn {\n return (cmd, args, opts = {}) =>\n defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });\n}\n\n/** Single-audit runner with the same error-to-result conversion that\n * `runAudits` applies. Exposed so the CLI can wrap each audit in its\n * own progress task (listr2) and surface per-audit completion timing,\n * while keeping audit implementations UI-free. */\nexport async function runOneAudit(site: Site, name: AuditName): Promise<AuditResult> {\n if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);\n const spawn = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);\n // `||` not `??`: an empty-string slug (Airtable Name with no slug-able chars)\n // must fall back to the path, not render a blank `AuditResult.site` that would\n // then collapse fleet write-back grouping under the \"\" key.\n const label = site.name || site.path;\n try {\n return await REGISTRY[name]({ site, spawn });\n } catch (err) {\n return {\n audit: name,\n site: label,\n status: \"fail\",\n summary: `${name}: unexpected error — ${String(err)}`,\n };\n }\n}\n\nexport async function runAudits(site: Site, which?: AuditName[]): Promise<AuditResult[]> {\n const names = which ?? ALL_AUDIT_NAMES;\n for (const n of names) {\n if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);\n }\n return Promise.all(names.map((n) => runOneAudit(site, n)));\n}\n\nexport async function runAuditsAcross(sites: Site[], which?: AuditName[]): Promise<AuditResult[]> {\n const all = await Promise.all(sites.map((s) => runAudits(s, which)));\n return all.flat();\n}\n\nexport { depsAudit, lintAudit, securityAudit, lighthouseAudit, a11yAudit };\n","import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport type { RecipeResult, Site, ConfigName } from \"../types.js\";\nimport { ALL_TEMPLATES, templatesByName, type ConfigTemplate } from \"./sync-configs/templates.js\";\nimport {\n CANONICAL_GITIGNORE_ENTRIES,\n mergeGitignore,\n findTrackedArtifacts,\n} from \"./sync-configs/gitignore.js\";\nimport { listTrackedFiles, removeFromIndex } from \"../util/git.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type SyncConfigsOptions = {\n which?: ConfigName[];\n};\n\nconst GITIGNORE_CONFIG: ConfigName = \"gitignore\";\nconst SVELTE_CONFIG: ConfigName = \"svelte\";\nconst NETLIFY_CONFIG: ConfigName = \"netlify\";\n\n/** A site's `svelte.config.js` is \"compliant\" — and left untouched by sync —\n * once it builds on the canonical helpers (createSvelteConfig + adapter-netlify).\n *\n * Unlike the other exact-match templates, svelte.config legitimately carries\n * site-specific `kit.alias` and `compilerOptions`; an exact overwrite would\n * clobber those on every sync (it silently dropped MSOT's $utils alias,\n * 2026-06-04). So once a config is on the canonical pattern we preserve it as-is\n * and only rewrite a genuinely off-pattern (or missing) config. `createSvelteConfig`\n * now provides the canonical `$lib` aliases itself, and a site's own `kit.alias`\n * overrides per key (and may add more), so a site's additive customization is safe\n * to preserve. */\nfunction isSvelteConfigCompliant(contents: string): boolean {\n return contents.includes(\"createSvelteConfig\") && contents.includes(\"@sveltejs/adapter-netlify\");\n}\n\n/** Any of the baseline security headers — the marker that a netlify.toml is\n * deliberately hardened (vs. e.g. a cache-control-only `[[headers]]` block). */\nconst SECURITY_HEADER_RE =\n /Strict-Transport-Security|Content-Security-Policy|X-Frame-Options|X-Content-Type-Options|Referrer-Policy|Permissions-Policy|Cross-Origin-Opener-Policy/i;\n\n/** A site's `netlify.toml` is \"compliant\" — and left untouched by sync — once it\n * carries a `[[headers]]` block AND a security header (HSTS/CSP/X-Frame-Options/…).\n *\n * Like svelte.config, netlify.toml legitimately holds site-specific config\n * (custom CSP, redirects, per-route headers). The canonical template ships the\n * baseline security headers, but an exact overwrite would CLOBBER a site's own\n * hardening — that bug stripped gallerysonder's headers on a routine sync\n * (2026-06-10). So a genuinely-hardened file is left alone, while a missing,\n * header-less (previously-stripped), OR merely cache-header file is non-compliant\n * and gets the canonical template, which backfills the security baseline. */\nfunction isNetlifyConfigCompliant(contents: string): boolean {\n return contents.includes(\"[[headers]]\") && SECURITY_HEADER_RE.test(contents);\n}\n\n/** Runtime enumeration of every `ConfigName`. Mirror of the union in\n * `src/types.ts`. Used by CLI `--only` validation; a missing entry would\n * silently accept typos. The type-test in `tests/types.test.ts` guards\n * against drift between this array and the union. */\nexport const ALL_CONFIG_NAMES: ConfigName[] = [\n \"lighthouse\",\n \"eslint\",\n \"prettier\",\n \"prettier-ignore\",\n \"playwright-a11y\",\n \"svelte\",\n \"gitignore\",\n \"ci\",\n \"renovate-action\",\n \"renovate-config\",\n \"netlify\",\n];\n\nexport function isConfigName(value: string): value is ConfigName {\n return (ALL_CONFIG_NAMES as string[]).includes(value);\n}\n\nasync function readMaybe(path: string): Promise<string | null> {\n try {\n return await readFile(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nasync function planTemplateDiffs(\n cwd: string,\n templates: ConfigTemplate[],\n): Promise<ConfigTemplate[]> {\n const diffs: ConfigTemplate[] = [];\n for (const t of templates) {\n const existing = await readMaybe(join(cwd, t.path));\n if (existing === t.contents) continue;\n // svelte.config is compliance-checked, not exact-matched: an existing config\n // already on the canonical pattern is left alone so its aliases/compilerOptions\n // survive. A missing (null) or off-pattern config still gets the canonical template.\n if (t.config === SVELTE_CONFIG && existing !== null && isSvelteConfigCompliant(existing)) {\n continue;\n }\n // netlify.toml is likewise compliance-checked: a file that already carries\n // `[[headers]]` is hardened and left alone (an exact overwrite would strip\n // its security headers). A header-less / missing file gets the template.\n if (t.config === NETLIFY_CONFIG && existing !== null && isNetlifyConfigCompliant(existing)) {\n continue;\n }\n diffs.push(t);\n }\n return diffs;\n}\n\ntype GitignorePlan =\n | { kind: \"noop\" }\n | { kind: \"apply\"; content: string; toUntrack: string[]; added: string[] };\n\nasync function planGitignore(cwd: string): Promise<GitignorePlan> {\n const existing = await readMaybe(join(cwd, \".gitignore\"));\n const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);\n const tracked = await listTrackedFiles(cwd);\n const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);\n if (merge.added.length === 0 && toUntrack.length === 0) return { kind: \"noop\" };\n return { kind: \"apply\", content: merge.content, toUntrack, added: merge.added };\n}\n\nasync function applyGitignore(\n cwd: string,\n plan: Extract<GitignorePlan, { kind: \"apply\" }>,\n): Promise<void> {\n await writeFile(join(cwd, \".gitignore\"), plan.content, \"utf-8\");\n if (plan.toUntrack.length > 0) {\n await removeFromIndex(cwd, plan.toUntrack);\n }\n}\n\nexport async function syncConfigs(\n site: Site,\n opts: SyncConfigsOptions = {},\n): Promise<RecipeResult> {\n const requested = opts.which ?? ALL_TEMPLATES.map((t) => t.config).concat(GITIGNORE_CONFIG);\n const templateNames = requested.filter((c): c is ConfigName => c !== GITIGNORE_CONFIG);\n const templates = templatesByName(templateNames);\n const includeGitignore = requested.includes(GITIGNORE_CONFIG);\n\n return withRecipe({\n name: \"sync-configs\",\n site,\n plan: async () => {\n const templateDiffs = await planTemplateDiffs(site.path, templates);\n const gitignorePlan: GitignorePlan = includeGitignore\n ? await planGitignore(site.path)\n : { kind: \"noop\" };\n if (templateDiffs.length === 0 && gitignorePlan.kind === \"noop\") {\n return { kind: \"noop\", notes: \"all targeted configs already match\" };\n }\n return { kind: \"apply\", plan: { templateDiffs, gitignorePlan } };\n },\n apply: async ({ templateDiffs, gitignorePlan }, { commit }) => {\n for (const t of templateDiffs) {\n const dest = join(site.path, t.path);\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, t.contents, \"utf-8\");\n await commit(`chore: sync ${t.config} config from @reddoorla/maintenance`);\n }\n if (gitignorePlan.kind === \"apply\") {\n await applyGitignore(site.path, gitignorePlan);\n await commit(`chore: sync gitignore from @reddoorla/maintenance`);\n }\n return { kind: \"ok\" };\n },\n });\n}\n","import type { ConfigName } from \"../../types.js\";\n\nexport type ConfigTemplate = {\n config: ConfigName;\n path: string;\n contents: string;\n};\n\nconst eslint: ConfigTemplate = {\n config: \"eslint\",\n path: \"eslint.config.js\",\n contents: `import { createEslintConfig } from \"@reddoorla/maintenance/configs/eslint\";\nimport svelteConfig from \"./svelte.config.js\";\n\nexport default createEslintConfig({ svelteConfig });\n`,\n};\n\nconst prettier: ConfigTemplate = {\n config: \"prettier\",\n path: \".prettierrc.json\",\n contents: `{\n \"trailingComma\": \"all\",\n \"singleQuote\": false,\n \"printWidth\": 100,\n \"plugins\": [\"prettier-plugin-svelte\"]\n}\n`,\n};\n\nconst prettierIgnore: ConfigTemplate = {\n config: \"prettier-ignore\",\n path: \".prettierignore\",\n contents: `pnpm-lock.yaml\n.svelte-kit/\nbuild/\n.netlify/\ndist/\n`,\n};\n\nconst lighthouse: ConfigTemplate = {\n config: \"lighthouse\",\n path: \"lighthouserc.json\",\n contents: `${JSON.stringify(\n {\n $note:\n \"Generated by @reddoorla/maintenance sync-configs; edit src/configs/lighthouse.ts in the package instead.\",\n extends: \"@reddoorla/maintenance/configs/lighthouse\",\n },\n null,\n 2,\n )}\n`,\n};\n\nconst playwrightA11y: ConfigTemplate = {\n config: \"playwright-a11y\",\n path: \"playwright.config.ts\",\n contents: `export { default } from \"@reddoorla/maintenance/configs/playwright-a11y\";\n`,\n};\n\nconst svelte: ConfigTemplate = {\n config: \"svelte\",\n path: \"svelte.config.js\",\n contents: `import { createSvelteConfig } from \"@reddoorla/maintenance/configs/svelte\";\nimport adapter from \"@sveltejs/adapter-netlify\";\n\n/** @type {import('@sveltejs/kit').Config} */\nexport default createSvelteConfig({\n kit: { adapter: adapter({ edge: false, split: false }) },\n});\n`,\n};\n\n// The `ci:` job name below + the reusable workflow's `ci` job name produce the\n// branch-protection check context \"ci / ci\" — kept in sync with REQUIRED_CHECK in\n// src/recipes/self-updating/index.ts. Renaming this job means updating that constant.\nconst ci: ConfigTemplate = {\n config: \"ci\",\n path: \".github/workflows/ci.yml\",\n contents: `name: ci\non:\n pull_request:\n push:\n branches: [main]\njobs:\n ci:\n uses: reddoorla/.github/.github/workflows/ci.yml@78c4da64b675f0f474961f12715f2a4c09d46eb5 # v1.0.0\n`,\n};\n\nconst renovateAction: ConfigTemplate = {\n config: \"renovate-action\",\n path: \".github/workflows/renovate.yml\",\n contents: `name: renovate\non:\n schedule:\n - cron: \"0 7 * * 1\"\n workflow_dispatch:\njobs:\n renovate:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: renovatebot/github-action@v46.1.14\n with:\n token: \\${{ secrets.RENOVATE_TOKEN }}\n env:\n RENOVATE_REPOSITORIES: \\${{ github.repository }}\n`,\n};\n\nconst renovateConfig: ConfigTemplate = {\n config: \"renovate-config\",\n path: \"renovate.json\",\n contents: `{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\"github>reddoorla/.github:renovate-config\"]\n}\n`,\n};\n\nconst netlify: ConfigTemplate = {\n config: \"netlify\",\n path: \"netlify.toml\",\n contents: `[build]\n command = \"pnpm build\"\n publish = \"build/\"\n functions = \"functions/\"\n\n[build.environment]\n NODE_VERSION = \"22\"\n COREPACK_INTEGRITY_KEYS = \"0\"\n\n# Baseline security headers for all responses. CSP is emitted per-response by\n# SvelteKit (see \\`kit.csp\\` in svelte.config.js) so it is intentionally omitted\n# here to avoid conflicting duplicates.\n[[headers]]\n for = \"/*\"\n [headers.values]\n Strict-Transport-Security = \"max-age=63072000; includeSubDomains; preload\"\n X-Content-Type-Options = \"nosniff\"\n X-Frame-Options = \"SAMEORIGIN\"\n Referrer-Policy = \"strict-origin-when-cross-origin\"\n Permissions-Policy = \"camera=(), microphone=(), geolocation=(), interest-cohort=()\"\n Cross-Origin-Opener-Policy = \"same-origin\"\n\n[[headers]]\n for = \"/favicon.png\"\n [headers.values]\n Cache-Control = \"public, max-age=31536000, immutable\"\n\n[[headers]]\n for = \"/_app/immutable/*\"\n [headers.values]\n Cache-Control = \"public, max-age=31536000, immutable\"\n`,\n};\n\nexport const ALL_TEMPLATES: ConfigTemplate[] = [\n eslint,\n prettier,\n prettierIgnore,\n lighthouse,\n playwrightA11y,\n svelte,\n ci,\n renovateAction,\n renovateConfig,\n netlify,\n];\n\nexport function templatesByName(which: ConfigName[]): ConfigTemplate[] {\n return ALL_TEMPLATES.filter((t) => which.includes(t.config));\n}\n","/**\n * Comment line written above the appended block so future runs (and humans)\n * can recognize the managed section. Presence of this line is incidental —\n * the merge logic is keyed on each entry's normalized form, not on the marker.\n */\nexport const MANAGED_MARKER = \"# canonical entries from @reddoorla/maintenance sync-configs\";\n\n/**\n * Build artifacts, test outputs, deploy caches, and secrets that should never\n * be tracked across the reddoor fleet. Sites may keep additional site-specific\n * entries — they are preserved on merge.\n */\nexport const CANONICAL_GITIGNORE_ENTRIES: readonly string[] = [\n \"node_modules/\",\n \"build/\",\n \"dist/\",\n \".svelte-kit/\",\n \"coverage/\",\n \".vitest-cache/\",\n \"playwright-report/\",\n \"test-results/\",\n \".lighthouseci/\",\n \".tsbuildinfo\",\n \".env\",\n \".env.*\",\n \"!.env.example\",\n \".DS_Store\",\n \"*.log\",\n \".vercel/\",\n \".netlify/\",\n \".reddoor-a11y/\",\n // The a11y audit's transient spec dir, written inside the checkout and\n // normally cleaned, but a timeout-SIGKILL of the parent orphans it. Ignored\n // fleet-wide so it never dirties a self-updating repo's tree (2026-06-10 M-D).\n \".reddoor-a11y-spec-*/\",\n];\n\nexport type MergeResult = { content: string; added: string[] };\n\nfunction stripLeadingSlash(s: string): string {\n return s.startsWith(\"/\") ? s.slice(1) : s;\n}\n\nfunction stripTrailingSlash(s: string): string {\n return s.endsWith(\"/\") ? s.slice(0, -1) : s;\n}\n\n/**\n * Normalize for presence comparison only: strip leading `/`, trailing `/`,\n * and surrounding whitespace. `build`, `/build`, `build/`, and `/build/` all\n * collapse to the same key.\n */\nfunction normalizePresence(line: string): string {\n return stripTrailingSlash(stripLeadingSlash(line.trim()));\n}\n\nfunction presentSet(existing: string): Set<string> {\n const set = new Set<string>();\n for (const raw of existing.split(/\\r?\\n/)) {\n const trimmed = raw.trim();\n if (!trimmed) continue;\n if (trimmed.startsWith(\"#\")) continue;\n set.add(normalizePresence(trimmed));\n }\n return set;\n}\n\n/**\n * Merge `canonical` entries into `existing` .gitignore content.\n *\n * - Missing entries are appended under a managed marker comment.\n * - Existing entries (in any normalized variant — `/build`, `build/`, etc.)\n * are preserved as-is; we never rewrite the site's own lines.\n * - When every canonical entry is already present, returns the original\n * content unchanged with `added: []` — the recipe can treat that as noop.\n */\nexport function mergeGitignore(existing: string | null, canonical: readonly string[]): MergeResult {\n if (existing === null) {\n const body = [MANAGED_MARKER, ...canonical].join(\"\\n\") + \"\\n\";\n return { content: body, added: [...canonical] };\n }\n const present = presentSet(existing);\n const added: string[] = [];\n for (const entry of canonical) {\n const norm = normalizePresence(entry);\n if (!present.has(norm)) {\n added.push(entry);\n present.add(norm);\n }\n }\n if (added.length === 0) {\n return { content: existing, added: [] };\n }\n let base = existing;\n if (!base.endsWith(\"\\n\")) base += \"\\n\";\n const block = [\"\", MANAGED_MARKER, ...added].join(\"\\n\") + \"\\n\";\n return { content: base + block, added };\n}\n\n/**\n * Of the tracked paths, return those that fall under a canonical *directory*\n * entry — i.e., paths that the freshly-synced .gitignore now wants ignored\n * but which git currently has in the index.\n *\n * File-pattern entries (`.env`, `*.log`, `.DS_Store`) are intentionally\n * skipped: they may contain user-meaningful data, and `git rm --cached`\n * cannot scrub secrets from history anyway. Surfaced for manual review\n * instead of auto-removing.\n */\nexport function findTrackedArtifacts(\n tracked: readonly string[],\n canonical: readonly string[],\n): string[] {\n const dirEntries: string[] = [];\n for (const raw of canonical) {\n const t = raw.trim();\n if (!t) continue;\n if (t.startsWith(\"!\")) continue;\n if (/[*?[]/.test(t)) continue;\n const noLead = stripLeadingSlash(t);\n if (!noLead.endsWith(\"/\")) continue;\n const name = stripTrailingSlash(noLead);\n if (!name) continue;\n dirEntries.push(name);\n }\n const matched: string[] = [];\n for (const path of tracked) {\n for (const dir of dirEntries) {\n if (path === dir || path.startsWith(dir + \"/\")) {\n matched.push(path);\n break;\n }\n }\n }\n return matched;\n}\n","import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst exec = promisify(execFile);\n\nasync function git(cwd: string, args: string[]): Promise<{ stdout: string; stderr: string }> {\n return exec(\"git\", args, { cwd, env: process.env });\n}\n\nexport function branchName(recipe: string, when: Date = new Date()): string {\n // ISO with millisecond precision: 2026-05-20T10:30:00.123Z → 20260520T103000123Z.\n // Millis (vs. second-precision) shrinks the collision window for parallel runs.\n const compact = when.toISOString().replace(/[-:.]/g, \"\");\n return `maint/${recipe}-${compact}`;\n}\n\nexport async function currentBranch(cwd: string): Promise<string> {\n const { stdout } = await git(cwd, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n return stdout.trim();\n}\n\nexport async function isWorkingTreeClean(cwd: string): Promise<boolean> {\n const { stdout } = await git(cwd, [\"status\", \"--porcelain\"]);\n return stdout.trim().length === 0;\n}\n\nexport async function createBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", \"-b\", name]);\n}\n\n/** Check out an existing branch. Throws (via git) if the branch is missing or\n * the checkout is blocked (e.g. uncommitted changes that would be overwritten). */\nexport async function checkoutBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", name]);\n}\n\n/**\n * Force-check-out an existing branch, DISCARDING any uncommitted changes on the\n * current branch. Used by the recipe failure path to return the operator to\n * their original branch even when a recipe left the work-in-progress branch\n * dirty. Only ever called with the operator's ORIGINAL branch as `name`. Does\n * not run `git clean`, so untracked operator files are left untouched.\n */\nexport async function forceCheckoutBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", \"-f\", name]);\n}\n\n/**\n * Delete a local branch with `-D` (force). Used by the recipe failure path to\n * remove the branch the recipe itself created so a re-run starts clean. Callers\n * MUST only ever pass the recipe-created branch here, never the operator's\n * original branch.\n */\nexport async function deleteBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"branch\", \"-D\", name]);\n}\n\nexport async function stageAll(cwd: string): Promise<void> {\n await git(cwd, [\"add\", \"-A\"]);\n}\n\nexport async function listTrackedFiles(cwd: string): Promise<string[]> {\n const { stdout } = await git(cwd, [\"ls-files\"]);\n return stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n}\n\nexport async function removeFromIndex(cwd: string, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n await git(cwd, [\"rm\", \"-r\", \"--cached\", \"--\", ...paths]);\n}\n\n/**\n * Stages all current changes and commits with `message`. Returns the commit SHA,\n * or `null` if there was nothing to commit.\n */\nexport async function commit(cwd: string, message: string): Promise<string | null> {\n await stageAll(cwd);\n const { stdout: status } = await git(cwd, [\"status\", \"--porcelain\"]);\n if (status.trim().length === 0) return null;\n await git(cwd, [\"commit\", \"-m\", message]);\n const { stdout: sha } = await git(cwd, [\"rev-parse\", \"HEAD\"]);\n return sha.trim();\n}\n\n/**\n * Strict GitHub repo identity: exactly two `[A-Za-z0-9._-]` segments separated\n * by a single slash. Rejects a scheme, host, extra path segment, traversal\n * (`..`), whitespace, or an argv flag — anything that could retarget a `gh`\n * write at an unintended (attacker/typo-controlled) repo.\n */\nexport const OWNER_REPO_RE = /^[A-Za-z0-9._-]+\\/[A-Za-z0-9._-]+$/;\n\n/** True when `repo` is a clean `owner/repo` (see {@link OWNER_REPO_RE}). The\n * explicit `..` reject covers traversal segments the char-class would otherwise\n * admit (`.` is a legal repo char, so `../evil` matches the shape regex). */\nexport function isOwnerRepo(repo: string): boolean {\n if (repo.includes(\"..\")) return false;\n return OWNER_REPO_RE.test(repo);\n}\n\n/**\n * True when two repo references name the same `owner/repo`. Each side may be a\n * full remote URL (https or scp-style), or already an `owner/repo`. Used to\n * verify an existing checkout's `origin` matches the site's expected repo\n * before reusing it. Returns false if either side is unparseable.\n */\nexport function sameOwnerRepo(a: string, b: string): boolean {\n const na = isOwnerRepo(a.trim()) ? a.trim() : parseOwnerRepo(a);\n const nb = isOwnerRepo(b.trim()) ? b.trim() : parseOwnerRepo(b);\n if (na === null || nb === null) return false;\n return na.toLowerCase() === nb.toLowerCase();\n}\n\n/** Derive `owner/repo` from a git remote URL (https or scp-style). Null if unparseable. */\nexport function parseOwnerRepo(remoteUrl: string): string | null {\n const trimmed = remoteUrl\n .trim()\n .replace(/\\.git$/, \"\")\n .replace(/\\/$/, \"\");\n // scp-style: git@github.com:owner/repo\n const scp = trimmed.match(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:(.+)$/);\n const path = scp ? scp[1]! : trimmed.replace(/^https?:\\/\\/[^/]+\\//, \"\");\n const segments = path.split(\"/\").filter(Boolean);\n if (segments.length < 2) return null;\n return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;\n}\n\n/** `origin` remote URL for a checkout, trimmed. Throws (via git) if there's no origin. */\nexport async function getRemoteUrl(cwd: string): Promise<string> {\n const { stdout } = await git(cwd, [\"remote\", \"get-url\", \"origin\"]);\n return stdout.trim();\n}\n\n/** Push a branch to origin, setting upstream. Throws on non-zero (execFile rejects). */\nexport async function push(cwd: string, branch: string): Promise<void> {\n await git(cwd, [\"push\", \"-u\", \"origin\", branch]);\n}\n","import type { RecipeName, RecipeResult, Site } from \"../types.js\";\nimport {\n branchName,\n checkoutBranch,\n commit as gitCommit,\n createBranch,\n currentBranch,\n deleteBranch,\n forceCheckoutBranch,\n isWorkingTreeClean,\n} from \"../util/git.js\";\nimport { siteLabel } from \"../util/site.js\";\n\n/** Outcome of the read-only planning phase. `noop` and `failed` short-circuit\n * without creating a branch; `apply` carries the recipe-specific plan data\n * forward to the apply phase. */\nexport type RecipePlan<P> =\n | { kind: \"noop\"; notes?: string }\n | { kind: \"failed\"; notes: string }\n | { kind: \"apply\"; plan: P };\n\nexport type RecipeApplyCtx = {\n /** Stage all current changes and commit. Returns the SHA, or null if\n * nothing was staged. The wrapper accumulates SHAs into the final\n * RecipeResult. */\n commit: (message: string) => Promise<string | null>;\n /** Branch name that was created for this run. */\n branch: string;\n /** Site path — same as `site.path`. */\n cwd: string;\n};\n\nexport type RecipeApplyResult = { kind: \"ok\"; notes?: string } | { kind: \"failed\"; notes: string };\n\nexport type RecipeBody<P> = {\n name: RecipeName;\n site: Site;\n /** Inspect the site and decide: noop, failed, or proceed (with plan data\n * passed to apply). Runs before the working-tree clean check unless\n * `checkTreeFirst: true` is set, so most recipes can noop on a dirty\n * tree without throwing. */\n plan: () => Promise<RecipePlan<P>>;\n /** Make the actual changes. Use `ctx.commit(msg)` for each logical step;\n * the wrapper collects SHAs into `RecipeResult.commits`. Return\n * `{ kind: \"failed\", notes }` to abort partway and surface the failure. */\n apply: (plan: P, ctx: RecipeApplyCtx) => Promise<RecipeApplyResult>;\n /** Check working tree clean BEFORE `plan()` runs. Use only when plan\n * itself mutates the tree (e.g. `bump-deps` runs `pnpm install` in plan\n * for an accurate outdated probe). Default false — clean check happens\n * after plan only if plan returns proceed, allowing noop-on-dirty for\n * read-only plans (a tree with stray edits + no recipe work to do\n * should not throw). */\n checkTreeFirst?: boolean;\n};\n\n/** Wrap a recipe's plan/apply phases. Centralises the siteLabel /\n * clean-tree check / branch creation / commit accumulation / RecipeResult\n * construction boilerplate that every recipe used to re-implement. */\nexport async function withRecipe<P>(body: RecipeBody<P>): Promise<RecipeResult> {\n const label = siteLabel(body.site);\n\n if (body.checkTreeFirst && !(await isWorkingTreeClean(body.site.path))) {\n throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);\n }\n\n const planned = await body.plan();\n\n if (planned.kind === \"noop\") {\n return {\n recipe: body.name,\n site: label,\n status: \"noop\",\n commits: [],\n ...(planned.notes ? { notes: planned.notes } : {}),\n };\n }\n if (planned.kind === \"failed\") {\n return {\n recipe: body.name,\n site: label,\n status: \"failed\",\n commits: [],\n notes: planned.notes,\n };\n }\n\n if (!body.checkTreeFirst && !(await isWorkingTreeClean(body.site.path))) {\n throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);\n }\n\n // Capture the operator's branch BEFORE we create the recipe branch, so we can\n // return them to it afterwards (#2) and so the failure path (#3) knows which\n // branch to force-restore to. Best-effort: if we can't read it (detached HEAD,\n // git error) we proceed with `original = null` and skip any force operations\n // rather than guess — we must NEVER force-discard/delete a branch we're unsure\n // of.\n let original: string | null = null;\n try {\n original = await currentBranch(body.site.path);\n } catch {\n original = null;\n }\n\n const branch = branchName(body.name);\n await createBranch(body.site.path, branch);\n\n /**\n * Best-effort restore to the operator's original branch. Never throws — a\n * restore failure must not turn an otherwise-clean recipe result into a\n * failure (#2). Skipped when we couldn't capture the original branch.\n *\n * IMPORTANT (composition): this is invoked only on the NOOP-from-apply path\n * (the recipe created a branch but committed nothing — leaving the operator\n * parked on an empty maint branch is pure downside). It is deliberately NOT\n * invoked on the APPLIED path: the fleet onboarding pipeline composes recipes\n * by running them in sequence against the SAME checkout, each building on the\n * prior's committed files in the working tree (convert-to-pnpm's lockfile →\n * onboard's deps → sync-configs → svelte-codemods). Restoring to the base\n * branch after an applied recipe would strip those files from the working\n * tree and break composition (verified live across the fleet). selfUpdating,\n * which PUSHES its branch, does its own post-push restore since its local\n * branch is disposable.\n */\n const restoreOriginal = async (): Promise<void> => {\n if (original === null || original === branch) return;\n try {\n await checkoutBranch(body.site.path, original);\n } catch (err) {\n // Leave the operator on the recipe branch rather than fail the result.\n console.warn(\n `warning: could not restore branch ${original} after ${body.name}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n };\n\n /**\n * Failure restore (#3): force the checkout back to the captured original\n * branch (discarding the recipe branch's uncommitted changes) and delete the\n * recipe branch, so a re-run starts clean. SAFETY: only ever force-checks-out\n * the captured `original` and only ever deletes `branch` (the recipe-created\n * branch); never deletes `original`, never runs `git clean`. If `original` is\n * unavailable we do nothing (the safe subset) — better to leave the operator\n * parked than to force anything we're unsure about. Best-effort: never throws.\n */\n const restoreAfterFailure = async (): Promise<void> => {\n if (original === null || original === branch) return;\n try {\n await forceCheckoutBranch(body.site.path, original);\n } catch (err) {\n console.warn(\n `warning: could not force-restore branch ${original} after failed ${body.name}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n // If we couldn't get off the recipe branch, deleting it would fail anyway;\n // and we must never delete the branch we're still on.\n return;\n }\n try {\n await deleteBranch(body.site.path, branch);\n } catch (err) {\n console.warn(\n `warning: could not delete recipe branch ${branch} after failed ${body.name}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n };\n\n const shas: string[] = [];\n let result: RecipeApplyResult;\n try {\n result = await body.apply(planned.plan, {\n cwd: body.site.path,\n branch,\n commit: async (msg) => {\n const sha = await gitCommit(body.site.path, msg);\n if (sha) shas.push(sha);\n return sha;\n },\n });\n } catch (err) {\n // Body threw mid-mutation: force-restore + delete the recipe branch so the\n // checkout is retriable, then re-throw (preserve the prior throw semantics —\n // callers like init treat an uncaught throw as an `error` step).\n await restoreAfterFailure();\n throw err;\n }\n\n if (result.kind === \"failed\") {\n await restoreAfterFailure();\n return {\n recipe: body.name,\n site: label,\n status: \"failed\",\n commits: shas,\n notes: result.notes,\n };\n }\n\n // NOOP-from-apply only: no commits to compose, so don't leave the operator\n // parked on an empty maint branch. The APPLIED path intentionally stays on the\n // maint branch so the onboarding pipeline can compose (see restoreOriginal).\n if (shas.length === 0) {\n await restoreOriginal();\n }\n\n const notes = result.notes ? `${result.notes}; branch: ${branch}` : `branch: ${branch}`;\n return {\n recipe: body.name,\n site: label,\n status: shas.length > 0 ? \"applied\" : \"noop\",\n commits: shas,\n notes,\n };\n}\n","import { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../types.js\";\nimport { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type BumpDepsGroup = \"patch\" | \"minor\" | \"major\";\n\nexport type BumpDepsOptions = {\n group?: BumpDepsGroup;\n spawn?: SpawnFn;\n};\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction outdatedFlagsForGroup(group: BumpDepsGroup): string[] {\n if (group === \"major\") return [\"--latest\"];\n if (group === \"minor\") return [];\n return [\"--depth\", \"0\"];\n}\n\nfunction upFlagsForGroup(group: BumpDepsGroup): string[] {\n if (group === \"major\") return [\"--latest\"];\n return [];\n}\n\ntype Plan = { group: BumpDepsGroup };\n\nexport async function bumpDeps(site: Site, opts: BumpDepsOptions = {}): Promise<RecipeResult> {\n const group: BumpDepsGroup = opts.group ?? \"minor\";\n const spawn = opts.spawn ?? defaultSpawn;\n\n return withRecipe<Plan>({\n name: \"bump-deps\",\n site,\n // pnpm install (in plan) mutates the lockfile, so the clean-tree check\n // MUST happen first — otherwise a desynced-lockfile resync would silently\n // land on top of whatever else was in the tree.\n checkTreeFirst: true,\n plan: async () => {\n // Pre-flight: the recipe is pnpm-only. A package-lock.json or yarn.lock\n // without pnpm-lock.yaml means the site is still on a different package\n // manager; we refuse to run rather than emit confusing pnpm errors.\n const hasPnpmLock = await exists(join(site.path, \"pnpm-lock.yaml\"));\n if (!hasPnpmLock) {\n const hasNpmLock = await exists(join(site.path, \"package-lock.json\"));\n const hasYarnLock = await exists(join(site.path, \"yarn.lock\"));\n if (hasNpmLock || hasYarnLock) {\n const competing = hasNpmLock ? \"package-lock.json\" : \"yarn.lock\";\n return {\n kind: \"failed\",\n notes: `site has ${competing} but no pnpm-lock.yaml — run convert-to-pnpm first`,\n };\n }\n }\n\n // Ensure the lockfile reflects the current package.json before we ask\n // pnpm what's outdated. Without this, a desynced lockfile can produce\n // stale or empty outdated reports.\n await spawn(\"pnpm\", [\"install\"], { cwd: site.path, streaming: true });\n\n const outdated = await spawn(\n \"pnpm\",\n [\"outdated\", \"--json\", ...outdatedFlagsForGroup(group)],\n { cwd: site.path },\n );\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(outdated.stdout || \"{}\") as Record<string, unknown>;\n } catch {\n parsed = {};\n }\n if (Object.keys(parsed).length === 0) {\n return { kind: \"noop\", notes: `pnpm outdated reported nothing for group=${group}` };\n }\n return { kind: \"apply\", plan: { group } };\n },\n apply: async ({ group: g }, { commit, cwd }) => {\n // Stream pnpm up's output so long-running upgrades don't look frozen.\n await spawn(\"pnpm\", [\"up\", ...upFlagsForGroup(g)], { cwd, streaming: true });\n await commit(`chore(deps): bump dependencies (${g})`);\n return { kind: \"ok\" };\n },\n });\n}\n","import { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { readPackageJson } from \"../../util/pkg.js\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\nimport { bumpToSvelte5Versions } from \"./step-bump-versions.js\";\nimport { migrateSvelteConfig } from \"./step-svelte-config.js\";\nimport { runSvelteMigrate } from \"./step-svelte-migrate.js\";\nimport { upgradeTailwind } from \"./step-tailwind-upgrade.js\";\nimport { applyGotchaCodemods } from \"./step-gotchas.js\";\nimport { verifyMigration } from \"./step-verify.js\";\nimport { writeMigrationSummary } from \"./step-summary.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\n\nexport type UpgradeSvelte4to5Options = {\n spawn?: SpawnFn;\n};\n\nasync function alreadyOnSvelte5(cwd: string): Promise<boolean> {\n try {\n const pkg = await readPackageJson(join(cwd, \"package.json\"));\n const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;\n return !!v && /^\\^?5\\./.test(v);\n } catch {\n return false;\n }\n}\n\nexport async function upgradeSvelte4to5(\n site: Site,\n opts: UpgradeSvelte4to5Options = {},\n): Promise<RecipeResult> {\n const spawn = opts.spawn ?? defaultSpawn;\n\n return withRecipe<true>({\n name: \"svelte-4-to-5\",\n site,\n plan: async () => {\n if (await alreadyOnSvelte5(site.path)) {\n return { kind: \"noop\", notes: \"site already declares svelte ^5.x\" };\n }\n return { kind: \"apply\", plan: true };\n },\n apply: async (_plan, { commit, cwd }) => {\n const bumped = await bumpToSvelte5Versions(cwd);\n if (bumped) {\n await commit(\"chore(svelte5): bump svelte/kit/vite/vite-plugin-svelte\");\n }\n\n const configChanged = await migrateSvelteConfig(cwd);\n if (configChanged) {\n await commit(\"refactor(svelte5): migrate svelte.config.js (drop vitePreprocess)\");\n }\n\n const migrate = await runSvelteMigrate(cwd, spawn);\n if (migrate.ran) {\n await commit(\"refactor(svelte5): run official svelte-migrate codemod\");\n }\n\n const tw = await upgradeTailwind(cwd, spawn);\n if (tw.ran) {\n await commit(\"chore(svelte5): tailwindcss 3 → 4 upgrade\");\n }\n\n const codemods = await applyGotchaCodemods(cwd);\n if (codemods.filesChanged > 0) {\n await commit(`refactor(svelte5): apply gotcha codemods (${codemods.filesChanged} files)`);\n }\n\n await verifyMigration(cwd, spawn);\n await commit(\"chore(svelte5): pnpm install + check\");\n\n await writeMigrationSummary({\n cwd,\n filesChangedByCodemods: codemods.filesChanged,\n svelteMigrateRan: migrate.ran,\n tailwindUpgraded: tw.ran,\n });\n await commit(\"docs(svelte5): add MIGRATION_SVELTE_5.md summary\");\n\n return { kind: \"ok\" };\n },\n });\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\n\nexport type PackageJsonLike = {\n name?: string;\n version?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n [key: string]: unknown;\n};\n\nexport async function readPackageJson(path: string): Promise<PackageJsonLike> {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as PackageJsonLike;\n}\n\n/** Sniff the indent style (tab vs 2 vs 4 vs N spaces) from existing package.json\n * content by looking at the first indented `\"key\"` line. Defaults to two spaces. */\nfunction detectIndentFromContent(raw: string): string {\n const match = raw.match(/\\n([ \\t]+)\"/);\n return match ? (match[1] ?? \" \") : \" \";\n}\n\nexport async function writePackageJson(path: string, pkg: PackageJsonLike): Promise<void> {\n let indent = \" \";\n try {\n const existing = await readFile(path, \"utf-8\");\n indent = detectIndentFromContent(existing);\n } catch {\n // file doesn't exist yet — first write — keep the 2-space default\n }\n const content = JSON.stringify(pkg, null, indent) + \"\\n\";\n await writeFile(path, content, \"utf-8\");\n}\n\nexport type BumpDepMode =\n | \"ensure\" // default: add to devDependencies if missing\n | \"bump-only\"; // never add; only update existing entries\n\nexport type BumpDepOptions = {\n mode?: BumpDepMode;\n};\n\nexport function bumpDep(\n pkg: PackageJsonLike,\n name: string,\n version: string,\n opts: BumpDepOptions = {},\n): PackageJsonLike {\n const mode = opts.mode ?? \"ensure\";\n\n const next: PackageJsonLike = {\n ...pkg,\n };\n\n if (pkg.dependencies) {\n next.dependencies = { ...pkg.dependencies };\n }\n if (pkg.devDependencies) {\n next.devDependencies = { ...pkg.devDependencies };\n }\n\n if (next.dependencies && name in next.dependencies) {\n if (next.dependencies[name] === version) return pkg;\n next.dependencies[name] = version;\n return next;\n }\n if (next.devDependencies && name in next.devDependencies) {\n if (next.devDependencies[name] === version) return pkg;\n next.devDependencies[name] = version;\n return next;\n }\n // Not present in either map. bump-only leaves the pkg alone so recipes\n // can express \"raise the floor on packages this site already uses\" without\n // also installing every related dep across the fleet.\n if (mode === \"bump-only\") return pkg;\n next.devDependencies = { ...(next.devDependencies ?? {}), [name]: version };\n return next;\n}\n","import { join } from \"node:path\";\nimport { readPackageJson, writePackageJson, bumpDep } from \"../../util/pkg.js\";\n\nconst SVELTE_5_VERSIONS: Record<string, string> = {\n svelte: \"^5.55.5\",\n \"@sveltejs/kit\": \"^2.59.0\",\n \"@sveltejs/vite-plugin-svelte\": \"^7.0.0\",\n \"@sveltejs/adapter-netlify\": \"^6.0.4\",\n \"@sveltejs/adapter-auto\": \"^7.0.0\",\n vite: \"^8.0.10\",\n \"svelte-check\": \"^4.4.7\",\n typescript: \"^6.0.3\",\n \"typescript-svelte-plugin\": \"^0.3.52\",\n};\n\nexport async function bumpToSvelte5Versions(cwd: string): Promise<boolean> {\n const pkgPath = join(cwd, \"package.json\");\n const pkg = await readPackageJson(pkgPath);\n let next = pkg;\n // bump-only: a svelte-4 site that doesn't declare e.g. adapter-netlify\n // should not get it added during the upgrade.\n for (const [name, version] of Object.entries(SVELTE_5_VERSIONS)) {\n next = bumpDep(next, name, version, { mode: \"bump-only\" });\n }\n if (next === pkg) return false;\n await writePackageJson(pkgPath, next);\n return true;\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nconst VITE_PLUGIN_PKG = \"@sveltejs/vite-plugin-svelte\";\n\n/** Match an import statement that pulls one or more named bindings from\n * `@sveltejs/vite-plugin-svelte`. Group 1 is the comma-separated name list. */\nconst IMPORT_FROM_VITE_PLUGIN = new RegExp(\n String.raw`^import\\s+\\{\\s*([^}]+?)\\s*\\}\\s+from\\s+[\"']` +\n VITE_PLUGIN_PKG.replace(/[/]/g, \"\\\\/\") +\n String.raw`[\"'];?[ \\t]*\\n`,\n \"m\",\n);\n\n/** Rewrite the import to drop only `vitePreprocess`, preserving any other\n * named bindings. If `vitePreprocess` was the sole import, the whole line\n * is removed. */\nfunction dropVitePreprocessImport(source: string): string {\n return source.replace(IMPORT_FROM_VITE_PLUGIN, (full, names: string) => {\n const remaining = names\n .split(\",\")\n .map((n) => n.trim())\n .filter((n) => n.length > 0 && n !== \"vitePreprocess\");\n if (remaining.length === 0) return \"\"; // drop entire line including its trailing newline\n return `import { ${remaining.join(\", \")} } from \"${VITE_PLUGIN_PKG}\";\\n`;\n });\n}\n\n/** Find the end of a balanced-paren call starting at `openIdx`, which must\n * point at the `(` character. Returns the index of the matching `)`, or -1\n * if unbalanced. */\nfunction findMatchingParen(source: string, openIdx: number): number {\n if (source[openIdx] !== \"(\") return -1;\n let depth = 0;\n for (let i = openIdx; i < source.length; i++) {\n const ch = source[i];\n if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n if (depth === 0) return i;\n }\n }\n return -1;\n}\n\n/** Remove a `preprocess: vitePreprocess(<anything>),?` key from a config\n * object. Handles the call with empty parens or with an options object. */\nfunction dropPreprocessKey(source: string): string {\n // Anchor on the start of the preprocess key on its own line so we don't\n // also strip whitespace / commas from neighboring keys.\n const startRe = /^(\\s*)preprocess:\\s*vitePreprocess\\(/m;\n const m = startRe.exec(source);\n if (!m) return source;\n\n const indent = m[1] ?? \"\";\n const parenOpenAbs = m.index + m[0].length - 1; // points at `(`\n const parenCloseAbs = findMatchingParen(source, parenOpenAbs);\n if (parenCloseAbs < 0) return source;\n\n // Consume an optional trailing comma and whitespace through end-of-line.\n let tailIdx = parenCloseAbs + 1;\n while (tailIdx < source.length && /[ \\t,]/.test(source[tailIdx] ?? \"\")) tailIdx++;\n if (source[tailIdx] === \"\\n\") tailIdx++;\n\n return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\\\n`), \"\");\n}\n\nexport async function migrateSvelteConfig(cwd: string): Promise<boolean> {\n const path = join(cwd, \"svelte.config.js\");\n let src: string;\n try {\n src = await readFile(path, \"utf-8\");\n } catch {\n return false;\n }\n\n let next = src;\n next = dropPreprocessKey(next);\n next = dropVitePreprocessImport(next);\n\n if (next === src) return false;\n await writeFile(path, next, \"utf-8\");\n return true;\n}\n","import { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\n\nexport async function runSvelteMigrate(\n cwd: string,\n spawn: SpawnFn = defaultSpawn,\n): Promise<{ ran: boolean; stderr: string }> {\n try {\n const { code, stderr } = await spawn(\n \"npx\",\n [\"--yes\", \"svelte-migrate\", \"svelte-5\", \"--no-install\"],\n { cwd, timeoutMs: 5 * 60_000 },\n );\n if (code !== 0) {\n return { ran: false, stderr };\n }\n return { ran: true, stderr };\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return { ran: false, stderr: \"npx unavailable\" };\n }\n throw err;\n }\n}\n","import { readPackageJson } from \"../../util/pkg.js\";\nimport { join } from \"node:path\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\n\nexport async function upgradeTailwind(\n cwd: string,\n spawn: SpawnFn = defaultSpawn,\n): Promise<{ ran: boolean; reason?: string }> {\n const pkg = await readPackageJson(join(cwd, \"package.json\"));\n const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;\n if (!tailwindVersion) return { ran: false, reason: \"tailwindcss not installed\" };\n if (/^\\^?4\\./.test(tailwindVersion)) return { ran: false, reason: \"already on tailwind 4.x\" };\n\n try {\n const { code, stderr } = await spawn(\"npx\", [\"--yes\", \"@tailwindcss/upgrade\", \"--force\"], {\n cwd,\n timeoutMs: 5 * 60_000,\n });\n if (code !== 0) return { ran: false, reason: stderr.slice(0, 200) };\n return { ran: true };\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return { ran: false, reason: \"npx unavailable\" };\n }\n throw err;\n }\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { glob } from \"tinyglobby\";\nimport { onEventToHandler } from \"./codemods/on-event-to-handler.js\";\nimport { exportLetToProps } from \"./codemods/dollar-props.js\";\nimport { removeDollarRestProps } from \"./codemods/dollar-restprops.js\";\nimport { stateEffectSyncToDerived } from \"./codemods/state-effect-sync.js\";\nimport { dollarPropsClass } from \"./codemods/dollar-props-class.js\";\nimport { legacyReactiveToRunes } from \"./codemods/legacy-reactive.js\";\n\nconst SVELTE_GLOBS = [\"src/**/*.svelte\"];\nconst IGNORE = [\"node_modules/**\", \".svelte-kit/**\", \"build/**\"];\n\ntype Codemod = (src: string) => string;\n\n// Order matters: exportLetToProps creates the $props() destructuring that\n// dollarPropsClass extends with a `class:` named prop.\nconst CODEMODS: Codemod[] = [\n onEventToHandler,\n exportLetToProps,\n removeDollarRestProps,\n stateEffectSyncToDerived,\n dollarPropsClass,\n legacyReactiveToRunes,\n];\n\nexport type CodemodChange = { rel: string; after: string };\n\nexport async function planGotchaCodemods(cwd: string): Promise<CodemodChange[]> {\n const changes: CodemodChange[] = [];\n const relPaths = await glob(SVELTE_GLOBS, { cwd, ignore: IGNORE, absolute: false });\n for (const rel of relPaths) {\n const path = join(cwd, rel);\n const before = await readFile(path, \"utf-8\");\n const after = CODEMODS.reduce((s, fn) => fn(s), before);\n if (after !== before) changes.push({ rel, after });\n }\n return changes;\n}\n\nexport async function applyGotchaCodemods(cwd: string): Promise<{ filesChanged: number }> {\n const changes = await planGotchaCodemods(cwd);\n for (const c of changes) {\n await writeFile(join(cwd, c.rel), c.after, \"utf-8\");\n }\n return { filesChanged: changes.length };\n}\n","const SCRIPT_BLOCK = /<script\\b[^>]*>[\\s\\S]*?<\\/script>/g;\nconst SIMPLE_ON_EVENT = /\\bon:([a-z]+)(?=\\s*=)/g;\nconst MODIFIER_EVENT = /\\bon:[a-z]+\\|[a-zA-Z]+(?=\\s*=)/g;\n\n/** Svelte 5 removed event modifier syntax (`on:click|preventDefault={fn}`).\n * The rewrite is non-trivial — the modifier behavior must be inlined into\n * the handler body — so this codemod doesn't attempt it automatically.\n * Instead it inserts a `@migration-task` marker immediately above each\n * offending element so the user gets a visible audit trail rather than\n * a silent build error from the Svelte 5 compiler. */\nfunction flagEventModifiers(source: string): string {\n const insertions: Array<{ tagStart: number; indent: string; modifier: string }> = [];\n let m: RegExpExecArray | null;\n MODIFIER_EVENT.lastIndex = 0;\n while ((m = MODIFIER_EVENT.exec(source)) !== null) {\n const tagStart = source.lastIndexOf(\"<\", m.index);\n if (tagStart === -1) continue;\n\n // Idempotency: if the line immediately above the tag already carries an\n // @migration-task marker for this site, don't double-insert on re-run.\n const prevLineEnd = tagStart - 1;\n if (prevLineEnd >= 0) {\n const prevLineStart = source.lastIndexOf(\"\\n\", prevLineEnd - 1) + 1;\n const prevLine = source.slice(prevLineStart, prevLineEnd + 1);\n if (/<!--\\s*@migration-task/.test(prevLine)) continue;\n }\n\n const lineStart = source.lastIndexOf(\"\\n\", tagStart - 1) + 1;\n const indent = source.slice(lineStart, tagStart);\n const safeIndent = /^[ \\t]*$/.test(indent) ? indent : \"\";\n insertions.push({ tagStart, indent: safeIndent, modifier: m[0] });\n }\n\n // Apply back-to-front so earlier insertion offsets stay valid.\n let out = source;\n for (let i = insertions.length - 1; i >= 0; i--) {\n const { tagStart, indent, modifier } = insertions[i]!;\n const comment = `<!-- @migration-task: Svelte 5 removed event modifier syntax (\\`${modifier}\\`). Rewrite inline, e.g. onclick={(e) => { e.preventDefault(); ... }}. -->\\n${indent}`;\n out = out.slice(0, tagStart) + comment + out.slice(tagStart);\n }\n return out;\n}\n\nexport function onEventToHandler(source: string): string {\n const masked: string[] = [];\n const placeholder = (i: number): string => ` SCRIPT_${i} `;\n const intermediate = source.replace(SCRIPT_BLOCK, (match) => {\n masked.push(match);\n return placeholder(masked.length - 1);\n });\n\n let processed = intermediate.replace(SIMPLE_ON_EVENT, (_full, name: string) => `on${name}`);\n processed = flagEventModifiers(processed);\n\n let out = processed;\n masked.forEach((blk, i) => {\n out = out.replace(placeholder(i), blk);\n });\n\n return out;\n}\n","const SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/;\nconst EXPORT_LET = /^\\s*export\\s+let\\s+(\\w+)\\s*(?::\\s*([^=;\\n]+))?\\s*(?:=\\s*([^;\\n]+))?;?\\s*$/gm;\n\ntype Prop = { name: string; type?: string | undefined; defaultExpr?: string | undefined };\n\nfunction transformScript(scriptBody: string, isTs: boolean): { body: string; changed: boolean } {\n const props: Prop[] = [];\n const cleaned = scriptBody.replace(\n EXPORT_LET,\n (_full, name: string, type?: string, defaultExpr?: string) => {\n props.push({\n name,\n type: type?.trim(),\n defaultExpr: defaultExpr?.trim(),\n });\n return \"\";\n },\n );\n if (props.length === 0) return { body: scriptBody, changed: false };\n\n const destructured = props\n .map((p) => (p.defaultExpr ? `${p.name} = ${p.defaultExpr}` : p.name))\n .join(\", \");\n\n let decl: string;\n if (isTs) {\n const typeSig = props\n .map((p) => {\n const optional = p.defaultExpr ? \"?\" : \"\";\n return `${p.name}${optional}: ${p.type ?? \"unknown\"}`;\n })\n .join(\"; \");\n decl = ` let { ${destructured} }: { ${typeSig} } = $props();`;\n } else {\n decl = ` let { ${destructured} } = $props();`;\n }\n\n const next = cleaned.replace(/^(\\s*)/, (m) => `${m}${decl}\\n`);\n return { body: next, changed: true };\n}\n\nexport function exportLetToProps(source: string): string {\n const match = source.match(SCRIPT_BLOCK);\n if (!match) return source;\n const attrs = match[1] ?? \"\";\n const inner = match[2] ?? \"\";\n const isTs = /\\blang=[\"']ts[\"']/.test(attrs);\n const { body, changed } = transformScript(inner, isTs);\n if (!changed) return source;\n return source.replace(SCRIPT_BLOCK, (full) => full.replace(inner, body));\n}\n","/** Find the index of the closing quote for a string literal that opens at\n * `openIdx`. Handles backslash escapes. Returns -1 if the string is\n * unterminated.\n *\n * Treats backtick template literals the same as `'…'` / `\"…\"` — the\n * closing backtick terminates. Callers needing precise `${…}` interpolation\n * handling will need a real parser; this helper is intentionally simple\n * and good enough for the codemod-grade string masking we do today. */\nexport function findStringEnd(source: string, openIdx: number): number {\n const quote = source[openIdx];\n let i = openIdx + 1;\n while (i < source.length) {\n const ch = source[i];\n if (ch === \"\\\\\") {\n i += 2;\n continue;\n }\n if (ch === quote) return i;\n i++;\n }\n return -1;\n}\n","/** Locate `interface $$Props {` declarations and remove them, including\n * the matching closing `}` even if the body has nested braces or spans\n * multiple lines. Regex alone can't do balanced-brace matching, so we\n * walk the string manually. */\nfunction removeInterfaceBlock(source: string): string {\n const re = /^\\s*interface\\s+\\$\\$Props\\s*\\{/m;\n let out = source;\n while (true) {\n const match = re.exec(out);\n if (!match) return out;\n\n const openBraceIdx = match.index + match[0].length - 1;\n let depth = 1;\n let i = openBraceIdx + 1;\n while (i < out.length && depth > 0) {\n const ch = out[i];\n if (ch === \"{\") depth++;\n else if (ch === \"}\") depth--;\n i++;\n }\n if (depth !== 0) return out; // unbalanced; bail rather than corrupt\n\n // Consume trailing whitespace through end-of-line.\n let endIdx = i;\n while (endIdx < out.length && /[ \\t]/.test(out[endIdx] ?? \"\")) endIdx++;\n if (out[endIdx] === \"\\n\") endIdx++;\n\n out = out.slice(0, match.index) + out.slice(endIdx);\n }\n}\n\nimport { findStringEnd } from \"../../../util/svelte-source.js\";\n\n/** Mask every `'…'`, `\"…\"`, and template literal in `source` with a placeholder\n * so subsequent regex passes can rewrite identifiers without corrupting string\n * contents. Returns the masked body and a function to restore originals. */\nfunction maskStringLiterals(source: string): {\n masked: string;\n restore: (s: string) => string;\n} {\n const strings: string[] = [];\n let out = \"\";\n let i = 0;\n while (i < source.length) {\n const ch = source[i];\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n const closeIdx = findStringEnd(source, i);\n if (closeIdx === -1) {\n out += source.slice(i);\n break;\n }\n const literal = source.slice(i, closeIdx + 1);\n out += `__RDMNT_STR_${strings.length}__`;\n strings.push(literal);\n i = closeIdx + 1;\n } else {\n out += ch;\n i++;\n }\n }\n return {\n masked: out,\n restore: (s) => s.replace(/__RDMNT_STR_(\\d+)__/g, (_full, idx) => strings[Number(idx)] ?? \"\"),\n };\n}\n\nconst PROPS_DECL = /let\\s*\\{([^}]*)\\}\\s*(?::\\s*\\{([^}]*)\\})?\\s*=\\s*\\$props\\(\\)\\s*;?/;\n\n/** If the script declares `let { … } = $props();` (with or without an inline\n * type annotation) and doesn't already collect `...rest`, inject it. For TS,\n * widen the inline type with an `[key: string]: unknown` index signature so\n * the rest binding actually captures excess attributes (without the widening,\n * TS infers `rest` as `{}` and the spread forwards nothing). */\nfunction injectRestIntoProps(scriptBody: string): string {\n const match = scriptBody.match(PROPS_DECL);\n if (!match) return scriptBody;\n const destructured = match[1] ?? \"\";\n if (/\\.\\.\\.\\s*\\w+/.test(destructured)) return scriptBody; // already has rest\n\n // Strip any trailing comma left over from a multi-line destructuring shape\n // (e.g. `{ foo, bar, }`). Without this, the template literal below emits\n // `bar,, ...rest` — invalid syntax that the codemod was happily committing\n // (regression: caltex's Accordian.svelte, 2026-05-27).\n const trimmed = destructured.trim().replace(/,\\s*$/, \"\");\n const newDestructured = trimmed === \"\" ? \" ...rest \" : ` ${trimmed}, ...rest `;\n\n let replacement: string;\n if (match[2] !== undefined) {\n const typeBody = match[2];\n const hasIndexSig = /\\[\\s*key\\s*:\\s*string\\s*\\]\\s*:/.test(typeBody);\n const newTypeBody = hasIndexSig\n ? typeBody\n : `${typeBody.trimEnd().replace(/;?\\s*$/, \"\")}; [key: string]: unknown `;\n replacement = `let {${newDestructured}}: {${newTypeBody}} = $props();`;\n } else {\n replacement = `let {${newDestructured}} = $props();`;\n }\n return scriptBody.replace(PROPS_DECL, replacement);\n}\n\nconst SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/;\nconst HAS_PROPS_CALL = /\\$props\\(\\s*\\)/;\n\nexport function removeDollarRestProps(source: string): string {\n const next = removeInterfaceBlock(source);\n\n const scriptMatch = next.match(SCRIPT_BLOCK);\n if (!scriptMatch) return next;\n if (!HAS_PROPS_CALL.test(scriptMatch[2] ?? \"\")) {\n // No $props() in this script — refuse to rewrite $$restProps anywhere, since\n // doing so would emit references to an undeclared identifier. The user sees\n // the original $$restProps and a clear Svelte 5 build error to migrate by hand.\n return next;\n }\n\n const scriptInner = scriptMatch[2] ?? \"\";\n const { masked, restore } = maskStringLiterals(scriptInner);\n let processed = injectRestIntoProps(masked);\n processed = processed.replace(/\\$\\$restProps/g, \"rest\");\n const restoredInner = restore(processed);\n\n // Use a function callback so `$$` in the restored script body isn't\n // interpreted as the `$` substitution pattern by String.prototype.replace.\n const newScriptBlock = scriptMatch[0].replace(scriptInner, () => restoredInner);\n const before = next.slice(0, scriptMatch.index!);\n const after = next.slice(scriptMatch.index! + scriptMatch[0].length);\n\n // Template (outside script) gets a plain swap. Template attribute strings\n // containing the literal text \"$$restProps\" are vanishingly rare in practice;\n // accept the limitation rather than parse the whole template.\n return (\n before.replace(/\\$\\$restProps/g, \"rest\") +\n newScriptBlock +\n after.replace(/\\$\\$restProps/g, \"rest\")\n );\n}\n","/**\n * Collapses the \"manual sync state with prop\" anti-pattern into `$derived`.\n *\n * Input:\n * let content = $state(data.page.data);\n * $effect(() => { data; content = data.page.data });\n *\n * Output:\n * let content = $derived(data.page.data);\n *\n * Only transforms when the `$state(...)` initializer expression matches the\n * effect's right-hand assignment exactly (after trim). Intervening statements\n * between the `let` and the `$effect` block prevent the match — keeps the\n * codemod conservative.\n *\n * Triggered by Svelte 5's `state_referenced_locally` warning, which fires\n * whenever a local `let X = $state(prop.expr)` captures a prop reference\n * only at init time.\n */\n// `;?` before the closing `}` so the multi-line effect form matches:\n// $effect(() => {\n// data;\n// content = data.page.data;\n// });\n// as well as the single-line form: $effect(() => { data; content = data.page.data })\nconst PATTERN =\n /let\\s+(\\w+)\\s*=\\s*\\$state\\(\\s*([^)]+?)\\s*\\)\\s*;[ \\t\\r\\n]*\\$effect\\(\\s*\\(\\s*\\)\\s*=>\\s*\\{\\s*\\w+\\s*;\\s*\\1\\s*=\\s*([^;}]+?)\\s*;?\\s*\\}\\s*\\)\\s*;?/g;\n\nexport function stateEffectSyncToDerived(source: string): string {\n return source.replace(PATTERN, (full, name: string, initExpr: string, effectExpr: string) => {\n if (initExpr.trim() !== effectExpr.trim()) return full;\n return `let ${name} = $derived(${initExpr.trim()});`;\n });\n}\n","/**\n * Converts the legacy `$$props.class` pattern (passing extra HTML class from\n * a parent component) to a Svelte 5 named-prop destructuring.\n *\n * Input:\n * <script lang=\"ts\">\n * let { foo }: { foo?: string } = $props();\n * </script>\n * <div class=\"other {$$props.class || ''}\">x</div>\n *\n * Output:\n * <script lang=\"ts\">\n * let { foo, class: className = \"\" }: { foo?: string; class?: string } = $props();\n * </script>\n * <div class=\"other {className || ''}\">x</div>\n *\n * Triggered by Svelte 5 build errors:\n * \"Cannot use `$$props` in runes mode\" (svelte.dev/e/legacy_props_invalid)\n *\n * The original svelte-migrate tool flagged this with a `@migration-task`\n * comment because it couldn't safely combine `$$props` with already-named\n * props. We can: `class` is the dominant case across the reddoor fleet,\n * so we destructure it as `class: className = \"\"` (renamed because `class`\n * is a JS reserved word as a bare binding) and rewrite template references.\n *\n * Conservative: only transforms files that have BOTH a template\n * `$$props.class` reference AND an existing `$props()` destructuring.\n * Files using `$$props.class` without a `$props()` declaration are left\n * for the `exportLetToProps` codemod to handle in a prior pass.\n */\n// Note: lazy `[\\s\\S]*?` (not `[^}]*`) so default values containing braces\n// — `() => {}`, `{ foo: 1 }`, etc. — don't truncate the match early.\nconst PROPS_DESTRUCTURE = /let\\s*\\{([\\s\\S]*?)\\}(\\s*:\\s*\\{([\\s\\S]*?)\\})?\\s*=\\s*\\$props\\(\\)/;\n// Two regexes: a stateless one for \"does this string contain $$props.class?\"\n// existence checks, and the /g one for the iterating template rewrite. Mixing\n// .test() and .replace() on the same /g regex makes lastIndex management\n// fragile — easy to forget the reset on a future edit.\nconst HAS_DOLLAR_PROPS_CLASS = /\\$\\$props\\.class\\b/;\nconst DOLLAR_PROPS_CLASS_GLOBAL = /\\$\\$props\\.class\\b/g;\nconst DOLLAR_PROPS_ANY = /\\$\\$props\\b/;\nconst SCRIPT_BLOCK = /<script\\b[^>]*>[\\s\\S]*?<\\/script>/g;\nconst MIGRATION_TASK = /^<!--\\s*@migration-task[\\s\\S]*?-->\\s*\\n?/gm;\nconst IDENT = \"className\";\n\nfunction maskScripts(source: string): { masked: string; blocks: string[] } {\n const blocks: string[] = [];\n const masked = source.replace(SCRIPT_BLOCK, (m) => {\n blocks.push(m);\n return `__SCRIPT_${blocks.length - 1}__`;\n });\n return { masked, blocks };\n}\n\nfunction restoreScripts(masked: string, blocks: string[]): string {\n let out = masked;\n blocks.forEach((blk, i) => {\n out = out.replace(`__SCRIPT_${i}__`, blk);\n });\n return out;\n}\n\nexport function dollarPropsClass(source: string): string {\n // Bail early if the template doesn't reference $$props.class\n const { masked } = maskScripts(source);\n if (!HAS_DOLLAR_PROPS_CLASS.test(masked)) return source;\n\n // Bail if there's no $props() destructuring to extend\n if (!PROPS_DESTRUCTURE.test(source)) return source;\n\n let updated = source.replace(PROPS_DESTRUCTURE, (full, body, typeAnno, typeBody) => {\n // Already migrated (someone added class manually)\n if (/\\bclass\\s*:/.test(body as string)) return full;\n\n const cleanBody = (body as string).trim().replace(/,\\s*$/, \"\").trim();\n const newBody = cleanBody ? `${cleanBody}, class: ${IDENT} = \"\"` : `class: ${IDENT} = \"\"`;\n\n if (typeAnno) {\n const cleanType = ((typeBody as string) ?? \"\").trim().replace(/;\\s*$/, \"\").trim();\n const newType = cleanType ? `${cleanType}; class?: string` : `class?: string`;\n return `let { ${newBody} }: { ${newType} } = $props()`;\n }\n return `let { ${newBody} } = $props()`;\n });\n\n // Replace $$props.class in template only (re-mask after destructuring update)\n const reMasked = maskScripts(updated);\n const templateRewritten = reMasked.masked.replace(DOLLAR_PROPS_CLASS_GLOBAL, IDENT);\n updated = restoreScripts(templateRewritten, reMasked.blocks);\n\n // Strip @migration-task comments if no $$props references remain anywhere\n // EXCEPT inside those very comments. Strip-then-check, restore if still dirty.\n const stripped = updated.replace(MIGRATION_TASK, \"\");\n if (!DOLLAR_PROPS_ANY.test(stripped)) {\n updated = stripped;\n }\n\n return updated;\n}\n","/**\n * Converts Svelte 4 `$:` reactive statements to Svelte 5 runes.\n *\n * - `$: var = expr;` → `let var = $derived(expr);`\n * - `$: { body }` → `$effect(() => { body });`\n *\n * Triggered by:\n * \"`$:` is not allowed in runes mode, use `$derived` or `$effect` instead\"\n * (svelte.dev/e/legacy_reactive_statement_invalid)\n *\n * Block patterns become `$effect` rather than per-variable `$derived` calls\n * because the block typically mutates multiple already-declared `let`\n * variables with conditional logic — too contextual for a safe automatic\n * decomposition into discrete derived values. The user can refine each\n * `$effect` into idiomatic `$derived` calls afterward if desired.\n *\n * Scoped to `<script>` content only — `$:` in template/style text is left\n * alone (it would only ever appear there as literal text anyway).\n */\nimport { findStringEnd } from \"../../../util/svelte-source.js\";\n\nconst SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/g;\nconst SIMPLE_REACTIVE = /^([ \\t]*)\\$:\\s*(\\w+)\\s*=\\s*([^;\\n]+);?[ \\t]*$/gm;\nconst BLOCK_REACTIVE_HEAD = /(^|\\n)([ \\t]*)\\$:\\s*\\{/g;\n\nfunction findMatchingClose(source: string, openIdx: number): number {\n let depth = 0;\n let i = openIdx;\n while (i < source.length) {\n const ch = source[i];\n // Skip over string literals so braces inside strings don't fool the counter.\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n const closeStr = findStringEnd(source, i);\n if (closeStr === -1) return -1;\n i = closeStr + 1;\n continue;\n }\n // Skip over comments so braces inside `// }` or `/* } */` don't fool the\n // counter. Regression: the old version silently corrupted source (depth\n // went off, real closing brace mis-matched) on inputs like `$: { // } ... }`.\n // The corrupted output still compiles in Svelte 5 — no parser to scream.\n if (ch === \"/\") {\n const next = source[i + 1];\n if (next === \"/\") {\n const eol = source.indexOf(\"\\n\", i + 2);\n i = eol === -1 ? source.length : eol; // step onto newline; outer loop handles it\n continue;\n }\n if (next === \"*\") {\n const end = source.indexOf(\"*/\", i + 2);\n if (end === -1) return -1; // unterminated block comment — bail rather than corrupt\n i = end + 2;\n continue;\n }\n }\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n depth--;\n if (depth === 0) return i;\n }\n i++;\n }\n return -1;\n}\n\n/** Flag each converted `$effect` block for manual review. The conversion is\n * syntactically safe (compiles), but if any of the locals the block mutates\n * was declared as plain `let` (not `$state`), the `$effect` runs once on\n * mount and never again — code silently loses its reactivity. We can't\n * detect that automatically (it would require scope analysis on the\n * declaration sites), so we leave a breadcrumb for the human reviewer. */\nconst MIGRATION_MARKER =\n \"// @migration-task: $effect won't trigger UI updates on plain `let` bindings — refine mutated locals to $state or split into per-variable $derived.\";\n\nfunction transformBlocks(body: string): string {\n const out: string[] = [];\n let last = 0;\n BLOCK_REACTIVE_HEAD.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = BLOCK_REACTIVE_HEAD.exec(body)) !== null) {\n const leadingNewline = m[1] ?? \"\";\n const indent = m[2] ?? \"\";\n const headEnd = m.index + m[0].length; // position just after `{`\n const openBraceIdx = headEnd - 1;\n const closeBraceIdx = findMatchingClose(body, openBraceIdx);\n if (closeBraceIdx === -1) continue;\n out.push(body.slice(last, m.index));\n out.push(leadingNewline);\n const blockBody = body.slice(openBraceIdx + 1, closeBraceIdx);\n out.push(`${indent}${MIGRATION_MARKER}\\n`);\n out.push(`${indent}$effect(() => {${blockBody}});`);\n last = closeBraceIdx + 1;\n BLOCK_REACTIVE_HEAD.lastIndex = last;\n }\n out.push(body.slice(last));\n return out.join(\"\");\n}\n\nfunction transformSimple(body: string): string {\n return body.replace(SIMPLE_REACTIVE, (_full, indent: string, name: string, expr: string) => {\n return `${indent}let ${name} = $derived(${expr.trim()});`;\n });\n}\n\nexport function legacyReactiveToRunes(source: string): string {\n return source.replace(SCRIPT_BLOCK, (full, _attrs: string, body: string) => {\n // Blocks first so an outer `$: { ... }` containing nothing matchable\n // for the simple pass still gets wrapped. Order doesn't matter for the\n // patterns currently in the fleet but keeps the codemod robust to future\n // shapes.\n let next = transformBlocks(body);\n next = transformSimple(next);\n if (next === body) return full;\n return full.replace(body, next);\n });\n}\n","import { defaultSpawn, type SpawnFn, type SpawnResult } from \"../../audits/util/spawn.js\";\n\nexport type VerifyResult = {\n install: SpawnResult | { skipped: true };\n check: SpawnResult | { skipped: true };\n};\n\nexport async function verifyMigration(\n cwd: string,\n spawn: SpawnFn = defaultSpawn,\n): Promise<VerifyResult> {\n let install: VerifyResult[\"install\"];\n try {\n install = await spawn(\"pnpm\", [\"install\"], { cwd, timeoutMs: 10 * 60_000 });\n } catch {\n install = { skipped: true };\n }\n\n let check: VerifyResult[\"check\"];\n try {\n check = await spawn(\"pnpm\", [\"run\", \"check\"], { cwd, timeoutMs: 5 * 60_000 });\n } catch {\n check = { skipped: true };\n }\n\n return { install, check };\n}\n","import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport type SummaryInput = {\n cwd: string;\n filesChangedByCodemods: number;\n svelteMigrateRan: boolean;\n tailwindUpgraded: boolean;\n};\n\nexport async function writeMigrationSummary(input: SummaryInput): Promise<string> {\n const lines = [\n `# Svelte 4 → 5 migration summary`,\n ``,\n `Generated by @reddoorla/maintenance.`,\n ``,\n `- svelte-migrate run: ${input.svelteMigrateRan ? \"yes\" : \"no\"}`,\n `- @tailwindcss/upgrade run: ${input.tailwindUpgraded ? \"yes\" : \"no\"}`,\n `- .svelte files touched by gotcha codemods: ${input.filesChangedByCodemods}`,\n ``,\n `Next steps:`,\n `- Run \\`pnpm run check\\` and resolve any remaining warnings.`,\n `- Spot-check rune migrations in components that use \\`reactive\\` statements.`,\n `- Verify Playwright a11y tests still pass.`,\n ];\n const content = lines.join(\"\\n\") + \"\\n\";\n const path = join(input.cwd, \"MIGRATION_SVELTE_5.md\");\n await writeFile(path, content, \"utf-8\");\n return path;\n}\n","import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../types.js\";\nimport { planGotchaCodemods } from \"./svelte-5/step-gotchas.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\ntype Change = { rel: string; after: string };\n\n/**\n * Standalone codemod pass for sites already on Svelte 5.\n *\n * Applies the same gotcha codemods the full `svelte-4-to-5` migration runs,\n * but skips the version checks and migration steps — useful when Svelte 5\n * surfaces new strictness warnings post-upgrade (e.g. `state_referenced_locally`)\n * and the fleet needs a clean re-application.\n *\n * Plans changes in memory first; only creates the branch + writes + commits\n * when there is something to apply. Re-runs against a clean tree are noop.\n */\nexport async function svelteCodemods(site: Site): Promise<RecipeResult> {\n return withRecipe<Change[]>({\n name: \"svelte-codemods\",\n site,\n plan: async () => {\n const changes = await planGotchaCodemods(site.path);\n if (changes.length === 0) {\n return { kind: \"noop\", notes: \"no codemod targets matched\" };\n }\n return { kind: \"apply\", plan: changes };\n },\n apply: async (changes, { commit, cwd }) => {\n for (const c of changes) {\n await writeFile(join(cwd, c.rel), c.after, \"utf-8\");\n }\n await commit(`refactor(svelte5): apply codemods (${changes.length} files)`);\n return { kind: \"ok\" };\n },\n });\n}\n","import { rm, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../types.js\";\nimport { readPackageJson, writePackageJson, type PackageJsonLike } from \"../util/pkg.js\";\nimport { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\nimport { rewriteScriptsForPnpm } from \"./convert-to-pnpm/script-rewrites.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type ConvertToPnpmOptions = {\n spawn?: SpawnFn;\n /** Version string written into package.json's `packageManager` field.\n * Defaults to the version baked into this package's own pnpm setup. */\n pnpmVersion?: string;\n};\n\n/** Pinned default — matches the `packageManager` field of this package\n * (kept in sync with package.json). Sites can override per-recipe. */\nconst DEFAULT_PNPM_VERSION = \"10.33.1\";\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\ntype Plan = { hasNpmLock: boolean; hasYarnLock: boolean };\n\nexport async function convertToPnpm(\n site: Site,\n opts: ConvertToPnpmOptions = {},\n): Promise<RecipeResult> {\n const spawn = opts.spawn ?? defaultSpawn;\n const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;\n\n const pnpmLockPath = join(site.path, \"pnpm-lock.yaml\");\n const npmLockPath = join(site.path, \"package-lock.json\");\n const yarnLockPath = join(site.path, \"yarn.lock\");\n\n return withRecipe<Plan>({\n name: \"convert-to-pnpm\",\n site,\n plan: async () => {\n if (await exists(pnpmLockPath)) {\n return { kind: \"noop\", notes: \"site already has pnpm-lock.yaml\" };\n }\n const hasNpmLock = await exists(npmLockPath);\n const hasYarnLock = await exists(yarnLockPath);\n if (!hasNpmLock && !hasYarnLock) {\n return {\n kind: \"noop\",\n notes: \"no convertible lockfile (package-lock.json or yarn.lock) at site root\",\n };\n }\n return { kind: \"apply\", plan: { hasNpmLock, hasYarnLock } };\n },\n apply: async ({ hasNpmLock, hasYarnLock }, { commit, cwd }) => {\n // Step 1: remove the npm/yarn lockfile(s).\n if (hasNpmLock) await rm(npmLockPath, { force: true });\n if (hasYarnLock) await rm(yarnLockPath, { force: true });\n const sourceLock = hasNpmLock ? \"package-lock.json\" : \"yarn.lock\";\n await commit(`chore(pnpm): remove ${sourceLock}`);\n\n // Step 2: pin packageManager + rewrite scripts (single commit — they\n // both touch package.json).\n const pkgPath = join(cwd, \"package.json\");\n const pkg = await readPackageJson(pkgPath);\n const next: PackageJsonLike = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };\n\n if (pkg.scripts && typeof pkg.scripts === \"object\") {\n const { scripts: rewritten, changedCount } = rewriteScriptsForPnpm(\n pkg.scripts as Record<string, string>,\n );\n if (changedCount > 0) {\n next.scripts = rewritten;\n }\n }\n\n await writePackageJson(pkgPath, next);\n await commit(\"chore(pnpm): pin packageManager + rewrite npm scripts\");\n\n // Step 3: remove any existing flat node_modules from a prior npm/yarn run\n // before pnpm installs. Sharing a node_modules across package managers\n // produces phantom-dep resolution issues (pnpm's nested layout disagrees\n // with what's already on disk). node_modules is gitignored on every\n // reddoor site so this doesn't dirty the tree.\n await rm(join(cwd, \"node_modules\"), { recursive: true, force: true });\n\n // Step 4: run pnpm install to materialize pnpm-lock.yaml.\n const installResult = await spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (installResult.code !== 0) {\n return { kind: \"failed\", notes: `pnpm install failed (exit ${installResult.code})` };\n }\n\n await commit(\"chore(pnpm): add pnpm-lock.yaml\");\n return { kind: \"ok\" };\n },\n });\n}\n","/**\n * Rewrite a single package.json script value to use pnpm equivalents\n * where the substitution is safe. Conservative on purpose: we only touch\n * patterns whose semantics are identical under pnpm.\n *\n * - `npm run <token>` → `pnpm run <token>` (identical behavior)\n * - `npx <token>` → `pnpm dlx <token>` (identical behavior in pnpm 7+)\n *\n * Intentionally NOT rewritten:\n * - `npm install`, `npm install <pkg>`, `npm install --save-dev <pkg>` —\n * subtle flag mapping (e.g. `--save-dev` → `-D`) and edge cases like\n * `--save-exact` / `--save-optional`. Better to leave for an operator\n * eyeball than to silently mis-translate.\n * - Hyphenated identifiers like `npm-check-updates` (word-boundary protected).\n * - `concurrently \"npm:scriptName\"` shorthand syntax — it isn't actually\n * running npm; it's a concurrently-specific script reference.\n */\nexport function rewriteScriptForPnpm(script: string): string {\n let out = script;\n // `npm run <name>` → `pnpm run <name>`. \\b before npm prevents\n // matching inside hyphenated identifiers. Lookahead `(?=\\s)` after run\n // ensures we don't match `runner`.\n out = out.replace(/\\bnpm run(?=\\s)/g, \"pnpm run\");\n // `npx ` → `pnpm dlx `. \\b before npx prevents matching `npx-something`.\n out = out.replace(/\\bnpx(?=\\s)/g, \"pnpm dlx\");\n return out;\n}\n\n/**\n * Rewrite every entry in a package.json `scripts` map. Returns the new\n * map alongside a count of scripts that were actually changed.\n */\nexport function rewriteScriptsForPnpm(scripts: Record<string, string>): {\n scripts: Record<string, string>;\n changedCount: number;\n} {\n const next: Record<string, string> = {};\n let changedCount = 0;\n for (const [name, value] of Object.entries(scripts)) {\n const rewritten = rewriteScriptForPnpm(value);\n next[name] = rewritten;\n if (rewritten !== value) changedCount++;\n }\n return { scripts: next, changedCount };\n}\n","import { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../types.js\";\nimport { readPackageJson, writePackageJson, bumpDep, type PackageJsonLike } from \"../util/pkg.js\";\nimport { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\nimport { selfCaretRange } from \"../util/self-version.js\";\nimport { baselineVersions } from \"../configs/baseline-versions.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type OnboardAudit = \"lighthouse\" | \"a11y\";\n\nexport type OnboardOptions = {\n spawn?: SpawnFn;\n /** Which audit-related deps to ensure. Defaults to all known audits. */\n audits?: OnboardAudit[];\n /** Version range to pin for @reddoorla/maintenance. Defaults to a caret\n * range against this package's own version at runtime — no manual\n * syncing required at each minor bump. */\n packageVersion?: string;\n};\n\nconst PACKAGE_NAME = \"@reddoorla/maintenance\";\n\nconst AUDIT_DEP_NAMES: Record<OnboardAudit, string[]> = {\n lighthouse: [\"@lhci/cli\"],\n a11y: [\"@playwright/test\", \"@axe-core/playwright\"],\n};\n\n/** Framework deps onboard ensures for every site, independent of which audits\n * are requested. The sync-configs svelte.config.js template does\n * `import adapter from \"@sveltejs/adapter-netlify\"`, so a site that lacks the\n * adapter declared can't build once configs are synced — onboard closes that\n * gap at the same time it adds the maintenance package. */\nconst FRAMEWORK_DEP_NAMES = [\"@sveltejs/adapter-netlify\"];\n\n/** Resolve framework dep versions from baselineVersions at module load so they\n * can't drift from the single source of truth — mirrors AUDIT_DEPS. Throws at\n * import time if a name is missing there (a programming error). */\nexport const FRAMEWORK_DEPS: Array<{ name: string; version: string }> = FRAMEWORK_DEP_NAMES.map(\n (name) => {\n const version = baselineVersions[name];\n if (!version) {\n throw new Error(\n `baseline-versions is missing framework dep \"${name}\" — add it to src/configs/baseline-versions.ts`,\n );\n }\n return { name, version };\n },\n);\n\n/** Look up each audit dep's version in baselineVersions at module load so\n * AUDIT_DEPS can't drift from the single source of truth across releases.\n * Throws at import time if baseline-versions is missing an audit dep —\n * which would be a programming error (every audit dep name above must\n * appear in baselineVersions). */\nexport const AUDIT_DEPS: Record<\n OnboardAudit,\n Array<{ name: string; version: string }>\n> = Object.fromEntries(\n (Object.entries(AUDIT_DEP_NAMES) as Array<[OnboardAudit, string[]]>).map(([audit, names]) => [\n audit,\n names.map((name) => {\n const version = baselineVersions[name];\n if (!version) {\n throw new Error(\n `baseline-versions is missing audit dep \"${name}\" — add it to src/configs/baseline-versions.ts`,\n );\n }\n return { name, version };\n }),\n ]),\n) as Record<OnboardAudit, Array<{ name: string; version: string }>>;\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction isDeclared(pkg: PackageJsonLike, name: string): boolean {\n return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);\n}\n\ntype Plan = {\n pkg: PackageJsonLike;\n toAdd: Array<{ name: string; version: string }>;\n};\n\nexport async function onboard(site: Site, opts: OnboardOptions = {}): Promise<RecipeResult> {\n const spawn = opts.spawn ?? defaultSpawn;\n const audits = opts.audits ?? ([\"lighthouse\", \"a11y\"] as OnboardAudit[]);\n const packageVersion = opts.packageVersion ?? selfCaretRange(import.meta.url);\n\n return withRecipe<Plan>({\n name: \"onboard\",\n site,\n plan: async () => {\n // Pre-flight: site must already be on pnpm. We don't auto-convert here;\n // that's the convert-to-pnpm recipe's job, and combining them would\n // hide the package-manager transition inside a bigger PR.\n if (!(await exists(join(site.path, \"pnpm-lock.yaml\")))) {\n return {\n kind: \"failed\",\n notes: \"no pnpm-lock.yaml at site root — run convert-to-pnpm first\",\n };\n }\n\n const pkgPath = join(site.path, \"package.json\");\n const pkg = await readPackageJson(pkgPath);\n\n // Determine what's missing. Anything already declared (even at a wildly\n // different version) is left alone — onboard never downgrades.\n const toAdd: Array<{ name: string; version: string }> = [];\n if (!isDeclared(pkg, PACKAGE_NAME)) {\n toAdd.push({ name: PACKAGE_NAME, version: packageVersion });\n }\n for (const dep of FRAMEWORK_DEPS) {\n if (!isDeclared(pkg, dep.name)) toAdd.push(dep);\n }\n for (const audit of audits) {\n for (const dep of AUDIT_DEPS[audit]) {\n if (!isDeclared(pkg, dep.name)) toAdd.push(dep);\n }\n }\n\n if (toAdd.length === 0) {\n return {\n kind: \"noop\",\n notes: `site already has ${PACKAGE_NAME}, framework deps, and audit deps (${audits.join(\"+\")})`,\n };\n }\n return { kind: \"apply\", plan: { pkg, toAdd } };\n },\n apply: async ({ pkg, toAdd }, { commit, cwd }) => {\n const pkgPath = join(cwd, \"package.json\");\n let next: PackageJsonLike = pkg;\n for (const dep of toAdd) {\n next = bumpDep(next, dep.name, dep.version);\n }\n await writePackageJson(pkgPath, next);\n\n // Run pnpm install so the lockfile reflects the new deps before we commit.\n // Stream output — install on a real site can take 30s+.\n const installResult = await spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (installResult.code !== 0) {\n return {\n kind: \"failed\",\n notes: `pnpm install failed (exit ${installResult.code})`,\n };\n }\n\n await commit(`chore(reddoor): onboard with ${PACKAGE_NAME} ${packageVersion}`);\n return {\n kind: \"ok\",\n notes: `Added ${toAdd.length} dep(s): ${toAdd.map((d) => d.name).join(\", \")}`,\n };\n },\n });\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Read this package's own version at runtime so recipe defaults don't go\n * stale at each minor bump.\n *\n * Pass `import.meta.url` from the calling file. Walks UP from the caller\n * looking for the first `package.json` whose `name` matches this package\n * (`@reddoorla/maintenance`). The older \"two levels up\" shortcut held for\n * `src/X/Y.ts` and `dist/cli/bin.js` (both happen to be 2 dirs deep) but\n * broke for `dist/index.js` (only 1 dir deep) — silently returned \"0.0.0\"\n * and pinned consumers to `^0.0.0`. Same bug class as the 0.10.1 bundled-\n * assets ENOENT (2026-05-27). Walk-up is robust regardless of bundling\n * layout.\n *\n * Returns \"0.0.0\" if no matching package.json is reachable (defensive\n * fallback; callers should treat that as a signal to either override\n * explicitly or fail loudly).\n */\nexport function selfPackageVersion(callerImportMetaUrl: string): string {\n try {\n let dir = dirname(fileURLToPath(callerImportMetaUrl));\n while (true) {\n const candidate = join(dir, \"package.json\");\n if (existsSync(candidate)) {\n const raw = readFileSync(candidate, \"utf-8\");\n const pkg = JSON.parse(raw) as { name?: string; version?: string };\n // Only accept OUR package.json — keep walking past random ancestor\n // package.jsons (the consumer's own, anything in node_modules) that\n // happen to sit above the bundle.\n if (pkg.name === \"@reddoorla/maintenance\") {\n return pkg.version ?? \"0.0.0\";\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return \"0.0.0\";\n dir = parent;\n }\n } catch {\n return \"0.0.0\";\n }\n}\n\n/** Caret-pinned range against this package's own version: e.g. \"^0.6.2\". */\nexport function selfCaretRange(callerImportMetaUrl: string): string {\n return `^${selfPackageVersion(callerImportMetaUrl)}`;\n}\n","import { access, mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\nimport { A11Y_FIXTURES_PAGE_RELATIVE, A11Y_FIXTURES_PAGE_TEMPLATE } from \"./template.js\";\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Writes a starter `src/routes/dev/a11y-fixtures/+page.svelte` if the route\n * doesn't already exist. The hardcoded URL in `src/configs/lighthouse.ts` +\n * `src/configs/playwright-a11y.ts` targets this path — newly-onboarded sites\n * need the route to exist for either audit to pass. Operator edits to an\n * existing page are never clobbered (noop on existing file).\n */\nexport async function a11yFixturesPage(site: Site): Promise<RecipeResult> {\n const target = join(site.path, A11Y_FIXTURES_PAGE_RELATIVE);\n return withRecipe<{ target: string }>({\n name: \"a11y-fixtures-page\",\n site,\n plan: async () => {\n if (await fileExists(target)) {\n return { kind: \"noop\", notes: `${A11Y_FIXTURES_PAGE_RELATIVE} already exists` };\n }\n return { kind: \"apply\", plan: { target } };\n },\n apply: async (planned, { commit }) => {\n await mkdir(dirname(planned.target), { recursive: true });\n await writeFile(planned.target, A11Y_FIXTURES_PAGE_TEMPLATE, \"utf-8\");\n await commit(\"feat: add /dev/a11y-fixtures starter route\");\n return { kind: \"ok\" };\n },\n });\n}\n","/** Relative path inside a site where the a11y fixtures route lives. The\n * hardcoded URL in `src/configs/lighthouse.ts` + `src/configs/playwright-a11y.ts`\n * is `/dev/a11y-fixtures`, so a SvelteKit `+page.svelte` here resolves. */\nexport const A11Y_FIXTURES_PAGE_RELATIVE = \"src/routes/dev/a11y-fixtures/+page.svelte\";\n\n/** Stub `+page.svelte` for newly-onboarded sites. Generic on purpose —\n * landmarks, heading hierarchy, and a relative link cover the axe-core +\n * lhci defaults without committing the operator to any specific fixture\n * shape. Replace with site-specific patterns over time. */\nexport const A11Y_FIXTURES_PAGE_TEMPLATE = `<svelte:head>\n <title>a11y fixtures — Reddoor</title>\n <meta\n name=\"description\"\n content=\"Reddoor accessibility fixtures — semantic landmarks, heading hierarchy, and a stable target for @lhci/cli and Playwright + axe-core coverage. Not linked from the public site.\"\n />\n</svelte:head>\n\n<main>\n <header>\n <h1>Accessibility fixtures</h1>\n <p>\n This page exists so <code>@lhci/cli</code> and Playwright + axe-core have a\n stable target with predictable a11y characteristics. It is not linked from\n the public site.\n </p>\n </header>\n\n <section aria-labelledby=\"landmarks-heading\">\n <h2 id=\"landmarks-heading\">Landmarks</h2>\n <p>\n A single <code>main</code> wraps the page; sections each declare\n <code>aria-labelledby</code> matched to their heading id so screen readers\n and axe both see a clean outline.\n </p>\n </section>\n\n <section aria-labelledby=\"links-heading\">\n <h2 id=\"links-heading\">Links</h2>\n <p>\n <a href=\"/\">Back to home</a> — relative link with descriptive visible text,\n so no <code>aria-label</code> override is needed.\n </p>\n </section>\n</main>\n`;\n","import type { AuditResult, RecipeResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { convertToPnpm } from \"./convert-to-pnpm.js\";\nimport { onboard } from \"./onboard.js\";\nimport { syncConfigs } from \"./sync-configs.js\";\nimport { svelteCodemods } from \"./svelte-codemods.js\";\nimport { a11yFixturesPage } from \"./a11y-fixtures-page/index.js\";\nimport { runAudits } from \"../audits/index.js\";\n\nexport type InitStepResult =\n | { kind: \"recipe\"; result: RecipeResult }\n | { kind: \"audit\"; results: AuditResult[] }\n | { kind: \"error\"; message: string };\n\nexport type InitStep = {\n name: string;\n run: (site: Site) => Promise<InitStepResult>;\n};\n\nexport type InitResult = {\n site: string;\n steps: Array<{ name: string; result: InitStepResult }>;\n /** True if every step ran; false if an `error` or `failed` recipe result\n * short-circuited the chain. `noop` recipes do not break completeness. */\n complete: boolean;\n};\n\nexport type InitOptions = {\n /** Override the default step list. Tests inject mocked steps; production\n * code relies on the default. */\n steps?: InitStep[];\n};\n\nfunction recipeStep(name: string, fn: (site: Site) => Promise<RecipeResult>): InitStep {\n return {\n name,\n run: async (site) => ({ kind: \"recipe\", result: await fn(site) }),\n };\n}\n\n/** convert-to-pnpm → onboard → sync-configs → svelte-codemods →\n * a11y-fixtures-page → audit. Order is deliberate — every step depends on\n * the prior one's output (pnpm before onboard's installs, onboard's deps\n * before sync-configs writes lighthouserc, fixtures page before audit\n * actually has a route to hit). */\nexport const DEFAULT_INIT_STEPS: InitStep[] = [\n recipeStep(\"convert-to-pnpm\", convertToPnpm),\n recipeStep(\"onboard\", onboard),\n recipeStep(\"sync-configs\", syncConfigs),\n recipeStep(\"svelte-codemods\", svelteCodemods),\n recipeStep(\"a11y-fixtures-page\", a11yFixturesPage),\n {\n name: \"audit\",\n run: async (site) => ({ kind: \"audit\", results: await runAudits(site) }),\n },\n];\n\n/**\n * One-shot guided onboarding. Runs the default step sequence against a\n * site, collecting per-step results into an InitResult. Each underlying\n * recipe still creates its own branch — init is a thin orchestrator, not\n * a branch-collapser; the operator ends up with one stack of branches per\n * mutated step (recipes that noop don't branch).\n *\n * Stops the chain on the first uncaught error or `failed` recipe result.\n * `noop` results are expected (e.g. running init twice) and continue the\n * chain. The final audit pass runs if no prior step errored.\n */\nexport async function init(site: Site, opts: InitOptions = {}): Promise<InitResult> {\n const steps = opts.steps ?? DEFAULT_INIT_STEPS;\n const out: Array<{ name: string; result: InitStepResult }> = [];\n\n for (const step of steps) {\n let result: InitStepResult;\n try {\n result = await step.run(site);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n out.push({ name: step.name, result: { kind: \"error\", message } });\n return { site: siteLabel(site), steps: out, complete: false };\n }\n out.push({ name: step.name, result });\n if (result.kind === \"recipe\" && result.result.status === \"failed\") {\n return { site: siteLabel(site), steps: out, complete: false };\n }\n }\n\n return { site: siteLabel(site), steps: out, complete: true };\n}\n","import type { RecipeName } from \"../types.js\";\nimport { syncConfigs, type SyncConfigsOptions } from \"./sync-configs.js\";\nimport { bumpDeps, type BumpDepsOptions } from \"./bump-deps.js\";\nimport { upgradeSvelte4to5, type UpgradeSvelte4to5Options } from \"./svelte-5/index.js\";\nimport { svelteCodemods } from \"./svelte-codemods.js\";\nimport { convertToPnpm, type ConvertToPnpmOptions } from \"./convert-to-pnpm.js\";\nimport { onboard, type OnboardOptions, type OnboardAudit } from \"./onboard.js\";\nimport { a11yFixturesPage } from \"./a11y-fixtures-page/index.js\";\nimport {\n init,\n DEFAULT_INIT_STEPS,\n type InitOptions,\n type InitResult,\n type InitStep,\n type InitStepResult,\n} from \"./init.js\";\n\nexport {\n syncConfigs,\n bumpDeps,\n upgradeSvelte4to5,\n svelteCodemods,\n convertToPnpm,\n onboard,\n a11yFixturesPage,\n init,\n DEFAULT_INIT_STEPS,\n};\nexport type {\n SyncConfigsOptions,\n BumpDepsOptions,\n UpgradeSvelte4to5Options,\n ConvertToPnpmOptions,\n OnboardOptions,\n OnboardAudit,\n InitOptions,\n InitResult,\n InitStep,\n InitStepResult,\n};\n\nexport const ALL_RECIPE_NAMES: RecipeName[] = [\n \"sync-configs\",\n \"bump-deps\",\n \"svelte-4-to-5\",\n \"svelte-codemods\",\n \"convert-to-pnpm\",\n \"onboard\",\n \"a11y-fixtures-page\",\n \"self-updating\",\n \"init\",\n];\n\nexport function isRecipeName(value: string): value is RecipeName {\n return (ALL_RECIPE_NAMES as string[]).includes(value);\n}\n","import { basename } from \"node:path\";\nimport type { InventoryProvider, Site } from \"../types.js\";\n\nexport type LocalPathOptions = {\n name?: string;\n};\n\nexport function localPath(path: string, opts: LocalPathOptions = {}): InventoryProvider {\n // `||` not `??`: an explicit empty `--name \"\"` should fall back to the path's\n // basename, not become a blank site name.\n const site: Site = { path, name: opts.name || basename(path) };\n return async () => [site];\n}\n","import { readFile } from \"node:fs/promises\";\nimport { isAbsolute } from \"node:path\";\nimport type { InventoryProvider, Site } from \"../types.js\";\nimport { isHttpUrl } from \"../util/url.js\";\n\nfunction validate(raw: unknown): Site[] {\n if (!Array.isArray(raw)) {\n throw new Error(\"inventory JSON must be an array of sites\");\n }\n return raw.map((entry, i) => {\n if (!entry || typeof entry !== \"object\") {\n throw new Error(`inventory entry ${i} is not an object`);\n }\n const e = entry as Record<string, unknown>;\n if (typeof e.path !== \"string\" || e.path.length === 0) {\n throw new Error(`inventory entry ${i} is missing required field: path`);\n }\n if (!isAbsolute(e.path)) {\n throw new Error(\n `inventory entry ${i}: path must be absolute (got \"${e.path}\"). ` +\n `Relative paths are rejected so cwd at invocation can't change which site is targeted.`,\n );\n }\n const site: Site = { path: e.path };\n if (typeof e.name === \"string\") site.name = e.name;\n if (typeof e.repoUrl === \"string\") site.repoUrl = e.repoUrl;\n // Carry gitRepo/deployedUrl like the Airtable provider does, so a JSON\n // inventory can drive checkout (clone-from-gitRepo) and deployed-URL audits.\n if (typeof e.gitRepo === \"string\") site.gitRepo = e.gitRepo;\n // Scheme-allowlist deployedUrl before it can reach Chrome/lhci (same SSRF /\n // local-file gate as the Airtable provider). A non-http(s) value is dropped\n // with a warning rather than trusted into the deployed audit.\n if (typeof e.deployedUrl === \"string\") {\n if (isHttpUrl(e.deployedUrl)) {\n site.deployedUrl = e.deployedUrl;\n } else {\n console.warn(\n `[inventory] entry ${i}: ignoring deployedUrl that is not http(s): ${JSON.stringify(e.deployedUrl)}`,\n );\n }\n }\n if (typeof e.meta === \"object\" && e.meta !== null) {\n site.meta = e.meta as Record<string, unknown>;\n }\n return site;\n });\n}\n\nexport function fromJsonFile(path: string): InventoryProvider {\n return async () => {\n const text = await readFile(path, \"utf-8\");\n let raw: unknown;\n try {\n raw = JSON.parse(text);\n } catch (e) {\n // A bare JSON.parse SyntaxError (\"Unexpected token … at position N\") names\n // neither the file nor that it's the inventory — useless to an operator\n // running a fleet command. Rethrow with the path for an actionable message,\n // preserving the original SyntaxError as `cause`.\n throw new Error(`could not parse inventory file ${path}: ${(e as Error).message}`, {\n cause: e,\n });\n }\n return validate(raw);\n };\n}\n","/**\n * True when `s` parses as an absolute URL whose scheme is `http:` or `https:`.\n *\n * The single allowlist gate for any value we hand to Chrome/Lighthouse. A\n * deployed-audit URL flows in from Airtable's `url` column (or a JSON\n * inventory's `deployedUrl`), so a `file://`/`gopher://`/`data:` value — or a\n * value pointing at an internal host — would otherwise become a local-file read\n * or SSRF when lhci drives a headless browser at it. Restricting to http(s)\n * keeps the audit to the real, network-reachable site.\n */\nexport function isHttpUrl(s: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(s);\n } catch {\n return false;\n }\n return parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n}\n","import type { FieldSet } from \"airtable\";\nimport type { AirtableBase } from \"./client.js\";\nimport type { LighthouseScores } from \"../types.js\";\n\nexport const WEBSITES_TABLE = \"Websites\";\n\nexport type Frequency = \"None\" | \"Monthly\" | \"Quarterly\" | \"Yearly\";\n\nexport type Status =\n | \"in development\"\n | \"launch period\"\n | \"maintenance\"\n | \"hosting\"\n | \"probably not our problem\"\n | \"deprecated\";\n\nexport type WebsiteRow = {\n id: string;\n name: string;\n url: string;\n status: Status | null;\n pointOfContact: string | null;\n maintenanceFreq: Frequency;\n testingFreq: Frequency;\n /** Last manually-recorded maintenance day (used as fallback when no Reports row exists). */\n maintenanceDay: string | null;\n testingDay: string | null;\n ga4PropertyId: string | null;\n /** Operator-supplied query for the Google search-presence check (e.g. the business name).\n * Null = no query set → the check is skipped for this site. */\n searchQuery: string | null;\n /** Explicit Search Console property for this site (`sc-domain:...` or `https://.../`).\n * Null = auto-resolve from the SA's visible properties by host. */\n searchConsoleProperty: string | null;\n /** GitHub repo identity as `owner/repo`. Null = no git wiring → self-update ops skip\n * (or, for local runs, fall back to the checkout's origin remote). */\n gitRepo: string | null;\n reportRecipientsTo: string | null;\n reportRecipientsCc: string | null;\n /** First attachment in the Header image field (Airtable's signed URL — fetch before expiry). */\n headerImage: { url: string; filename: string; type: string } | null;\n /** Lighthouse \"current state\" snapshot, kept fresh by `audit lighthouse --write-airtable`. */\n pScore: number | null;\n rScore: number | null;\n bpScore: number | null;\n seoScore: number | null;\n /** ISO timestamp set by `audit lighthouse --write-airtable` when scores were last refreshed. */\n lastLighthouseAuditAt: string | null;\n /** Last-known counts from non-lighthouse audits, written by\n * `audit --write-airtable`. `null` = never audited (or this audit\n * type was skipped on the last run). 0 = audited, clean. */\n a11yViolations: number | null;\n /** Declared-range drift vs the Reddoor baseline (what package.json asks for). */\n depsDrifted: number | null;\n depsMajorBehind: number | null;\n /** Real installed-version drift: deps behind the registry's latest, from the\n * committed lockfile (`pnpm outdated`). Null = not determined this run. */\n depsOutdated: number | null;\n securityVulnsCritical: number | null;\n securityVulnsHigh: number | null;\n securityVulnsModerate: number | null;\n securityVulnsLow: number | null;\n /** Per-site copy overrides (M6a). Blank → null → the DEFAULT_COPY value. */\n copyIntro: string | null;\n copyContact: string | null;\n copyFooter: string | null;\n /** Go-live timestamp, stamped when a Launch report sends (M6b). Null = not yet launched. */\n launchedAt: string | null;\n /** Optional per-site webhook (e.g. Zapier Catch Hook). When set, the ingest\n * POSTs newsletter-formType submissions here (best-effort). Blank → null. */\n newsletterWebhook: string | null;\n /** GitHub-signals sweep (slice 2a), written nightly by `github-signals --fleet`. */\n renovateFailingCis: number | null;\n defaultBranchCi: string | null; // \"passing\" | \"failing\" | \"pending\" | \"none\"\n lastCommitAt: string | null;\n githubSignalsAt: string | null;\n};\n\nexport function siteSlug(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\n/** Blank-trim-to-null: a non-string or whitespace-only value becomes null,\n * otherwise the trimmed string. */\nfunction trimToNull(raw: unknown): string | null {\n if (typeof raw !== \"string\") return null;\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n/**\n * Active sites: actively-maintained or pre-launch. Single source of truth for\n * \"is this a live site\" — the operator cockpit shows these, and the fleet\n * audit/report path runs against these. A `null` status (not-yet-active) is\n * deliberately excluded.\n */\nexport const ACTIVE_STATUSES: ReadonlySet<Status> = new Set<Status>([\n \"maintenance\",\n \"launch period\",\n]);\n\nexport function isDashboardVisible(site: WebsiteRow): boolean {\n return site.status !== null && ACTIVE_STATUSES.has(site.status);\n}\n\n// NOTE: every `f[\"...\"]` key below is a load-bearing magic string that must match\n// the live Airtable \"Websites\" column name EXACTLY — including the legacy\n// misspelling `\"maintenence freq\"`, the mixed-case `\"GA4 property ID\"`, and the\n// lowercase `\"url\"` / `\"point of contact\"`. A column rename in Airtable silently\n// returns undefined here (→ null), which degrades quietly (GA skipped, recipients\n// empty) with no error. If you rename a column, change it here too.\nexport function mapRow(rec: { id: string; fields: Record<string, unknown> }): WebsiteRow {\n const f = rec.fields;\n const attachments =\n (f[\"Header image\"] as Array<{ url: string; filename: string; type: string }> | undefined) ?? [];\n const header = attachments[0] ?? null;\n return {\n id: rec.id,\n name: String(f[\"Name\"] ?? \"\"),\n url: String(f[\"url\"] ?? \"\"),\n status: (f[\"Status\"] as Status | undefined) ?? null,\n pointOfContact: (f[\"point of contact\"] as string | undefined) ?? null,\n maintenanceFreq: ((f[\"maintenence freq\"] as string | undefined) ?? \"None\") as Frequency,\n testingFreq: ((f[\"testing freq\"] as string | undefined) ?? \"None\") as Frequency,\n maintenanceDay: (f[\"maintenance day\"] as string | undefined) ?? null,\n testingDay: (f[\"testing day\"] as string | undefined) ?? null,\n ga4PropertyId: (f[\"GA4 property ID\"] as string | undefined) ?? null,\n searchQuery: (f[\"Search query\"] as string | undefined) ?? null,\n searchConsoleProperty: (f[\"Search Console property\"] as string | undefined) ?? null,\n gitRepo: (f[\"Git repo\"] as string | undefined) ?? null,\n reportRecipientsTo: (f[\"Report recipients (To)\"] as string | undefined) ?? null,\n reportRecipientsCc: (f[\"Report recipients (CC)\"] as string | undefined) ?? null,\n headerImage: header,\n pScore: (f[\"pScore\"] as number | undefined) ?? null,\n rScore: (f[\"rScore\"] as number | undefined) ?? null,\n bpScore: (f[\"bpScore\"] as number | undefined) ?? null,\n seoScore: (f[\"seoScore\"] as number | undefined) ?? null,\n lastLighthouseAuditAt: (f[\"Last lighthouse audit at\"] as string | undefined) ?? null,\n a11yViolations: (f[\"A11y Violations\"] as number | undefined) ?? null,\n depsDrifted: (f[\"Deps Drifted\"] as number | undefined) ?? null,\n depsMajorBehind: (f[\"Deps Major Behind\"] as number | undefined) ?? null,\n depsOutdated: (f[\"Deps Outdated\"] as number | undefined) ?? null,\n securityVulnsCritical: (f[\"Security Vulns Critical\"] as number | undefined) ?? null,\n securityVulnsHigh: (f[\"Security Vulns High\"] as number | undefined) ?? null,\n securityVulnsModerate: (f[\"Security Vulns Moderate\"] as number | undefined) ?? null,\n securityVulnsLow: (f[\"Security Vulns Low\"] as number | undefined) ?? null,\n copyIntro: trimToNull(f[\"Copy — Intro\"]),\n copyContact: trimToNull(f[\"Copy — Contact\"]),\n copyFooter: trimToNull(f[\"Copy — Footer\"]),\n launchedAt: (f[\"Launched at\"] as string | undefined) ?? null,\n newsletterWebhook: trimToNull(f[\"Newsletter Webhook\"]),\n renovateFailingCis: (f[\"Renovate Failing CIs\"] as number | undefined) ?? null,\n defaultBranchCi: (f[\"Default Branch CI\"] as string | undefined) ?? null,\n lastCommitAt: (f[\"Last Commit At\"] as string | undefined) ?? null,\n githubSignalsAt: (f[\"GitHub Signals At\"] as string | undefined) ?? null,\n };\n}\n\nexport async function listWebsites(base: AirtableBase): Promise<WebsiteRow[]> {\n const out: WebsiteRow[] = [];\n await base(WEBSITES_TABLE)\n .select({ pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\nexport async function getWebsiteBySlug(\n base: AirtableBase,\n slug: string,\n): Promise<WebsiteRow | null> {\n // Slugs are siteSlug() output: [a-z0-9] segments joined by single hyphens.\n // Reject anything else — it can't match a real row, and it keeps URL-supplied\n // input out of the filter formula below (formula-injection guard).\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) return null;\n\n // Narrow the fetch to the slug-matching row server-side instead of paging the\n // whole table per request (MEDIUM-H). The formula replicates siteSlug() on\n // {Name} — lowercase → non-alnum runs to \"-\" → strip leading/trailing \"-\" —\n // verified against the live base. maxRecords caps it (slug collisions keep the\n // prior first-match-wins behavior).\n const formula = `REGEX_REPLACE(REGEX_REPLACE(LOWER({Name}),\"[^a-z0-9]+\",\"-\"),\"^-|-$\",\"\")=${JSON.stringify(\n slug,\n )}`;\n const rows: WebsiteRow[] = [];\n await base(WEBSITES_TABLE)\n .select({ filterByFormula: formula, maxRecords: 1 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n // Confirm the match in JS too: keeps the function correct if the formula and\n // siteSlug() ever drift, and under test fakes that don't evaluate the formula.\n return rows.find((w) => siteSlug(w.name) === slug) ?? null;\n}\n\n// ── audit-field builders ─────────────────────────────────────────────────────\n// One source of truth for the column-name → value mappings of each audit type.\n// The per-audit `updateXxxCounts` writers delegate to these (for their other\n// callers), and `updateAuditFields` merges whichever are present into ONE write —\n// so the field-name magic strings live in exactly one place.\n\nexport type A11yCounts = { violations: number };\nexport type DepsCounts = { drifted: number; majorBehind: number; outdated: number | null };\nexport type SecurityCounts = { critical: number; high: number; moderate: number; low: number };\n\nfunction scoreFields(scores: LighthouseScores): FieldSet {\n return {\n pScore: scores.performance,\n rScore: scores.accessibility,\n bpScore: scores.bestPractices,\n seoScore: scores.seo,\n \"Last lighthouse audit at\": new Date().toISOString(),\n };\n}\n\nfunction a11yFields(counts: A11yCounts): FieldSet {\n return { \"A11y Violations\": counts.violations };\n}\n\nfunction depsFields(counts: DepsCounts): FieldSet {\n const fields: FieldSet = {\n \"Deps Drifted\": counts.drifted,\n \"Deps Major Behind\": counts.majorBehind,\n };\n // Only write the outdated count when it was determined — a null (no/stale\n // lockfile this run) must not clobber a previously-good value.\n if (counts.outdated !== null) {\n fields[\"Deps Outdated\"] = counts.outdated;\n }\n return fields;\n}\n\nfunction securityFields(counts: SecurityCounts): FieldSet {\n return {\n \"Security Vulns Critical\": counts.critical,\n \"Security Vulns High\": counts.high,\n \"Security Vulns Moderate\": counts.moderate,\n \"Security Vulns Low\": counts.low,\n };\n}\n\n/**\n * Write the four Lighthouse scores + a refreshed-at timestamp onto a Websites row.\n * Called by `audit lighthouse --write-airtable` after a successful audit run, so\n * the operator never has to paste numbers manually before drafting a report.\n */\nexport async function updateScores(\n base: AirtableBase,\n recordId: string,\n scores: LighthouseScores,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: scoreFields(scores) }]);\n}\n\n/** Persist a11y violation count. */\nexport async function updateA11yCounts(\n base: AirtableBase,\n recordId: string,\n counts: A11yCounts,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: a11yFields(counts) }]);\n}\n\n/** Persist deps drift counts (declared-range drift + real outdated installs). */\nexport async function updateDepsCounts(\n base: AirtableBase,\n recordId: string,\n counts: DepsCounts,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: depsFields(counts) }]);\n}\n\n/** Persist security vulnerability counts by severity. */\nexport async function updateSecurityCounts(\n base: AirtableBase,\n recordId: string,\n counts: SecurityCounts,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: securityFields(counts) }]);\n}\n\n/**\n * Persist all of a single audit run's results to one Websites row in ONE atomic\n * `update()` — instead of up to four sequential updates on the same id (which left\n * a row half-written on a mid-sequence failure and quadrupled the request volume).\n * Pass only the audit slices that produced real values; each present slice is merged\n * via the SAME field mappings the per-audit writers use. Omit a slice (or pass\n * undefined) to leave those columns untouched. Returns the merged FieldSet so the\n * caller can enumerate what was written.\n */\nexport async function updateAuditFields(\n base: AirtableBase,\n recordId: string,\n audits: {\n scores?: LighthouseScores;\n a11y?: A11yCounts;\n deps?: DepsCounts;\n security?: SecurityCounts;\n },\n): Promise<FieldSet> {\n const fields: FieldSet = {};\n if (audits.scores) Object.assign(fields, scoreFields(audits.scores));\n if (audits.a11y) Object.assign(fields, a11yFields(audits.a11y));\n if (audits.deps) Object.assign(fields, depsFields(audits.deps));\n if (audits.security) Object.assign(fields, securityFields(audits.security));\n await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);\n return fields;\n}\n\n/** Persist the GitHub-signals sweep onto a Websites row (slice 2a). A null\n * `lastCommitAt` is OMITTED so a not-determined-this-run value never clobbers a\n * previously-good timestamp (mirrors updateDepsCounts' outdated handling). */\nexport async function updateGitHubSignals(\n base: AirtableBase,\n recordId: string,\n signals: {\n renovateFailingCis: number;\n ciState: string;\n lastCommitAt: string | null;\n sweptAt: string;\n },\n): Promise<void> {\n const fields: FieldSet = {\n \"Renovate Failing CIs\": signals.renovateFailingCis,\n \"Default Branch CI\": signals.ciState,\n \"GitHub Signals At\": signals.sweptAt,\n };\n if (signals.lastCommitAt !== null) {\n fields[\"Last Commit At\"] = signals.lastCommitAt;\n }\n await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);\n}\n\n/** Mark a site launched: flip Status → maintenance + stamp Launched at (M6b).\n * The first code that writes Status. Called after a Launch report sends. */\nexport async function updateLaunched(\n base: AirtableBase,\n recordId: string,\n at: string,\n): Promise<void> {\n const fields: FieldSet = { Status: \"maintenance\", \"Launched at\": at };\n await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);\n}\n","import type { Site, InventoryProvider } from \"../types.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { listWebsites, siteSlug, ACTIVE_STATUSES } from \"../reports/airtable/websites.js\";\nimport { isHttpUrl } from \"../util/url.js\";\n\nexport type AirtableInventoryOptions = {\n /**\n * Local workdir to compute each site's path as `{workdir}/{slug}`.\n * Defaults to REDDOOR_FLEET_WORKDIR env var if not provided.\n * Airtable doesn't store local checkout paths, so this is required.\n */\n workdir?: string;\n};\n\n/**\n * Read sites from the Airtable Websites table as an InventoryProvider.\n * Each row becomes one Site; `path` is computed as `{workdir}/{slug}`.\n * Only `maintenance` / `launch period` sites that have a `url` are included\n * (the live sites we audit + report on). The production URL is exposed as\n * `Site.deployedUrl` so the lighthouse audit can run against it with no\n * checkout. `repoUrl` is intentionally NOT set from `url` — a clone source\n * must come from `gitRepo` (`owner/repo`), never the production URL.\n */\nexport function fromAirtableBase(\n base: AirtableBase,\n opts: AirtableInventoryOptions = {},\n): InventoryProvider {\n return async (): Promise<Site[]> => {\n const workdir = opts.workdir ?? process.env.REDDOOR_FLEET_WORKDIR;\n if (!workdir) {\n throw new Error(\n \"fromAirtableBase requires `workdir` option or REDDOOR_FLEET_WORKDIR env (sites need a local path)\",\n );\n }\n const websites = await listWebsites(base);\n return websites\n .filter((w) => w.status !== null && ACTIVE_STATUSES.has(w.status) && w.url.length > 0)\n .flatMap((w) => {\n const slug = siteSlug(w.name);\n // An empty slug (a Name with no slug-able characters) can't form a stable\n // path and — fatally — can't be matched back to its Websites row on\n // write-back: every empty-slug site would collapse under the \"\" key and\n // mis-write or fail. Skip it loudly rather than silently mis-map it.\n if (slug.length === 0) {\n console.warn(\n `[inventory] skipping \"${w.name}\" (row ${w.id}): Name has no slug-able characters (empty slug)`,\n );\n return [];\n }\n const site: Site = {\n path: `${workdir}/${slug}`,\n name: slug,\n meta: { airtableRowId: w.id, displayName: w.name },\n };\n // Scheme-allowlist the Airtable `url` before exposing it as the\n // deployed-audit target (it's handed straight to Chrome/lhci). A\n // `file://`/`gopher://`/internal-host value would be a local-file read\n // or SSRF — skip the deployed audit for that site rather than trust it.\n if (isHttpUrl(w.url)) {\n site.deployedUrl = w.url;\n } else {\n console.warn(\n `[inventory] skipping deployed audit for \"${w.name}\": url is not http(s): ${JSON.stringify(w.url)}`,\n );\n }\n if (w.gitRepo) site.gitRepo = w.gitRepo;\n return [site];\n });\n };\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { ReportType, LighthouseScores } from \"./types.js\";\nimport { renderReportHtml } from \"./render.js\";\nimport { siteSlug } from \"./airtable/websites.js\";\nimport { resolveCopy } from \"./copy.js\";\nimport type { WebsiteRow } from \"./airtable/websites.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { createDraft, setDraftReady, listReportsForSite } from \"./airtable/reports.js\";\nimport { uploadAttachment } from \"./airtable/attachments.js\";\nimport type { AirtableBase } from \"./airtable/client.js\";\nimport { readGaConfig } from \"./ga/config.js\";\nimport { fetchPeriodUsers } from \"./ga/client.js\";\nimport { fetchSearchPresence } from \"./search/client.js\";\nimport type { SearchPresence } from \"./search/client.js\";\n\nexport type DraftOptions = {\n /** Where to write the local preview HTML when `previewOnly`. Defaults to `reports/<slug>/draft.html`. */\n previewPath?: string;\n /** If true: render locally only, never touch Airtable. */\n previewOnly?: boolean;\n /** UTC \"YYYY-MM\" recurrence key; falls back to periodEnd's month when omitted. */\n period?: string;\n /** Airtable record id of an EXISTING (not-ready) row to COMPLETE in place rather\n * than creating a new one. When set, we skip createDraft and only re-render →\n * upload the HTML attachment → flip Draft ready on this row. Used by the --due\n * re-draft path to finish a draft whose createDraft succeeded but whose\n * setDraftReady never ran (a crash mid-sequence wedged the period). */\n completeRowId?: string;\n /** The mapped ReportRow being completed, returned as `reportRow` from the\n * complete path so callers keep the same shape they get on the create path. */\n existingRow?: ReportRow;\n};\n\n/** An enrichment fetch that *errored* (not one that was legitimately skipped\n * because it isn't configured / the site lacks the inputs). Surfaced so a\n * fleet-wide GA/Search outage is visible in a `--due` batch summary instead of\n * hiding behind one easily-missed console.warn per site. */\nexport type SoftFailure = \"ga\" | \"search\";\n\nexport type DraftResult = {\n /** null when previewOnly. */\n reportRow: ReportRow | null;\n /** Path to the local preview file (only set when previewOnly). */\n htmlPath: string | null;\n /** Always present — the rendered HTML string. */\n html: string;\n /** Enrichment fetches that errored for this site (empty on success or skip). */\n softFailures: SoftFailure[];\n};\n\nfunction scoresFromWebsite(siteRow: WebsiteRow): LighthouseScores {\n const { pScore, rScore, bpScore, seoScore } = siteRow;\n if (pScore === null || rScore === null || bpScore === null || seoScore === null) {\n throw new Error(\n `Site '${siteRow.name}' is missing one or more Lighthouse scores on the Websites row (pScore, rScore, bpScore, seoScore). ` +\n `Run 'reddoor-maint audit lighthouse' from the site's checkout and paste the four numbers into Airtable, then retry.`,\n );\n }\n return { performance: pScore, accessibility: rScore, bestPractices: bpScore, seo: seoScore };\n}\n\nfunction daysAgo(today: Date, n: number): Date {\n // UTC accessors to stay TZ-consistent with `due.ts` (and avoid landing\n // Airtable's `Period start` on a different calendar day than the operator\n // expects on late-night runs near a month boundary). See morning brief\n // 2026-05-29 (M1) for context.\n const out = new Date(today);\n out.setUTCDate(out.getUTCDate() - n);\n return out;\n}\n\n/**\n * Render and create an Airtable draft for one site.\n *\n * No idempotency guard here — the recurrence guard lives in draftDueReports\n * (cli/commands/report.ts), keyed on reportPeriodKey(dueDate). The manual\n * single-site path intentionally always drafts (an operator asking for a draft\n * gets one). findReportByPeriod (airtable/reports.ts) is the real-Airtable\n * point lookup available to dashboard/digest callers that need the same\n * idempotency guarantee outside the CLI batch loop.\n */\nexport async function draftReportForSite(\n base: AirtableBase | null,\n siteRow: WebsiteRow,\n reportType: ReportType,\n options: DraftOptions = {},\n): Promise<DraftResult> {\n const scores = scoresFromWebsite(siteRow);\n\n const today = new Date();\n const slug = siteSlug(siteRow.name);\n\n const periodStart =\n base !== null ? await derivePeriodStart(base, siteRow, reportType, today) : daysAgo(today, 30);\n\n const periodEnd = today;\n const completedOn = today;\n const lastTestedDate =\n reportType === \"Maintenance\" && siteRow.testingDay ? new Date(siteRow.testingDay) : null;\n\n // GA enrichment (real path only). Soft-fail: any GA problem leaves the numbers null so\n // the draft still proceeds (operator fills them manually) — GA is an enhancement, not a\n // gate. Rendered with the fetched numbers so the review HTML matches the Airtable fields.\n // An *error* (vs a legitimate not-configured skip) is recorded in softFailures so the\n // caller can surface a fleet-wide outage in the batch summary.\n const gaResult =\n base !== null ? await fetchGaUsers(siteRow, periodStart, periodEnd) : NO_ENRICHMENT;\n const searchResult =\n base !== null ? await fetchSearch(siteRow, periodStart, periodEnd) : NO_ENRICHMENT;\n const gaUsers = gaResult.value;\n const search = searchResult.value;\n const softFailures: SoftFailure[] = [\n ...(gaResult.softFailed ? ([\"ga\"] as const) : []),\n ...(searchResult.softFailed ? ([\"search\"] as const) : []),\n ];\n\n const cidName = `${slug}-header`;\n const { html } = await renderReportHtml({\n siteName: siteRow.name,\n siteUrl: siteRow.url,\n reportType,\n completedOn,\n lighthouse: scores,\n gaUsersCurrent: gaUsers?.current,\n gaUsersPrevious: gaUsers?.previous,\n searchPosition: search?.foundOnPage1 ? (search.position ?? undefined) : undefined,\n lastTestedDate,\n commentary: null,\n copy: resolveCopy(siteRow),\n headerImageCid: cidName,\n });\n\n if (options.previewOnly) {\n const path = options.previewPath ?? `reports/${slug}/draft.html`;\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, html, \"utf-8\");\n return { reportRow: null, htmlPath: path, html, softFailures };\n }\n\n if (base === null) throw new Error(\"base required when previewOnly=false\");\n\n // \"Finish an existing row\" path (the --due re-draft wedge fix). When the caller\n // hands us a row that was created but never made Draft-ready — a crash between\n // createDraft and setDraftReady leaves exactly this — we DON'T createDraft again\n // (that would duplicate the period). We re-attach the rendered HTML and flip the\n // ready flag against the EXISTING row, completing the half-made draft in place.\n // The row's other fields (scores, period, dates) were already written at create\n // time; the only pieces a crash drops are the attachment + the ready flag.\n if (options.completeRowId) {\n await finishDraftRow(base, options.completeRowId, slug, periodEnd, html);\n return { reportRow: options.existingRow ?? null, htmlPath: null, html, softFailures };\n }\n\n const reportId = `${siteRow.name} — ${reportType} — ${periodEnd.toISOString().slice(0, 10)}`;\n const created = await createDraft(base, {\n reportId,\n siteId: siteRow.id,\n reportType,\n period: options.period ?? periodEnd.toISOString().slice(0, 7),\n periodStart,\n periodEnd,\n completedOn,\n lighthouse: scores,\n lastTestedDate,\n ...(gaUsers ? { gaUsersCurrent: gaUsers.current, gaUsersPrevious: gaUsers.previous } : {}),\n ...(search ? { searchFoundPage1: search.foundOnPage1 } : {}),\n ...(search?.foundOnPage1 && search.position !== null\n ? { searchPosition: search.position }\n : {}),\n });\n\n await finishDraftRow(base, created.id, slug, periodEnd, html);\n\n return { reportRow: created, htmlPath: null, html, softFailures };\n}\n\n/** Attach the rendered HTML and flip Draft ready=true on an existing Reports row.\n * Shared by both the create path and the \"complete a half-made row\" path so the\n * upload + ready-flag steps are identical (and re-runnable) either way. */\nasync function finishDraftRow(\n base: AirtableBase,\n rowId: string,\n slug: string,\n periodEnd: Date,\n html: string,\n): Promise<void> {\n const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;\n await uploadAttachment(rowId, \"Rendered HTML\", html, htmlFilename, \"text/html\");\n await setDraftReady(base, rowId, true);\n}\n\n/** Result of an enrichment fetch: the value (null if unavailable) plus whether\n * it errored (`softFailed`) as opposed to being legitimately not-configured. */\ntype Enrichment<T> = { value: T | null; softFailed: boolean };\n/** A not-configured / skipped enrichment — null value, not a soft-failure. */\nconst NO_ENRICHMENT: Enrichment<never> = { value: null, softFailed: false };\n\n/**\n * Fetch GA \"Users\" for the period, soft-failing to null. Returns a null value (no enrichment)\n * when GA isn't configured (`GA_SUBJECT` unset) or the site has no GA4 property ID — those are\n * legitimate skips, `softFailed: false`. When the GA API errors it logs a one-line warning and\n * returns `softFailed: true`. Never throws, so a GA problem can never block a draft; the\n * operator can always enter the numbers by hand.\n */\nasync function fetchGaUsers(\n siteRow: WebsiteRow,\n periodStart: Date,\n periodEnd: Date,\n): Promise<Enrichment<{ current: number; previous: number }>> {\n const cfg = readGaConfig();\n if (!cfg || !siteRow.ga4PropertyId) return NO_ENRICHMENT;\n try {\n const value = await fetchPeriodUsers(\n { propertyId: siteRow.ga4PropertyId, subject: cfg.subject, keyPath: cfg.keyPath },\n periodStart,\n periodEnd,\n );\n return { value, softFailed: false };\n } catch (e) {\n console.warn(`⚠ GA skipped for ${siteRow.name}: ${(e as Error).message}`);\n return { value: null, softFailed: true };\n }\n}\n\n/**\n * Fetch the site's Google search presence for the period, soft-failing to null. Returns a null\n * value when GA/SA isn't configured (`readGaConfig()` null — search shares the SA credentials)\n * or the site has no `searchQuery` (legitimate skips, `softFailed: false`). When the Search\n * Console API errors it logs a one-line warning and returns `softFailed: true`. Never throws,\n * so a search problem can never block a draft.\n */\nasync function fetchSearch(\n siteRow: WebsiteRow,\n periodStart: Date,\n periodEnd: Date,\n): Promise<Enrichment<SearchPresence>> {\n const cfg = readGaConfig();\n if (!cfg || !siteRow.searchQuery) return NO_ENRICHMENT;\n try {\n const value = await fetchSearchPresence(\n {\n keyPath: cfg.keyPath,\n subject: cfg.subject,\n property: siteRow.searchConsoleProperty ?? undefined,\n host: siteRow.url,\n query: siteRow.searchQuery,\n },\n periodStart,\n periodEnd,\n );\n return { value, softFailed: false };\n } catch (e) {\n console.warn(`⚠ Search presence skipped for ${siteRow.name}: ${(e as Error).message}`);\n return { value: null, softFailed: true };\n }\n}\n\nasync function derivePeriodStart(\n base: AirtableBase,\n siteRow: WebsiteRow,\n reportType: ReportType,\n today: Date,\n): Promise<Date> {\n const prior = await listReportsForSite(base, siteRow.id);\n const sameType = prior\n .filter((r) => r.reportType === reportType && r.periodEnd)\n .map((r) => r.periodEnd!)\n .sort();\n const latest = sameType[sameType.length - 1];\n if (!latest) return daysAgo(today, 30);\n // Half-open periods. The prior report's GA/Search windows are inclusive of its\n // periodEnd, so starting this report on the *same* day double-counts that\n // boundary day across two consecutive reports (and inflates the headline Users\n // count). Start the next day instead. UTC to stay TZ-consistent with daysAgo.\n const start = new Date(latest);\n start.setUTCDate(start.getUTCDate() + 1);\n return start;\n}\n","import mjml2html from \"mjml\";\nimport type { ReportData } from \"./types.js\";\nimport { buildMjml } from \"./maintenance-email/template.js\";\nimport { buildLaunchMjml } from \"./launch-email/template.js\";\n\nexport type RenderResult = {\n html: string;\n warnings: Array<{ line: number; message: string }>;\n};\n\nexport async function renderReportHtml(data: ReportData): Promise<RenderResult> {\n const mjml = data.reportType === \"Launch\" ? buildLaunchMjml(data) : buildMjml(data);\n const out = await mjml2html(mjml, { validationLevel: \"strict\" });\n return { html: out.html, warnings: out.errors ?? [] };\n}\n","import type { WebsiteRow } from \"./airtable/websites.js\";\n\nexport type ResolvedCopy = {\n maintenanceIntro: string;\n maintenanceChecks: string[]; // 6; index 3 is the Google row's no-position default\n testingIntro: string;\n testingChecklist: string[]; // 6\n notesHeader: string;\n seoCta: string;\n contact: string[]; // closing invitation lines\n footerOrg: string;\n footerAddress: string[];\n launchHeading: string;\n launchBody: string;\n launchSetupItems: string[];\n};\n\nexport const DEFAULT_COPY: ResolvedCopy = {\n maintenanceIntro:\n \"Includes checking the hosting, DNS, Content Management System (CMS, if applicable), search indexing and security of the site for major flaws and updating as necessary.\",\n maintenanceChecks: [\n \"Reviewed Logs\",\n \"CMS Checked\",\n \"DNS Checked\",\n \"Google Indexed\",\n \"Reviewed Certificate\",\n \"Security Updates\",\n ],\n testingIntro:\n \"Testing includes checks similar to those at launch: testing on common browsers and operating systems, at different screen sizes, and checking every function, and updating all packages for performance rather than just those needed for security.\",\n testingChecklist: [\n \"Desktop Browsers\",\n \"Mobile Browsers\",\n \"Package Updates\",\n \"Bottlenecks\",\n \"Form Functionality\",\n \"Animation Functionality\",\n ],\n notesHeader: \"NOTES\",\n seoCta: \"Contact us if you are interested in more in-depth data or have questions about SEO.\",\n contact: [\"Just hit reply.\", \"We're here to help in any way we can.\"],\n footerOrg: \"Reddoor Creative, LLC\",\n footerAddress: [\"29027 Dapper Dan\", \"Fair Oaks Ranch, TX 78015\"],\n launchHeading: \"LAUNCHED\",\n launchBody:\n \"Your site is live. We've set it up on the Reddoor stack with hosting, security, and automatic maintenance so it stays fast and healthy. Here's what's in place:\",\n launchSetupItems: [\n \"Hosting, DNS, and SSL configured\",\n \"Continuous integration + automatic dependency updates\",\n \"Analytics and uptime monitoring\",\n ],\n};\n\n/** Trim an override to null when blank (mirrors the trim-to-null handling). */\nfunction override(v: string | null): string | null {\n if (typeof v !== \"string\") return null;\n const t = v.trim();\n return t.length > 0 ? t : null;\n}\n\n/**\n * Resolve a site's effective copy: DEFAULT_COPY with the three per-site narrative\n * overrides applied. Only maintenanceIntro/contact/footer are per-site (M6a §2);\n * everything else is the shared default. PURE.\n */\n/** Split an operator override into lines: tolerate CRLF, drop blank lines (a stray\n * blank in the Airtable cell shouldn't render an empty address row). */\nfunction splitLines(s: string): string[] {\n return s.split(/\\r?\\n/).filter((l) => l.trim().length > 0);\n}\n\nexport function resolveCopy(site: WebsiteRow): ResolvedCopy {\n const intro = override(site.copyIntro);\n const contact = override(site.copyContact);\n const footer = override(site.copyFooter);\n const footerLines = footer ? splitLines(footer) : null;\n return {\n ...DEFAULT_COPY,\n maintenanceIntro: intro ?? DEFAULT_COPY.maintenanceIntro,\n contact: contact ? splitLines(contact) : DEFAULT_COPY.contact,\n footerOrg: footerLines?.[0] ?? DEFAULT_COPY.footerOrg,\n footerAddress: footerLines ? footerLines.slice(1) : DEFAULT_COPY.footerAddress,\n };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const CHECK_CID = \"rd-check-png\";\nexport const BLURRED_CID = \"rd-blurred-tests-jpg\";\n\nexport type BundledImage = {\n bytes: Uint8Array;\n contentType: string;\n cid: string;\n filename: string;\n};\n\n// Walk up from the current module's URL looking for the assets dir in either\n// the dev layout (src/reports/maintenance-email/assets/) or the published\n// layout (dist/reports/maintenance-email/assets/). REQUIRED because tsup\n// inlines this module into dist/cli/bin.js — so `import.meta.url`-based\n// sibling resolution looks in dist/cli/ for the PNGs and fails with ENOENT.\n// Regression that shipped in 0.10.0–0.10.1; tests passed in dev because\n// vitest evaluates the source file where import.meta.url is already correct.\nlet cachedAssetsDir: string | null = null;\nfunction resolveAssetsDir(): string {\n if (cachedAssetsDir) return cachedAssetsDir;\n let dir = dirname(fileURLToPath(import.meta.url));\n while (true) {\n // Source layout preferred — single source of truth in the workspace\n // and the only one present in dev/test environments.\n const srcCandidate = join(dir, \"src\", \"reports\", \"maintenance-email\", \"assets\", \"check.png\");\n if (existsSync(srcCandidate)) {\n cachedAssetsDir = dirname(srcCandidate);\n return cachedAssetsDir;\n }\n // Published layout — only `dist/` ships per package.json#files, so\n // consumers fall through to here.\n const distCandidate = join(dir, \"dist\", \"reports\", \"maintenance-email\", \"assets\", \"check.png\");\n if (existsSync(distCandidate)) {\n cachedAssetsDir = dirname(distCandidate);\n return cachedAssetsDir;\n }\n const parent = dirname(dir);\n if (parent === dir) {\n throw new Error(\n `loadBundledImages: could not locate maintenance-email assets dir by walking up from ${fileURLToPath(import.meta.url)}. Checked both src/ and dist/ layouts.`,\n );\n }\n dir = parent;\n }\n}\n\n/**\n * Read the bundled image bytes from disk. Both Maintenance and Testing\n * variants reference `check.png`; only the Maintenance variant references\n * `blurredTests.jpg`.\n */\nexport async function loadBundledImages(): Promise<{\n check: BundledImage;\n blurred: BundledImage;\n}> {\n const assetsDir = resolveAssetsDir();\n const [check, blurred] = await Promise.all([\n readFile(join(assetsDir, \"check.png\")),\n readFile(join(assetsDir, \"blurredTests.jpg\")),\n ]);\n return {\n check: {\n bytes: new Uint8Array(check),\n contentType: \"image/png\",\n cid: CHECK_CID,\n filename: \"check.png\",\n },\n blurred: {\n bytes: new Uint8Array(blurred),\n contentType: \"image/jpeg\",\n cid: BLURRED_CID,\n filename: \"blurredTests.jpg\",\n },\n };\n}\n","/**\n * Shared HTML/XML escape. One implementation behind the dashboard renderers\n * (`src/dashboard/render.ts`, `fleet-render.ts`), the daily digest\n * (`src/reports/digest.ts`), and the MJML email templates\n * (`src/reports/*-email/template.ts`).\n *\n * The set is the strict-XML set (`& < > \" '`), which is exactly what MJML's\n * `validationLevel: \"strict\"` parser needs and a superset of what plain HTML text\n * interpolation needs — so the SAME function serves both sinks. Site names\n * (e.g. \"Brown & Co\"), URLs, and operator commentary must not break the markup or\n * inject. The MJML templates re-export this as `escapeXml` for their callers.\n */\nexport function escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/** Allow only http(s) URLs in an href context; everything else collapses to \"#\". */\nexport function safeUrl(raw: string): string {\n try {\n const u = new URL(raw);\n if (u.protocol === \"http:\" || u.protocol === \"https:\") return raw;\n } catch {\n // fall through\n }\n return \"#\";\n}\n","import type { ReportData } from \"../types.js\";\nimport { DEFAULT_COPY, type ResolvedCopy } from \"../copy.js\";\nimport { CHECK_CID, BLURRED_CID } from \"./assets/index.js\";\nimport { escapeHtml } from \"../../util/html.js\";\nimport { isHttpUrl } from \"../../util/url.js\";\n\n/**\n * Escape operator/site-controlled strings before interpolating into the MJML markup.\n * MJML parses as XML with `validationLevel: \"strict\"`. Under mjml@4.18 a raw `&`, `<`,\n * or `>` does NOT throw — it passes straight through into the rendered output, so an\n * unescaped value (e.g. a site name \"Brown & Co\", a URL, or commentary) silently\n * injects HTML/markup into the email. A raw `\"` inside an ATTRIBUTE value (e.g. the\n * image `href`/`alt`) is the one that throws — it terminates the attribute and trips a\n * parse error that blocks the send. So we escape for two reasons: prevent\n * HTML/markup injection in text, and prevent the attribute-quote parse error. Apply\n * to every interpolation of siteName / siteUrl / commentary / copy.\n *\n * This IS `src/util/html.ts`'s `escapeHtml` (the strict-XML set is identical),\n * re-exported under the name the email templates import (the launch template imports\n * `escapeXml` from here).\n */\nexport const escapeXml = escapeHtml;\n\n// Bundled images: shipped in dist/ via tsup onSuccess copy, attached inline via\n// CID by orchestrate.ts at send time. No external CDN dependency.\nconst CHECK_PNG = `cid:${CHECK_CID}`;\nconst BLURRED_TESTS = `cid:${BLURRED_CID}`;\n\nexport function fmtDate(d: Date | null): string {\n // Guard BOTH null AND an Invalid Date — `new Date(\"not-a-date\")` (a malformed\n // Airtable date string) is a truthy Date whose getUTC* accessors all return\n // NaN, which would render \"NaN.NaN.NaN\" into a real client email. `!d` alone\n // misses it; `Number.isNaN(d.getTime())` catches it.\n if (!d || Number.isNaN(d.getTime())) return \"\";\n // Airtable date fields are wall-clock YYYY-MM-DD strings parsed as UTC midnight.\n // Use UTC accessors so the rendered date matches what the operator entered.\n // US format: MM.DD.YYYY (Reddoor is Texas-based, clients are US).\n const mm = String(d.getUTCMonth() + 1).padStart(2, \"0\");\n const dd = String(d.getUTCDate()).padStart(2, \"0\");\n const yyyy = d.getUTCFullYear();\n return `${mm}.${dd}.${yyyy}`;\n}\n\nfunction fmtUsers(n: number): string {\n return n.toLocaleString(\"en-US\");\n}\n\nconst TREND_UP = \"#2E7D32\"; // positive green — growth reads as good\nconst TREND_NEUTRAL = \"#757575\"; // muted grey — dips/flat aren't failures (and brand red is reserved)\n\nfunction trendText(color: string, text: string): string {\n return `<mj-text color=\"${color}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${text}</mj-text>`;\n}\n\n/**\n * The line under \"{N} Users\": a directional trend vs the previous period when both numbers\n * are real, else a graceful fallback. `undefined` means GA was unavailable (distinct from a\n * real 0). Up = green; down/flat = muted grey (a traffic dip isn't a failure).\n */\nfunction analyticsTrendLine(cur: number | undefined, prev: number | undefined): string {\n if (cur === undefined || prev === undefined) {\n // GA unavailable for one/both — show the prior count if we have it, else an em dash.\n return trendText(TREND_NEUTRAL, `Last Period: ${prev !== undefined ? fmtUsers(prev) : \"—\"}`);\n }\n if (prev === 0) {\n return cur > 0\n ? trendText(TREND_UP, \"▲ New this period (0 last period)\")\n : trendText(TREND_NEUTRAL, \"Last Period: 0\");\n }\n const pct = Math.round(((cur - prev) / prev) * 100);\n const range = `(${fmtUsers(prev)} → ${fmtUsers(cur)})`;\n if (pct > 0) return trendText(TREND_UP, `▲ ${pct}% vs last period ${range}`);\n if (pct < 0) return trendText(TREND_NEUTRAL, `▼ ${Math.abs(pct)}% vs last period ${range}`);\n return trendText(TREND_NEUTRAL, `No change vs last period (${fmtUsers(prev)})`);\n}\n\nfunction maintenanceChecksSection(copy: ResolvedCopy, searchPosition?: number): string {\n const googleLabel =\n searchPosition !== undefined\n ? `Page 1 Google Result (#${searchPosition})`\n : (copy.maintenanceChecks[3] ?? \"\");\n const rows = copy.maintenanceChecks.map((label, i) => (i === 3 ? googleLabel : label));\n return rows\n .map(\n (label, i) => `\n <mj-section background-color=\"white\" padding=\"0px\"${i === rows.length - 1 ? ' padding-bottom=\"36px\"' : \"\"}>\n <mj-group>\n <mj-column padding-left=\"0px\" width=\"90%\"${i < rows.length - 1 ? ' border-bottom=\"solid #CCCCCC 1px\"' : \"\"}>\n <mj-text height=\"25px\" padding-left=\"0px\" color=\"#757575\" padding-top=\"20px\" padding-bottom=\"7.5px\" font-size=\"16px\">${escapeXml(label)}</mj-text>\n </mj-column>\n <mj-column width=\"10%\"${i < rows.length - 1 ? ' border-bottom=\"solid #CCCCCC 1px\"' : \"\"} padding-top=\"15px\">\n <mj-image align=\"right\" padding-right=\"0px\" width=\"20px\" height=\"20px\" padding-top=\"2.5px\" padding-bottom=\"15px\" src=\"${CHECK_PNG}\" />\n </mj-column>\n </mj-group>\n </mj-section>`,\n )\n .join(\"\");\n}\n\nfunction testingChecklistSection(copy: ResolvedCopy): string {\n const rows = copy.testingChecklist;\n return rows\n .map(\n (label, i) => `\n <mj-section background-color=\"#F4F4F4\" padding=\"0px\"${i === rows.length - 1 ? ' padding-bottom=\"60px\"' : \"\"}>\n <mj-group>\n <mj-column width=\"90%\" padding-left=\"0px\"${i < rows.length - 1 ? ' border-bottom=\"solid #CCCCCC 1px\"' : \"\"}>\n <mj-text height=\"25px\" padding-left=\"0px\" color=\"#757575\" padding-top=\"20px\" padding-bottom=\"7.5px\" font-size=\"16px\">${escapeXml(label)}</mj-text>\n </mj-column>\n <mj-column width=\"10%\"${i < rows.length - 1 ? ' border-bottom=\"solid #CCCCCC 1px\"' : \"\"} padding-top=\"15px\">\n <mj-image align=\"right\" padding-right=\"0px\" width=\"20px\" height=\"20px\" padding-top=\"2.5px\" padding-bottom=\"15px\" src=\"${CHECK_PNG}\" />\n </mj-column>\n </mj-group>\n </mj-section>`,\n )\n .join(\"\");\n}\n\nfunction maintenanceTestingPlaceholder(lastTested: Date | null): string {\n return `\n <mj-section background-color=\"#F4F4F4\">\n <mj-column>\n <mj-image href=\"mailto:info@reddoorla.com\" src=\"${BLURRED_TESTS}\" />\n </mj-column>\n </mj-section>\n <mj-section background-color=\"#F4F4F4\" padding-top=\"0px\">\n <mj-column>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">Last Tested: ${fmtDate(lastTested)}</mj-text>\n </mj-column>\n </mj-section>`;\n}\n\nfunction testingIntroSection(copy: ResolvedCopy): string {\n return `\n <mj-section background-color=\"#F4F4F4\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">TESTING</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${escapeXml(copy.testingIntro)}</mj-text>\n </mj-column>\n </mj-section>`;\n}\n\nfunction commentarySection(text: string, copy: ResolvedCopy): string {\n return `\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"55px\">${escapeXml(copy.notesHeader)}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${escapeXml(text).replace(/\\r\\n?|\\n/g, \"<br/>\")}</mj-text>\n </mj-column>\n </mj-section>`;\n}\n\nfunction hasHeaderDims(\n data: ReportData,\n): data is ReportData & { headerWidth: number; headerHeight: number; headerBgColor: string } {\n return Boolean(data.headerWidth && data.headerHeight && data.headerBgColor);\n}\n\nexport function headerImageTag(data: ReportData): string {\n const src = `cid:${data.headerImageCid}`;\n const alt = `${escapeXml(data.siteName)} maintenance report`;\n // escapeXml only escapes markup chars — it does NOT neutralize a dangerous URL\n // scheme. A `javascript:`/`data:` siteUrl would survive escaping and become a live\n // header href. Gate on isHttpUrl (the same http(s) allowlist the audit path uses)\n // and DROP a non-http(s) href entirely (fall back to \"#\") rather than linking it.\n const href = isHttpUrl(data.siteUrl) ? escapeXml(data.siteUrl) : \"#\";\n // Reserve the box and show a matched placeholder while the image loads / if blocked.\n // Critically, we do NOT set an mj-image `height` — MJML would emit `height:<px>` while\n // keeping `width:100%`, locking the height while the width scales and distorting the\n // image at any rendered width != the design width (mobile, narrow panes). Instead the\n // image stays `height:auto` (proportional) and the box is reserved via `aspect-ratio`\n // in the head <mj-style> below (see headerStyleBlock). `container-background-color` is\n // the placeholder; the bare fallback (no dims, e.g. local preview) keeps today's behavior.\n if (hasHeaderDims(data)) {\n return `<mj-image href=\"${href}\" src=\"${src}\" alt=\"${alt}\" width=\"${data.headerWidth}px\" css-class=\"rd-header\" container-background-color=\"${data.headerBgColor}\" />`;\n }\n return `<mj-image href=\"${href}\" src=\"${src}\" alt=\"${alt}\" />`;\n}\n\nexport function headerStyleBlock(data: ReportData): string {\n if (!hasHeaderDims(data)) return \"\";\n // Reserve the header's vertical space by aspect ratio so it scales proportionally with\n // its fluid (width:100%) width — no fixed pixel height, so it never squishes.\n // `height:auto !important` defends against any client honoring MJML's inline height.\n return `<mj-style>.rd-header img { height: auto !important; aspect-ratio: ${data.headerWidth} / ${data.headerHeight}; }</mj-style>`;\n}\n\nexport function buildMjml(data: ReportData): string {\n const copy = data.copy ?? DEFAULT_COPY;\n const isTesting = data.reportType === \"Testing\";\n const previewText = `Checked up on ${escapeXml(data.siteName)}`;\n\n return `<mjml>\n <mj-head>\n <mj-attributes>\n <mj-text font-family=\"helvetica, sans-serif\" padding-left=\"5px\" padding-right=\"5px\" />\n <mj-section padding-left=\"11%\" padding-right=\"11%\"/>\n <mj-image padding=\"0px\" />\n </mj-attributes>\n <mj-preview>${previewText}</mj-preview>\n ${headerStyleBlock(data)}\n </mj-head>\n <mj-body background-color=\"white\">\n <mj-section background-color=\"#F4F4F4\" padding-top=\"0px\" padding-bottom=\"0px\" padding-left=\"0px\" padding-right=\"0px\">\n <mj-column>\n ${headerImageTag(data)}\n </mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">COMPLETED ON</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\">${fmtDate(data.completedOn)}</mj-text>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">MAINTENANCE CHECKS</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${escapeXml(copy.maintenanceIntro)}</mj-text>\n </mj-column>\n </mj-section>\n ${maintenanceChecksSection(copy, data.searchPosition)}\n <mj-section background-color=\"#F4F4F4\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"55px\">LIGHTHOUSE SCORES*</mj-text>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"300\" padding-top=\"25px\">Performance</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\" padding-top=\"0px\">${data.lighthouse.performance}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"0px\" padding-bottom=\"36px\">Acceptable 50–89 // Ideal 90–100</mj-text>\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"300\" padding-top=\"25px\">Readability</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\" padding-top=\"0px\">${data.lighthouse.accessibility}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"0px\" padding-bottom=\"36px\">Acceptable 80–99 // Ideal 100</mj-text>\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"300\" padding-top=\"25px\">Best Practices</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\" padding-top=\"0px\">${data.lighthouse.bestPractices}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"0px\" padding-bottom=\"36px\">Acceptable 60–79 // Ideal 80–92</mj-text>\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"300\" padding-top=\"25px\">Site Structure</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\" padding-top=\"0px\">${data.lighthouse.seo}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"0px\" padding-bottom=\"36px\">Acceptable 50–89 // Ideal 90–100</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" padding-bottom=\"36px\" line-height=\"20px\">*A Lighthouse score is a numerical measure provided by Google's Lighthouse tool, which evaluates various aspects of a web page's quality.</mj-text>\n </mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">ANALYTICS</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\">${data.gaUsersCurrent !== undefined ? fmtUsers(data.gaUsersCurrent) : \"—\"} Users</mj-text>\n ${analyticsTrendLine(data.gaUsersCurrent, data.gaUsersPrevious)}\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" padding-bottom=\"36px\" line-height=\"20px\">${escapeXml(copy.seoCta)}</mj-text>\n </mj-column>\n </mj-section>\n ${isTesting ? testingIntroSection(copy) + testingChecklistSection(copy) : maintenanceTestingPlaceholder(data.lastTestedDate)}\n ${data.commentary ? commentarySection(data.commentary, copy) : \"\"}\n <mj-section background-color=\"white\">\n <mj-column padding-top=\"36px\">\n <mj-text color=\"#C00\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"700\" padding-top=\"36px\" line-height=\"36px\">Any questions, concerns or requests?</mj-text>\n ${copy.contact\n .map((line, i) =>\n i === copy.contact.length - 1\n ? `<mj-text font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"300\" padding-top=\"0px\" line-height=\"30px\" padding-bottom=\"36px\">${escapeXml(line)}</mj-text>`\n : `<mj-text font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"300\" line-height=\"30px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\\n \")}\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" line-height=\"20px\" font-style=\"italic\">Copyright ${new Date().getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"700\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">Our mailing address is:</mj-text>\n ${[copy.footerOrg, ...copy.footerAddress]\n .map(\n (line) =>\n `<mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\\n \")}\n </mj-column>\n </mj-section>\n </mj-body>\n</mjml>`;\n}\n","import type { ReportData } from \"../types.js\";\nimport { DEFAULT_COPY } from \"../copy.js\";\nimport {\n escapeXml,\n fmtDate,\n headerImageTag,\n headerStyleBlock,\n} from \"../maintenance-email/template.js\";\n\nconst RED = \"#C00\";\nconst GREY = \"#757575\";\n\n/** Purpose-built go-live email: header · LAUNCHED + date · message · what-we-set-up\n * · contact · footer. Reuses the M6a copy layer (contact/footer honor per-site\n * overrides). No maintenance checklist / Lighthouse / analytics. */\nexport function buildLaunchMjml(data: ReportData): string {\n const copy = data.copy ?? DEFAULT_COPY;\n const previewText = `${escapeXml(data.siteName)} is live`;\n // All copy — launchHeading/launchBody/launchSetupItems included — is escaped\n // (spec §3.3: all copy escaped). It keeps strict MJML from choking on a stray\n // `&`/`<` if the default copy ever gains one, matching contact/footer below.\n const setupRows = copy.launchSetupItems\n .map(\n (item) => `\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\" padding-top=\"4px\" padding-bottom=\"4px\">• ${escapeXml(item)}</mj-text>`,\n )\n .join(\"\");\n const contactRows = copy.contact\n .map(\n (line) => `\n <mj-text font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"300\" line-height=\"30px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\");\n const footerAddressRows = copy.footerAddress\n .map(\n (line) => `\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\");\n\n return `<mjml>\n <mj-head>\n <mj-attributes>\n <mj-text font-family=\"helvetica, sans-serif\" padding-left=\"5px\" padding-right=\"5px\" />\n <mj-section padding-left=\"11%\" padding-right=\"11%\"/>\n <mj-image padding=\"0px\" />\n </mj-attributes>\n <mj-preview>${previewText}</mj-preview>\n ${headerStyleBlock(data)}\n </mj-head>\n <mj-body background-color=\"white\">\n <mj-section background-color=\"#F4F4F4\" padding-top=\"0px\" padding-bottom=\"0px\" padding-left=\"0px\" padding-right=\"0px\">\n <mj-column>${headerImageTag(data)}</mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"${RED}\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">${escapeXml(copy.launchHeading)}</mj-text>\n <mj-text color=\"${RED}\" font-size=\"44px\" font-weight=\"400\">${fmtDate(data.completedOn)}</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\" padding-top=\"20px\">${escapeXml(copy.launchBody)}</mj-text>\n ${setupRows}\n </mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column padding-top=\"36px\">\n <mj-text color=\"${RED}\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"700\" padding-top=\"36px\" line-height=\"36px\">Any questions, concerns or requests?</mj-text>\n ${contactRows}\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" line-height=\"20px\" font-style=\"italic\">Copyright ${new Date().getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"700\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">Our mailing address is:</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(copy.footerOrg)}</mj-text>\n ${footerAddressRows}\n </mj-column>\n </mj-section>\n </mj-body>\n</mjml>`;\n}\n","import type { FieldSet, Records } from \"airtable\";\nimport type { AirtableBase } from \"./client.js\";\nimport type { ReportType, LighthouseScores } from \"../types.js\";\n\nexport const REPORTS_TABLE = \"Reports\";\n\nconst REPORT_TYPES: readonly ReportType[] = [\"Maintenance\", \"Testing\", \"Launch\"];\n\n/** Coerce the Airtable `Report type` (a single-select string) to a known\n * ReportType. A bare `as ReportType` cast is a compile-time lie: if the\n * single-select gains an unexpected option, the bad value flows to render.ts,\n * where `reportType === \"Launch\"` silently falls through to the Maintenance\n * template. Validate at the boundary; warn + default to \"Maintenance\" so an\n * unknown type is VISIBLE in the logs rather than silently mis-templated. */\nfunction toReportType(raw: string | undefined): ReportType {\n if (raw && (REPORT_TYPES as readonly string[]).includes(raw)) return raw as ReportType;\n if (raw)\n console.warn(`[reports] unknown Report type ${JSON.stringify(raw)} — treating as Maintenance`);\n return \"Maintenance\";\n}\n\nexport type DeliveryStatus = \"pending\" | \"delivered\" | \"bounced\" | \"complained\";\n\nexport type ReportRow = {\n id: string;\n reportId: string;\n siteId: string;\n reportType: ReportType;\n /** UTC `YYYY-MM` recurrence key (idempotency for search-before-create). Null on legacy rows. */\n period: string | null;\n periodStart: string | null;\n periodEnd: string | null;\n completedOn: string | null;\n lighthouse: LighthouseScores | null;\n gaUsersCurrent: number | null;\n gaUsersPrevious: number | null;\n searchFoundPage1: boolean | null;\n searchPosition: number | null;\n lastTestedDate: string | null;\n commentary: string | null;\n subjectOverride: string | null;\n draftReady: boolean;\n approvedToSend: boolean;\n sentAt: string | null;\n approvedAt: string | null;\n approvedBy: string | null;\n deliveryStatus: DeliveryStatus;\n renderedHtmlAttachment: { url: string; filename: string } | null;\n /** Read out of the Resend response and stored in a hidden field; needed for webhook reconciliation. */\n resendMessageId: string | null;\n};\n\n/**\n * The \"Ready for your yes\" gate: Draft ready ∧ ¬Approved to send ∧ Sent at BLANK.\n * The single source of truth for \"pending the operator's approval\" — `listPendingApproval`,\n * `runDigest`'s ready-list, the per-site dashboard, and the fleet cockpit all key off this\n * one predicate so the surfaces can't drift.\n */\nexport function isPendingApproval(r: ReportRow): boolean {\n return r.draftReady && !r.approvedToSend && r.sentAt === null;\n}\n\nfunction mapRow(rec: { id: string; fields: Record<string, unknown> }): ReportRow {\n const f = rec.fields;\n const linkSites = (f[\"Site\"] as string[] | undefined) ?? [];\n const html =\n ((f[\"Rendered HTML\"] as Array<{ url: string; filename: string }> | undefined) ?? [])[0] ?? null;\n return {\n id: rec.id,\n reportId: String(f[\"Report ID\"] ?? \"\"),\n siteId: linkSites[0] ?? \"\",\n reportType: toReportType(f[\"Report type\"] as string | undefined),\n period: (f[\"Period\"] as string | undefined) ?? null,\n periodStart: (f[\"Period start\"] as string | undefined) ?? null,\n periodEnd: (f[\"Period end\"] as string | undefined) ?? null,\n completedOn: (f[\"Completed on\"] as string | undefined) ?? null,\n lighthouse: lighthouseFromFields(f),\n gaUsersCurrent: (f[\"GA users (period)\"] as number | undefined) ?? null,\n gaUsersPrevious: (f[\"GA users (prev period)\"] as number | undefined) ?? null,\n searchFoundPage1:\n typeof f[\"Search found page 1\"] === \"boolean\" ? (f[\"Search found page 1\"] as boolean) : null,\n searchPosition: (f[\"Search position\"] as number | undefined) ?? null,\n lastTestedDate: (f[\"Last tested date\"] as string | undefined) ?? null,\n commentary: (f[\"Commentary\"] as string | undefined) ?? null,\n subjectOverride: (f[\"Subject override\"] as string | undefined) ?? null,\n draftReady: Boolean(f[\"Draft ready\"]),\n approvedToSend: Boolean(f[\"Approved to send\"]),\n sentAt: (f[\"Sent at\"] as string | undefined) ?? null,\n approvedAt: (f[\"Approved At\"] as string | undefined) ?? null,\n approvedBy: (f[\"Approved By\"] as string | undefined) ?? null,\n deliveryStatus: ((f[\"Delivery status\"] as string | undefined) ?? \"pending\") as DeliveryStatus,\n renderedHtmlAttachment: html,\n resendMessageId: (f[\"Resend message ID\"] as string | undefined) ?? null,\n };\n}\n\nfunction lighthouseFromFields(f: Record<string, unknown>): LighthouseScores | null {\n const p = f[\"Lighthouse — Performance\"];\n const a = f[\"Lighthouse — Accessibility\"];\n const b = f[\"Lighthouse — Best Practices\"];\n const s = f[\"Lighthouse — SEO\"];\n if (\n typeof p !== \"number\" ||\n typeof a !== \"number\" ||\n typeof b !== \"number\" ||\n typeof s !== \"number\"\n )\n return null;\n return { performance: p, accessibility: a, bestPractices: b, seo: s };\n}\n\nexport type DraftInput = {\n reportId: string;\n siteId: string;\n reportType: ReportType;\n /** UTC `YYYY-MM` recurrence key. Omitted on legacy callers; written only when supplied. */\n period?: string;\n periodStart: Date;\n periodEnd: Date;\n completedOn: Date;\n lighthouse: LighthouseScores;\n lastTestedDate: Date | null;\n /** GA \"Users\" for the period / previous period. Omitted when GA is not configured\n * for the site or the fetch failed — the operator fills the fields manually. */\n gaUsersCurrent?: number;\n gaUsersPrevious?: number;\n /** Search-presence result. `searchFoundPage1` is written whenever the check ran (true or\n * false — false is the operator-only negative signal). `searchPosition` only when found. */\n searchFoundPage1?: boolean;\n searchPosition?: number;\n};\n\nfunction ymd(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Escape a string for safe interpolation into an Airtable filterByFormula.\n * Airtable formulas use SQL-like string literals; we escape backslash and\n * double quote. Used wherever an externally-supplied string flows into a\n * formula (e.g. Resend message ids on the webhook path).\n */\nexport function escapeFormulaString(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nexport async function createDraft(base: AirtableBase, input: DraftInput): Promise<ReportRow> {\n // Set Delivery status to \"pending\" at creation time, NOT at send time. This\n // matters for H4: if stampSent wrote \"pending\" after the webhook had already\n // written \"delivered\" (race), the operator would see a regressed status.\n const fields: FieldSet = {\n \"Report ID\": input.reportId,\n Site: [input.siteId],\n \"Report type\": input.reportType,\n \"Period start\": ymd(input.periodStart),\n \"Period end\": ymd(input.periodEnd),\n \"Completed on\": ymd(input.completedOn),\n \"Lighthouse — Performance\": input.lighthouse.performance,\n \"Lighthouse — Accessibility\": input.lighthouse.accessibility,\n \"Lighthouse — Best Practices\": input.lighthouse.bestPractices,\n \"Lighthouse — SEO\": input.lighthouse.seo,\n \"Delivery status\": \"pending\",\n };\n if (input.lastTestedDate) fields[\"Last tested date\"] = ymd(input.lastTestedDate);\n // GA fields are written only when supplied (GA configured + fetch succeeded). When\n // omitted the row keeps them blank for manual entry — the pre-GA behavior.\n if (input.gaUsersCurrent !== undefined) fields[\"GA users (period)\"] = input.gaUsersCurrent;\n if (input.gaUsersPrevious !== undefined) fields[\"GA users (prev period)\"] = input.gaUsersPrevious;\n if (input.searchFoundPage1 !== undefined) fields[\"Search found page 1\"] = input.searchFoundPage1;\n if (input.searchPosition !== undefined) fields[\"Search position\"] = input.searchPosition;\n if (input.period !== undefined) fields[\"Period\"] = input.period;\n const created = (await base(REPORTS_TABLE).create([{ fields }])) as Records<FieldSet>;\n const rec = created[0];\n if (!rec) throw new Error(\"Airtable create returned no records\");\n return mapRow({ id: rec.id, fields: rec.fields });\n}\n\nexport async function setDraftReady(\n base: AirtableBase,\n recordId: string,\n ready: boolean,\n): Promise<void> {\n await base(REPORTS_TABLE).update([{ id: recordId, fields: { \"Draft ready\": ready } }]);\n}\n\n/**\n * Overwrite the four `Lighthouse — *` score cells (and, when supplied, `Completed\n * on`) on an EXISTING Reports row. The launch re-run path uses this: it reuses the\n * already-created Launch row but must refresh its scores to match the freshly-run\n * audit — otherwise the re-rendered preview shows new scores while the row (and the\n * eventually-sent email, which reads the row) keeps the stale ones. The create path\n * already writes fresh scores via `createDraft`; this is its update-side mirror, using\n * the same exact field names so the two stay in lockstep.\n */\nexport async function updateReportScores(\n base: AirtableBase,\n recordId: string,\n scores: LighthouseScores,\n completedOn?: Date,\n): Promise<void> {\n const fields: FieldSet = {\n \"Lighthouse — Performance\": scores.performance,\n \"Lighthouse — Accessibility\": scores.accessibility,\n \"Lighthouse — Best Practices\": scores.bestPractices,\n \"Lighthouse — SEO\": scores.seo,\n };\n if (completedOn) fields[\"Completed on\"] = ymd(completedOn);\n await base(REPORTS_TABLE).update([{ id: recordId, fields }]);\n}\n\nexport async function listSendableReports(base: AirtableBase): Promise<ReportRow[]> {\n const out: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({\n filterByFormula:\n \"AND({Draft ready} = TRUE(), {Approved to send} = TRUE(), {Sent at} = BLANK())\",\n pageSize: 100,\n })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\n/**\n * Fetch every Reports row, unfiltered. Site-scoped callers filter the result in\n * memory: the `Site` linked-record field CANNOT be formula-filtered by record id\n * (see findReportByPeriod's doc for why), and the fleet's Reports table is small\n * enough that one paged fetch-all beats N broken-or-per-site queries.\n */\nexport async function listAllReports(base: AirtableBase): Promise<ReportRow[]> {\n const out: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({ pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\nexport async function listReportsForSite(base: AirtableBase, siteId: string): Promise<ReportRow[]> {\n // Client-side match on the mapped siteId (mapRow reads the record id from the\n // REST response, where it IS present) — record ids can't appear in formulas.\n return (await listAllReports(base)).filter((r) => r.siteId === siteId);\n}\n\n/**\n * Mark a row as sent: write `Sent at` and (when known) `Resend message ID`.\n * Crucially does NOT touch `Delivery status` — that's set to \"pending\" in\n * createDraft and updated by the webhook from there. If we wrote \"pending\" here\n * we could clobber a \"delivered\" that the webhook raced ahead and wrote first (H4).\n *\n * `messageId` may be `null`: the 409 idempotency-conflict path has no recoverable\n * id (the original send's id was lost when the prior run's stamp failed), so it\n * passes null and we write ONLY `Sent at`. Writing a sentinel string like\n * \"idempotent-conflict\" into the id column would masquerade as a real Resend id\n * and silently orphan `findReportByMessageId` lookups; leaving it null is honest —\n * the row still stops replaying (Sent at is set), delivery tracking for that one\n * report is simply degraded (the id is genuinely unknowable on that path).\n */\nexport async function stampSent(\n base: AirtableBase,\n recordId: string,\n sentAt: Date,\n messageId: string | null,\n): Promise<void> {\n const fields: Record<string, string> = { \"Sent at\": sentAt.toISOString() };\n if (messageId !== null) fields[\"Resend message ID\"] = messageId;\n await base(REPORTS_TABLE).update([\n {\n id: recordId,\n fields,\n },\n ]);\n}\n\nexport async function setDeliveryStatus(\n base: AirtableBase,\n recordId: string,\n status: DeliveryStatus,\n): Promise<void> {\n await base(REPORTS_TABLE).update([{ id: recordId, fields: { \"Delivery status\": status } }]);\n}\n\n/**\n * Stamp the approval on a Reports row: flips `Approved to send` TRUE and records\n * who/when for the audit trail. The caller (approveReport handler) is responsible\n * for idempotency — this is the raw write. Never touches `Sent at`.\n */\nexport async function approveReportRow(\n base: AirtableBase,\n recordId: string,\n approvedAt: Date,\n approvedBy: string,\n): Promise<void> {\n await base(REPORTS_TABLE).update([\n {\n id: recordId,\n fields: {\n \"Approved to send\": true,\n \"Approved At\": approvedAt.toISOString(),\n \"Approved By\": approvedBy,\n },\n },\n ]);\n}\n\n/**\n * True when an `.find` rejection is a GENUINE not-found, not a transient failure.\n * The Airtable SDK stamps `.statusCode` (404) and/or `.error` (\"NOT_FOUND\") on\n * its errors. Anything else (429 rate-limit, 500 outage, bad-PAT 401, network\n * error) must NOT be masked as a 404 — see getReportById.\n */\nfunction isNotFoundError(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) return false;\n const e = err as { statusCode?: unknown; error?: unknown; name?: unknown; message?: unknown };\n if (e.statusCode === 404) return true;\n const tag = String(e.error ?? e.name ?? e.message ?? \"\");\n return tag === \"NOT_FOUND\" || /not found/i.test(tag);\n}\n\n/**\n * Fetch one Reports row by its Airtable record id, or null if it doesn't exist.\n * Only a GENUINE not-found (404 / NOT_FOUND) collapses to null; every other\n * failure (outage, 429, bad PAT, network error) is rethrown so the adapter\n * surfaces a 500 instead of a misleading 404. Swallowing all throws previously\n * turned an Airtable outage into a \"no such report\".\n */\nexport async function getReportById(\n base: AirtableBase,\n recordId: string,\n): Promise<ReportRow | null> {\n try {\n const rec = await base(REPORTS_TABLE).find(recordId);\n return mapRow({ id: rec.id, fields: rec.fields as Record<string, unknown> });\n } catch (err) {\n if (isNotFoundError(err)) return null;\n throw err;\n }\n}\n\nexport async function findReportByMessageId(\n base: AirtableBase,\n messageId: string,\n): Promise<ReportRow | null> {\n const rows: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({\n filterByFormula: `{Resend message ID} = \"${escapeFormulaString(messageId)}\"`,\n maxRecords: 1,\n })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return rows[0] ?? null;\n}\n\n/**\n * Find the Reports row for a `(site, reportType, period)` triple, or null. The\n * idempotency lookup behind search-before-create drafting.\n *\n * The site is matched CLIENT-side, never in the formula: Airtable's formula layer\n * renders linked-record fields ({Site}) as the linked rows' PRIMARY-FIELD NAMES,\n * not record ids, so any formula comparing {Site} or ARRAYJOIN({Site}) against a\n * `recXXX` id matches NOTHING (live-proven against the real base — do not\n * reintroduce that idiom). Record ids exist only in the REST response, where\n * mapRow reads them. So the formula filters on the real scalar fields (Report\n * type + Period — escaped, keeping it injection-safe if their source ever\n * changes), and the first mapped row whose siteId matches wins. The candidate\n * set is at most one row per site for the (type, period), so this stays small.\n */\nexport async function findReportByPeriod(\n base: AirtableBase,\n siteId: string,\n reportType: ReportType,\n period: string,\n): Promise<ReportRow | null> {\n const safeType = escapeFormulaString(reportType);\n const safePeriod = escapeFormulaString(period);\n const formula = `AND({Report type} = \"${safeType}\", {Period} = \"${safePeriod}\")`;\n const rows: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({ filterByFormula: formula, pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return rows.find((r) => r.siteId === siteId) ?? null;\n}\n","/** Cheap HTML sniff: an Airtable signed-URL \"200\" that is really a login/error page\n * starts with `<!doctype html`, `<html`, or `<head` after an optional UTF-8 BOM /\n * leading whitespace. We only need to catch the common error-page case, not parse\n * HTML. */\nfunction looksLikeHtml(bytes: Uint8Array): boolean {\n // Inspect the first ~64 bytes as ASCII (1 byte → 1 char; enough for a doctype /\n // opening tag). Skip a leading UTF-8 BOM (bytes EF BB BF) by index, then strip any\n // leading ASCII whitespace, and match the common HTML openers case-insensitively.\n const start = bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ? 3 : 0;\n const head = Buffer.from(bytes.slice(start, start + 64))\n .toString(\"ascii\")\n .replace(/^[\\s]+/, \"\")\n .toLowerCase();\n return head.startsWith(\"<!doctype html\") || head.startsWith(\"<html\") || head.startsWith(\"<head\");\n}\n\nexport async function fetchAttachmentBytes(\n url: string,\n): Promise<{ bytes: Uint8Array; contentType: string }> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch Airtable attachment ${res.status} ${res.statusText} (url=${url})`,\n );\n }\n const contentType = res.headers.get(\"content-type\") ?? \"application/octet-stream\";\n const ab = await res.arrayBuffer();\n const bytes = new Uint8Array(ab);\n // Sanity-gate the body: a 200 that is actually an HTML error/login page (expired\n // signed URL, auth wall) would otherwise be attached as the \"image\" and ship a\n // broken header. Accept an explicit image/* content-type; otherwise reject anything\n // that sniffs as HTML — so the send fails loudly rather than emailing a broken image.\n const isImageType = contentType.toLowerCase().startsWith(\"image/\");\n if (!isImageType && looksLikeHtml(bytes)) {\n throw new Error(\n `Airtable attachment did not return image data (content-type=\"${contentType}\", ` +\n `body looks like an HTML page — the signed URL may have expired) (url=${url})`,\n );\n }\n return { bytes, contentType };\n}\n\n/**\n * Upload bytes (or a string) as an attachment to a specific record + field.\n * Uses Airtable's content.airtable.com upload endpoint (base64 body) because\n * the standard SDK only accepts public URLs for attachments, and we don't\n * host the generated content anywhere public.\n *\n * Docs: https://airtable.com/developers/web/api/upload-attachment\n *\n * Requires AIRTABLE_PAT + AIRTABLE_BASE_ID in env (same as the rest of the\n * reports module). The fieldName is URL-encoded for the request path.\n */\nexport async function uploadAttachment(\n recordId: string,\n fieldName: string,\n body: Uint8Array | string,\n filename: string,\n contentType: string,\n): Promise<void> {\n const apiKey = process.env.AIRTABLE_PAT;\n const baseId = process.env.AIRTABLE_BASE_ID;\n if (!apiKey || !baseId) {\n throw new Error(\"AIRTABLE_PAT and AIRTABLE_BASE_ID must be set\");\n }\n const base64 =\n typeof body === \"string\"\n ? Buffer.from(body, \"utf-8\").toString(\"base64\")\n : Buffer.from(body).toString(\"base64\");\n const payload = { contentType, file: base64, filename };\n const url = `https://content.airtable.com/v0/${baseId}/${recordId}/${encodeURIComponent(fieldName)}/uploadAttachment`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n if (!res.ok) {\n throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);\n }\n}\n","import { dirname, join } from \"node:path\";\nimport { defaultCredentialsPath } from \"../../util/credentials.js\";\n\nexport type GaConfig = {\n /** Workspace user the service account impersonates (domain-wide delegation). */\n subject: string;\n /** Absolute path to the service-account JSON key file. */\n keyPath: string;\n};\n\n/**\n * Read GA configuration from the environment (credentials.env is already loaded into\n * process.env by the CLI entrypoint). Returns null when `GA_SUBJECT` is unset — the\n * signal that GA enrichment is simply not configured, so drafting skips it silently.\n *\n * `GA_SA_KEY_PATH` is optional; it defaults to `ga-service-account.json` alongside the\n * credentials file (e.g. ~/.config/reddoor-maint/), keeping the key out of the repo.\n */\nexport function readGaConfig(): GaConfig | null {\n const subject = process.env.GA_SUBJECT?.trim();\n if (!subject) return null;\n const keyPath =\n process.env.GA_SA_KEY_PATH?.trim() ||\n join(dirname(defaultCredentialsPath()), \"ga-service-account.json\");\n return { subject, keyPath };\n}\n","import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** Resolve the canonical credentials file path. Respects $XDG_CONFIG_HOME\n * (Linux/macOS convention) and falls back to ~/.config/reddoor-maint/. */\nexport function defaultCredentialsPath(): string {\n const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\");\n return join(base, \"reddoor-maint\", \"credentials.env\");\n}\n\n/** Parse a tiny subset of dotenv: `KEY=value` per line, `# comments`,\n * blank lines. A leading `export ` token is stripped (dotenv does this),\n * so a hand-edited `export AIRTABLE_PAT=…` parses instead of being dropped.\n * Quoted values strip the surrounding quotes. A non-blank, non-comment line\n * that still doesn't parse (no `=`, bad key) is skipped with a one-line\n * stderr warning naming the line number — this is a credentials file, so a\n * silent drop turns into a confusing \"missing credential\" downstream. */\nexport function parseEnvFile(contents: string): Record<string, string> {\n const out: Record<string, string> = {};\n const lines = contents.split(/\\r?\\n/);\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n // Strip a leading `export ` so `export KEY=value` (a common hand-edit)\n // parses the same as `KEY=value`.\n const line = trimmed.replace(/^export\\s+/, \"\");\n const eq = line.indexOf(\"=\");\n const key = eq > 0 ? line.slice(0, eq).trim() : \"\";\n if (eq <= 0 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n console.warn(`credentials: skipping unparseable line ${i + 1}: ${trimmed}`);\n continue;\n }\n let value = line.slice(eq + 1).trim();\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n out[key] = value;\n }\n return out;\n}\n\n/** Load credentials from `path` (default: canonical file) into `process.env`.\n * `process.env` values win — file-defined keys are only applied when the\n * env var is currently undefined. Missing/unreadable file is a silent\n * no-op; commands that need the credentials will fail downstream with\n * their own clear error. Returns the keys actually applied (diagnostics). */\nexport function loadCredentialsIntoEnv(path: string = defaultCredentialsPath()): string[] {\n let contents: string;\n try {\n contents = readFileSync(path, \"utf-8\");\n } catch {\n return [];\n }\n const parsed = parseEnvFile(contents);\n const applied: string[] = [];\n for (const [k, v] of Object.entries(parsed)) {\n if (process.env[k] === undefined) {\n process.env[k] = v;\n applied.push(k);\n }\n }\n return applied;\n}\n","import { readFileSync } from \"node:fs\";\nimport { JWT } from \"google-auth-library\";\nimport { BetaAnalyticsDataClient } from \"@google-analytics/data\";\n\nconst ANALYTICS_READONLY = \"https://www.googleapis.com/auth/analytics.readonly\";\nconst MS_PER_DAY = 86_400_000;\n\nexport type GaQuery = {\n /** GA4 numeric property ID (e.g. \"471880366\"). */\n propertyId: string;\n /** Workspace user to impersonate via domain-wide delegation. */\n subject: string;\n /** Path to the service-account JSON key. */\n keyPath: string;\n};\n\n/** UTC YYYY-MM-DD — matches the rest of the reports pipeline's date handling. */\nfunction ymd(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Fetch GA4 `activeUsers` (\"Users\") for a report period and the equal-length window\n * immediately before it, via a domain-wide-delegation service account that impersonates\n * `subject`. Throws on any auth/API error — the caller (draftReportForSite) soft-fails.\n *\n * Previous window: same length as the current period, ending the day before `periodStart`.\n */\nexport async function fetchPeriodUsers(\n query: GaQuery,\n periodStart: Date,\n periodEnd: Date,\n): Promise<{ current: number; previous: number }> {\n const key = JSON.parse(readFileSync(query.keyPath, \"utf8\")) as {\n client_email: string;\n private_key: string;\n };\n const authClient = new JWT({\n email: key.client_email,\n key: key.private_key,\n scopes: [ANALYTICS_READONLY],\n subject: query.subject,\n });\n const client = new BetaAnalyticsDataClient({ authClient });\n\n const lengthDays = Math.round((periodEnd.getTime() - periodStart.getTime()) / MS_PER_DAY);\n const prevEnd = new Date(periodStart.getTime() - MS_PER_DAY);\n const prevStart = new Date(prevEnd.getTime() - lengthDays * MS_PER_DAY);\n\n const property = `properties/${query.propertyId}`;\n const run = async (start: Date, end: Date): Promise<number> => {\n const [resp] = await client.runReport({\n property,\n dateRanges: [{ startDate: ymd(start), endDate: ymd(end) }],\n metrics: [{ name: \"activeUsers\" }],\n });\n const raw = resp.rows?.[0]?.metricValues?.[0]?.value ?? \"0\";\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) ? n : 0;\n };\n\n const current = await run(periodStart, periodEnd);\n const previous = await run(prevStart, prevEnd);\n return { current, previous };\n}\n","import { readFileSync } from \"node:fs\";\nimport { JWT } from \"google-auth-library\";\n\nconst WEBMASTERS_READONLY = \"https://www.googleapis.com/auth/webmasters.readonly\";\nconst SC_BASE = \"https://searchconsole.googleapis.com/webmasters/v3\";\n/** Average-position threshold for \"on page 1\" (10 organic results per page). */\nconst PAGE_1_MAX_POSITION = 10;\n\nexport type SearchPresenceQuery = {\n /** Path to the service-account JSON key (same one GA uses). */\n keyPath: string;\n /** Workspace user to impersonate via domain-wide delegation. */\n subject: string;\n /** Explicit Search Console property (`sc-domain:...` or `https://.../`). Overrides auto-resolution. */\n property?: string | undefined;\n /** Site host, used to auto-resolve the property from `sites.list` when `property` is absent. */\n host: string;\n /** Operator-supplied query string (e.g. the business name). */\n query: string;\n};\n\nexport type SearchPresence = {\n /** True when the average position for the query is on page 1 (<= 10). */\n foundOnPage1: boolean;\n /** Rounded average position, or null when not found / no data. */\n position: number | null;\n};\n\ntype SiteEntry = { siteUrl: string };\n\n/** Reduce any property string or URL to a bare host: no `sc-domain:`, scheme, `www.`, path, lowercased. */\nexport function bareHost(s: string): string {\n return s\n .trim()\n .replace(/^sc-domain:/i, \"\")\n .replace(/^https?:\\/\\//i, \"\")\n .split(\"/\")[0]!\n .replace(/^www\\./i, \"\")\n .toLowerCase();\n}\n\n/**\n * All Search Console properties matching `host`, ordered for query fallback: Domain\n * (`sc-domain:`) forms first (broadest coverage), then URL-prefix forms. A site can be verified\n * as both; a freshly-created Domain property has no backfilled history, so its data can be empty\n * even while a long-lived URL-prefix property has data — hence we return every match and let the\n * caller try them in order until one returns data. Empty list = nothing matches.\n */\nexport function resolvePropertyCandidates(entries: SiteEntry[], host: string): string[] {\n const target = bareHost(host);\n const matches = entries.filter((e) => bareHost(e.siteUrl) === target).map((e) => e.siteUrl);\n const domains = matches.filter((s) => s.toLowerCase().startsWith(\"sc-domain:\"));\n const prefixes = matches.filter((s) => !s.toLowerCase().startsWith(\"sc-domain:\"));\n return [...domains, ...prefixes];\n}\n\n/** UTC YYYY-MM-DD — matches the rest of the reports pipeline. */\nfunction ymd(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Query Google Search Console for the average position of `query` on the site over the report\n * period, via a domain-wide-delegation service account impersonating `subject`. Uses `property`\n * verbatim when given (operator's choice is final — no fallback); otherwise auto-discovers all\n * matching properties via `sites.list` and tries them in order (Domain first) until one returns\n * data. Throws on any auth/API error — the caller (draftReportForSite) soft-fails.\n */\nexport async function fetchSearchPresence(\n q: SearchPresenceQuery,\n periodStart: Date,\n periodEnd: Date,\n): Promise<SearchPresence> {\n const key = JSON.parse(readFileSync(q.keyPath, \"utf8\")) as {\n client_email: string;\n private_key: string;\n };\n const jwt = new JWT({\n email: key.client_email,\n key: key.private_key,\n scopes: [WEBMASTERS_READONLY],\n subject: q.subject,\n });\n\n const explicit = q.property?.trim();\n let candidates: string[];\n if (explicit) {\n candidates = [explicit];\n } else {\n const list = await jwt.request<{ siteEntry?: SiteEntry[] }>({\n url: `${SC_BASE}/sites`,\n method: \"GET\",\n });\n candidates = resolvePropertyCandidates(list.data.siteEntry ?? [], q.host);\n if (candidates.length === 0) return { foundOnPage1: false, position: null };\n }\n\n for (const property of candidates) {\n const res = await jwt.request<{ rows?: Array<{ position?: number }> }>({\n url: `${SC_BASE}/sites/${encodeURIComponent(property)}/searchAnalytics/query`,\n method: \"POST\",\n data: {\n startDate: ymd(periodStart),\n endDate: ymd(periodEnd),\n dimensions: [\"query\"],\n dimensionFilterGroups: [\n {\n filters: [\n { dimension: \"query\", operator: \"equals\", expression: q.query.toLowerCase() },\n ],\n },\n ],\n rowLimit: 1,\n },\n });\n const pos = res.data.rows?.[0]?.position;\n if (typeof pos === \"number\") {\n // Search Console can average below 1; floor to 1 so the template never\n // renders a nonsensical \"#0\" (positions are 1-indexed).\n return { foundOnPage1: pos <= PAGE_1_MAX_POSITION, position: Math.max(1, Math.round(pos)) };\n }\n }\n return { foundOnPage1: false, position: null };\n}\n","import Airtable from \"airtable\";\nimport { defaultCredentialsPath } from \"../../util/credentials.js\";\n\nexport type AirtableConfig = {\n apiKey: string;\n baseId: string;\n};\n\nfunction missing(name: string): Error {\n return Object.assign(\n new Error(\n `${name} not set. Export it in your shell or put it in ${defaultCredentialsPath()} as ${name}=...`,\n ),\n { exitCode: 2 },\n );\n}\n\nexport function readAirtableConfig(): AirtableConfig {\n const apiKey = process.env.AIRTABLE_PAT;\n const baseId = process.env.AIRTABLE_BASE_ID;\n if (!apiKey) throw missing(\"AIRTABLE_PAT\");\n if (!baseId) throw missing(\"AIRTABLE_BASE_ID\");\n return { apiKey, baseId };\n}\n\nexport type AirtableBase = ReturnType<typeof openBase>;\n\nexport function openBase(cfg: AirtableConfig) {\n return new Airtable({ apiKey: cfg.apiKey }).base(cfg.baseId);\n}\n","import sharp from \"sharp\";\n\nexport type PreparedHeaderImage = {\n /** Resized JPEG bytes to attach inline (CID) in place of the Airtable original. */\n bytes: Uint8Array;\n /** Always \"image/jpeg\" — we re-encode for predictable size and a flat white background. */\n contentType: string;\n /** CSS display width in px (≤ requested, never wider than the source has pixels for). */\n displayWidth: number;\n /** CSS display height in px, source aspect ratio preserved (no distortion). */\n displayHeight: number;\n /** Dominant-color hex (e.g. \"#cfc3a8\"), used as the loading/blocked placeholder box. */\n placeholderColor: string;\n};\n\nexport type PrepareHeaderImageOptions = {\n /** Intended CSS display width. The email body is 600px, so that's the default. */\n displayWidth?: number;\n};\n\nconst DEFAULT_DISPLAY_WIDTH = 600;\n/** Encode the source at 2× display width so it stays crisp on retina screens. */\nconst RETINA_SCALE = 2;\n/** Quality is for *resized* pixels — at 1200px the texture/text read as sharp; bytes are tiny. */\nconst JPEG_QUALITY = 82;\n\nfunction channelToHex(value: number): string {\n return Math.max(0, Math.min(255, Math.round(value)))\n .toString(16)\n .padStart(2, \"0\");\n}\n\n/**\n * Downscale an oversized header image for email: 2× the display width (retina) at most,\n * never upscaled, re-encoded as JPEG on a flat white background. Also reports the display\n * dimensions (so the template can reserve the box and stop reflow) and a dominant color\n * (so the reserved box shows a matched placeholder while the image loads).\n *\n * Root cause this addresses: Airtable headers can be multi-MB / 2400px+ while the email\n * renders them at ~600px — shipping ~16× more pixels than the display can use.\n */\nexport async function prepareHeaderImage(\n bytes: Uint8Array,\n options: PrepareHeaderImageOptions = {},\n): Promise<PreparedHeaderImage> {\n const requestedDisplayWidth = options.displayWidth ?? DEFAULT_DISPLAY_WIDTH;\n const input = Buffer.from(bytes);\n\n const meta = await sharp(input).metadata();\n const origWidth = meta.width;\n const origHeight = meta.height;\n if (!origWidth || !origHeight) {\n throw new Error(\"prepareHeaderImage: could not read source image dimensions\");\n }\n\n // Never claim a wider display than the source can fill at 1×.\n const displayWidth = Math.min(requestedDisplayWidth, origWidth);\n const displayHeight = Math.round((displayWidth * origHeight) / origWidth);\n\n // Encode at 2× display for retina, but never enlarge a smaller original.\n const targetSourceWidth = Math.min(origWidth, displayWidth * RETINA_SCALE);\n\n const out = await sharp(input)\n .resize({ width: targetSourceWidth, withoutEnlargement: true })\n .flatten({ background: \"#ffffff\" })\n .jpeg({ quality: JPEG_QUALITY })\n .toBuffer();\n\n const { dominant } = await sharp(out).stats();\n const placeholderColor = `#${channelToHex(dominant.r)}${channelToHex(dominant.g)}${channelToHex(dominant.b)}`;\n\n return {\n bytes: new Uint8Array(out),\n contentType: \"image/jpeg\",\n displayWidth,\n displayHeight,\n placeholderColor,\n };\n}\n","import { Resend } from \"resend\";\n\nexport type ResendSendInput = {\n from: string;\n to: string[];\n cc?: string[];\n replyTo?: string;\n subject: string;\n html: string;\n attachments?: Array<{\n filename: string;\n content: string; // base64\n contentType?: string;\n /** Setting this attaches the file as inline; reference it from HTML as `src=\"cid:<id>\"`. */\n inlineContentId?: string;\n }>;\n /**\n * Stable key forwarded as the `Idempotency-Key` header. Resend dedupes calls\n * with the same key for 24 hours, returning the original message id. Use a\n * key that's stable across retries of the same logical send (e.g. the\n * Reports row id), so a network blip during stamping doesn't cause a\n * duplicate email to the client.\n */\n idempotencyKey?: string;\n};\n\nexport type ResendSendResult = {\n messageId: string;\n};\n\nexport type ResendClient = {\n send: (input: ResendSendInput) => Promise<ResendSendResult>;\n};\n\nexport function defaultResendClient(): ResendClient {\n const key = process.env.RESEND_API_KEY;\n if (!key) throw Object.assign(new Error(\"RESEND_API_KEY not set\"), { exitCode: 2 });\n const resend = new Resend(key);\n return {\n async send(input) {\n const payload: Parameters<typeof resend.emails.send>[0] = {\n from: input.from,\n to: input.to,\n subject: input.subject,\n html: input.html,\n };\n if (input.cc) payload.cc = input.cc;\n if (input.replyTo) payload.replyTo = input.replyTo;\n if (input.attachments) payload.attachments = input.attachments;\n const options: Parameters<typeof resend.emails.send>[1] = {};\n if (input.idempotencyKey) options.idempotencyKey = input.idempotencyKey;\n const { data, error } = await resend.emails.send(payload, options);\n if (error) throw new Error(`Resend error: ${error.message}`);\n if (!data?.id) throw new Error(\"Resend returned no message id\");\n return { messageId: data.id };\n },\n };\n}\n","/**\n * True when a thrown send error is Resend's same-key + DIFFERENT-body 409\n * (`invalid_idempotent_request`). The ResendClient (send/resend.ts) discards the\n * status/name and only surfaces the message string, so we match defensively on the\n * stable message substring \"idempotency key has been used\" (case-insensitive); a\n * `name`/`statusCode` of 409/`invalid_idempotent_request` is also accepted if a\n * future client happens to preserve it. A same-key + SAME-body re-send is deduped\n * by Resend (returns the original id) and never reaches here.\n *\n * Shared by both send surfaces that key into Resend's idempotency window:\n * `runDigest` (digest.ts, `digest-<date>` key) and `sendOne` (orchestrate.ts,\n * `report:<id>` key). Both treat a 409 as \"the email already went out under this\n * key on a prior run\" — a no-op for the digest, an already-done success for sendOne.\n */\nexport function isIdempotencyConflict(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n if (/idempotency key has been used/i.test(message)) return true;\n const e = err as { name?: unknown; statusCode?: unknown };\n if (e.name === \"invalid_idempotent_request\") return true;\n if (e.statusCode === 409) return true;\n return false;\n}\n","import { openBase, readAirtableConfig } from \"../airtable/client.js\";\nimport { listSendableReports, stampSent } from \"../airtable/reports.js\";\nimport { listWebsites, siteSlug, updateLaunched } from \"../airtable/websites.js\";\nimport type { WebsiteRow } from \"../airtable/websites.js\";\nimport type { ReportRow } from \"../airtable/reports.js\";\nimport { fetchAttachmentBytes } from \"../airtable/attachments.js\";\nimport { renderReportHtml } from \"../render.js\";\nimport { resolveCopy } from \"../copy.js\";\nimport { loadBundledImages } from \"../maintenance-email/assets/index.js\";\nimport { prepareHeaderImage } from \"../maintenance-email/header-image.js\";\nimport { defaultResendClient, type ResendClient, type ResendSendInput } from \"./resend.js\";\nimport { isIdempotencyConflict } from \"./idempotency.js\";\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\nconst REPLY_TO = \"info@reddoorla.com\";\n\nconst MONTHS = [\n \"January\",\n \"February\",\n \"March\",\n \"April\",\n \"May\",\n \"June\",\n \"July\",\n \"August\",\n \"September\",\n \"October\",\n \"November\",\n \"December\",\n];\n\n/** \"May 2026\" — UTC month/year, consistent with the rest of the reports pipeline's dates. */\nfunction monthYear(d: Date): string {\n return `${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}`;\n}\n\ntype InlineAttachment = NonNullable<ResendSendInput[\"attachments\"]>[number];\n\n/** Build a Resend inline (CID-referenced) attachment from raw bytes — the header\n * image and both bundled images share this exact shape. */\nfunction toInlineAttachment(a: {\n bytes: Uint8Array;\n filename: string;\n contentType: string;\n cid: string;\n}): InlineAttachment {\n return {\n filename: a.filename,\n content: Buffer.from(a.bytes).toString(\"base64\"),\n contentType: a.contentType,\n inlineContentId: a.cid,\n };\n}\n\nexport type OrchestrateOptions = {\n resend?: ResendClient;\n};\n\nexport async function sendApprovedReports(\n options: OrchestrateOptions = {},\n): Promise<{ output: string; code: number }> {\n const base = openBase(readAirtableConfig());\n const client = options.resend ?? defaultResendClient();\n\n const sendable = await listSendableReports(base);\n if (sendable.length === 0) return { output: \"No reports ready to send.\", code: 0 };\n\n const websites = await listWebsites(base);\n const sites = new Map(websites.map((w) => [w.id, w]));\n\n const lines: string[] = [];\n let anyFailed = false;\n for (const report of sendable) {\n const site = sites.get(report.siteId);\n if (!site) {\n lines.push(`✗ ${report.reportId} — Site row not found for id=${report.siteId}`);\n anyFailed = true;\n continue;\n }\n try {\n const messageId = await sendOne(client, base, site, report);\n lines.push(`✓ sent: ${report.reportId} (${messageId})`);\n if (report.reportType === \"Launch\") {\n try {\n await updateLaunched(base, site.id, new Date().toISOString());\n lines.push(` ↳ launched: ${site.name} flipped to maintenance`);\n } catch (e) {\n lines.push(` ⚠ launch flip failed for ${site.name}: ${(e as Error).message}`);\n }\n }\n } catch (e) {\n lines.push(`✗ ${report.reportId} — ${(e as Error).message}`);\n anyFailed = true;\n }\n }\n return { output: lines.join(\"\\n\"), code: anyFailed ? 1 : 0 };\n}\n\nasync function sendOne(\n client: ResendClient,\n base: ReturnType<typeof openBase>,\n site: WebsiteRow,\n report: ReportRow,\n): Promise<string> {\n if (!site.headerImage) {\n throw new Error(`Site '${site.name}' has no Header image set on the Websites row`);\n }\n if (!report.lighthouse) {\n throw new Error(\n `Report ${report.reportId} has no Lighthouse scores — all four cells ` +\n `(Lighthouse — Performance / Accessibility / Best Practices / SEO) must be numeric ` +\n `on the Reports row; one non-numeric or blank cell nulls all four`,\n );\n }\n\n // Resolve + validate recipients BEFORE the expensive work (header fetch + sharp\n // downscale + full MJML render). A misconfigured-recipients site is a guaranteed\n // failure, so fail fast here rather than after burning that work. Same checks +\n // messages as before — only the position moved.\n const explicitTo = parseAddresses(site.reportRecipientsTo);\n // Run pointOfContact through the parser too — operators sometimes paste\n // \"a@x, b@y\" into that single-line field.\n const fallbackTo = parseAddresses(site.pointOfContact);\n const to = explicitTo ?? fallbackTo ?? [];\n if (to.length === 0) {\n throw new Error(\n `Site '${site.name}' has no recipients (Report recipients (To) AND point of contact are both empty)`,\n );\n }\n for (const addr of to) {\n if (!isProbablyEmail(addr)) {\n throw new Error(\n `Site '${site.name}' recipient is malformed: ${addr} — use a bare address only ` +\n `(no \\`Name <addr>\\` display-name syntax); fix Report recipients (To) or point of contact in Airtable`,\n );\n }\n }\n const cc = parseAddresses(site.reportRecipientsCc);\n if (cc) {\n for (const addr of cc) {\n if (!isProbablyEmail(addr)) {\n throw new Error(\n `Site '${site.name}' CC is malformed: ${addr} — fix Report recipients (CC) in Airtable`,\n );\n }\n }\n }\n\n const original = await fetchAttachmentBytes(site.headerImage.url);\n // Downscale the (often multi-MB / 2400px+) Airtable header to email display size, and get\n // back display dims + a placeholder color so the template can reserve the box.\n const header = await prepareHeaderImage(original.bytes);\n const bundled = await loadBundledImages();\n\n const slug = siteSlug(site.name);\n const cidName = `${slug}-header`;\n const { html } = await renderReportHtml({\n siteName: site.name,\n siteUrl: site.url,\n reportType: report.reportType,\n completedOn: report.completedOn ? new Date(report.completedOn) : new Date(),\n lighthouse: report.lighthouse,\n gaUsersCurrent: report.gaUsersCurrent ?? undefined,\n gaUsersPrevious: report.gaUsersPrevious ?? undefined,\n searchPosition:\n report.searchFoundPage1 && report.searchPosition !== null ? report.searchPosition : undefined,\n lastTestedDate: report.lastTestedDate ? new Date(report.lastTestedDate) : null,\n commentary: report.commentary,\n copy: resolveCopy(site),\n headerImageCid: cidName,\n headerWidth: header.displayWidth,\n headerHeight: header.displayHeight,\n headerBgColor: header.placeholderColor,\n });\n\n const reportDate = report.completedOn ? new Date(report.completedOn) : new Date();\n const subject =\n report.subjectOverride ?? `${site.name} — ${monthYear(reportDate)} ${report.reportType} Report`;\n\n const payload: Parameters<ResendClient[\"send\"]>[0] = {\n from: FROM_ADDRESS,\n to,\n replyTo: REPLY_TO,\n subject,\n html,\n attachments: [\n toInlineAttachment({\n bytes: header.bytes,\n filename: `${cidName}.jpg`,\n contentType: header.contentType,\n cid: cidName,\n }),\n // Bundled images referenced via cid:rd-check-png / cid:rd-blurred-tests-jpg\n // in the template. Attached inline so the email is self-contained — no\n // external CDN dependency, no image-blocked broken icons in webmail.\n toInlineAttachment({\n bytes: bundled.check.bytes,\n filename: bundled.check.filename,\n contentType: bundled.check.contentType,\n cid: bundled.check.cid,\n }),\n toInlineAttachment({\n bytes: bundled.blurred.bytes,\n filename: bundled.blurred.filename,\n contentType: bundled.blurred.contentType,\n cid: bundled.blurred.cid,\n }),\n ],\n // Stable across retries of the same row — if Airtable stamping fails after a\n // successful Resend, the next --send-ready replays with the same key and\n // Resend returns the original message id rather than sending a duplicate.\n idempotencyKey: `report:${report.id}`,\n };\n if (cc) payload.cc = cc;\n\n let result: Awaited<ReturnType<ResendClient[\"send\"]>>;\n try {\n result = await client.send(payload);\n } catch (err) {\n // The send path is at-least-once: client.send succeeds → stampSent writes\n // `Sent at` (the ONLY thing that removes the row from listSendableReports). If\n // stampSent threw on a PRIOR run (an Airtable blip), `Sent at` stayed null and\n // the row replays here. By replay time the rendered body has usually changed\n // (operator Commentary edit, `report --due` rewrote scores, or the header\n // re-encodes non-deterministically), so Resend rejects the same-key\n // (`report:<id>`) / different-body re-send with a 409 (`invalid_idempotent_request`).\n //\n // That 409 means the email ALREADY WENT OUT under this key on the prior run.\n // Do NOT re-throw and do NOT re-send (re-throwing leaves the row unstamped, and\n // after the 24h key TTL a SECOND real email would go out). Instead stamp the row\n // so it stops replaying, then return success so the caller runs the Launch flip —\n // which self-heals a launch that sent-but-never-flipped on the prior run.\n //\n // Any OTHER error (real network/Resend failure) re-throws, exactly as before, so\n // a genuine failure still fails loudly and the row replays next run.\n if (isIdempotencyConflict(err)) {\n // Stamp `Sent at` ONLY — the original send's messageId is unrecoverable on\n // the 409 path, so we leave `Resend message ID` null rather than writing a\n // sentinel that would masquerade as a real id and orphan webhook lookups.\n // Still return the sentinel string so the caller logs the already-sent path\n // and runs the Launch flip.\n await stampSent(base, report.id, new Date(), null);\n console.log(`↻ already sent (idempotency conflict), stamped: ${report.reportId}`);\n return \"idempotent-conflict\";\n }\n throw err;\n }\n await stampSent(base, report.id, new Date(), result.messageId);\n return result.messageId;\n}\n\n/**\n * Split a comma/newline-separated address field into a clean array.\n * Lowercases (case-insensitive dedupe) and removes empty entries. Returns\n * null if nothing survives. Does NOT understand `Display Name <email>` —\n * operators should put a bare address in the Airtable field, or use multiple\n * lines if needing multiple recipients.\n */\nexport function parseAddresses(field: string | null): string[] | null {\n if (!field) return null;\n const seen = new Set<string>();\n const list: string[] = [];\n for (const raw of field.split(/[,\\n]/)) {\n const trimmed = raw.trim().toLowerCase();\n if (!trimmed) continue;\n if (seen.has(trimmed)) continue;\n seen.add(trimmed);\n list.push(trimmed);\n }\n return list.length > 0 ? list : null;\n}\n\n/**\n * Cheap email shape check — must contain exactly one @, with non-empty\n * local and domain parts and at least one dot in the domain. We're not\n * trying to be a full RFC validator; we're trying to catch operator\n * mistakes like \"ops at acme dot com\" or a missing @ before they 422\n * at Resend.\n */\nexport function isProbablyEmail(s: string): boolean {\n const at = s.indexOf(\"@\");\n if (at < 1 || at !== s.lastIndexOf(\"@\")) return false;\n const local = s.slice(0, at);\n const domain = s.slice(at + 1);\n if (!local || !domain) return false;\n if (!domain.includes(\".\")) return false;\n if (/\\s/.test(s)) return false;\n return true;\n}\n","import type { WebsiteRow, Frequency, Status } from \"./airtable/websites.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport type { ReportType } from \"./types.js\";\n\n/** Statuses where reports are appropriate. Drops \"deprecated\" and\n * \"probably not our problem\" — even if the operator left a freq set, we don't\n * want to surface those sites in --due output. Sites with status=null pass\n * through (existing data is partial; better to surface than silently skip). */\nconst ELIGIBLE_STATUSES: ReadonlySet<Status> = new Set<Status>([\n \"in development\",\n \"launch period\",\n \"maintenance\",\n \"hosting\",\n]);\n\nexport type DueItem = {\n site: WebsiteRow;\n reportType: ReportType;\n /** Inclusive: the day the next report became due. */\n dueDate: Date;\n /** ISO date of the last `Sent at` for this (site, type), or null if there's never been one. */\n lastSent: string | null;\n};\n\nconst MONTHS: Record<Exclude<Frequency, \"None\">, number> = {\n Monthly: 1,\n Quarterly: 3,\n Yearly: 12,\n};\n\n/**\n * Add `n` calendar months in UTC, clamped to the last day of the target month.\n * Jan 31 + 1 month = Feb 28 (not Mar 3, which is what naive setMonth produces).\n * All-UTC accessors mean the result is timezone-independent.\n */\nfunction addMonths(d: Date, n: number): Date {\n const out = new Date(d);\n const day = out.getUTCDate();\n out.setUTCDate(1);\n out.setUTCMonth(out.getUTCMonth() + n);\n const lastDayOfTargetMonth = new Date(\n Date.UTC(out.getUTCFullYear(), out.getUTCMonth() + 1, 0),\n ).getUTCDate();\n out.setUTCDate(Math.min(day, lastDayOfTargetMonth));\n return out;\n}\n\n/** Truncate to UTC midnight. Avoids local-TZ skew when comparing Airtable date-only fields. */\nfunction startOfDay(d: Date): Date {\n const out = new Date(d);\n out.setUTCHours(0, 0, 0, 0);\n return out;\n}\n\nfunction lastSentForType(reports: ReportRow[], siteId: string, type: ReportType): string | null {\n const candidates = reports\n .filter((r) => r.siteId === siteId && r.reportType === type && r.sentAt !== null)\n .map((r) => r.sentAt!)\n .sort();\n return candidates[candidates.length - 1] ?? null;\n}\n\n/**\n * Computes which (site, type) pairs are due as of `today`.\n *\n * Algorithm per (site, type):\n * 1. If freq === \"None\", skip.\n * 2. baseDate = max(last Sent at for this type, site's `maintenance/testing day` fallback).\n * 3. If no baseDate exists at all, the site is due now.\n * 4. dueDate = baseDate + frequency months.\n * 5. Due iff startOfDay(today) >= startOfDay(dueDate).\n */\nexport function findDueReports(\n websites: WebsiteRow[],\n reports: ReportRow[],\n today: Date,\n): DueItem[] {\n const out: DueItem[] = [];\n const todayStart = startOfDay(today);\n\n for (const site of websites) {\n // Skip explicitly-non-active statuses (deprecated, \"probably not our problem\").\n // Null status is treated as active for backwards compat with rows that pre-date\n // the Status convention.\n if (site.status !== null && !ELIGIBLE_STATUSES.has(site.status)) continue;\n\n for (const type of [\"Maintenance\", \"Testing\"] as const) {\n const rawFreq = type === \"Maintenance\" ? site.maintenanceFreq : site.testingFreq;\n // Normalize obvious whitespace so a trailing-space typo (\"Quarterly \") still\n // schedules. The LOUD warning below is the real safety net for genuine\n // casing/spelling mistakes (\"monthly\", \"Quaterly\").\n const freq = (typeof rawFreq === \"string\" ? rawFreq.trim() : rawFreq) as Frequency;\n // Intentional silent skip — \"None\" (and the empty/blank default) means \"no\n // schedule\", not a mistake.\n if (freq === \"None\" || freq === (\"\" as Frequency)) continue;\n // A non-empty, non-None value that doesn't match a known schedule used to\n // silently produce no due date — the site just vanished from the loop. Warn\n // LOUDLY so a casing/typo Airtable value is fixable instead of invisible.\n if (!(freq in MONTHS)) {\n console.warn(\n `⚠ ${site.name}: unrecognized ${type === \"Maintenance\" ? \"maintenance\" : \"testing\"} frequency '${rawFreq}' — not scheduling; fix the Airtable value`,\n );\n continue;\n }\n\n const lastSent = lastSentForType(reports, site.id, type);\n const fallback = type === \"Maintenance\" ? site.maintenanceDay : site.testingDay;\n const baseIso = lastSent ?? fallback;\n\n if (!baseIso) {\n out.push({ site, reportType: type, dueDate: todayStart, lastSent });\n continue;\n }\n\n const dueDate = addMonths(new Date(baseIso), MONTHS[freq]);\n if (todayStart.getTime() >= startOfDay(dueDate).getTime()) {\n out.push({ site, reportType: type, dueDate, lastSent });\n }\n }\n }\n\n return out;\n}\n\n/**\n * The UTC `YYYY-MM` of a `dueDate` from {@link findDueReports} — the per-recurrence\n * idempotency key for drafting. Monthly recurrences land in distinct months; quarterly\n * and yearly land in distinct due-months too, so this uniquely names one draft per cycle.\n * UTC accessors keep it timezone-independent, consistent with the rest of this module.\n */\nexport function reportPeriodKey(dueDate: Date): string {\n if (Number.isNaN(dueDate.getTime())) throw new TypeError(\"reportPeriodKey: invalid Date\");\n const year = dueDate.getUTCFullYear();\n const month = String(dueDate.getUTCMonth() + 1).padStart(2, \"0\");\n return `${year}-${month}`;\n}\n","/** Render an absolute timestamp as a coarse \"Xd ago\" relative string for the\n * fleet card. Takes an explicit `now` for testability; defaults to wall clock\n * for callers (the Netlify function). Returns \"—\" for null / unparseable. */\nexport function relativeTimeFromNow(iso: string | null, now: Date = new Date()): string {\n if (!iso) return \"—\";\n const t = Date.parse(iso);\n if (Number.isNaN(t)) return \"—\";\n\n const seconds = Math.max(0, Math.floor((now.getTime() - t) / 1000));\n if (seconds < 60) return \"just now\";\n\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}d ago`;\n\n const weeks = Math.floor(days / 7);\n if (weeks < 4) return `${weeks}w ago`;\n\n const months = Math.floor(days / 30);\n return `${months}mo ago`;\n}\n","// The reddoor mark (32×32 PNG, ~554 B) inlined as a data-URI favicon. The\n// dashboard pages are rendered by Netlify functions with no static-asset\n// pipeline, so embedding the icon in the <head> brands every page without a\n// second request or a hosted file. Source: reddoor-website/static/favicon.png.\nconst REDDOOR_FAVICON_PNG_BASE64 =\n \"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAk1BMVEVHcEzkGTfjGDbjGTfjGTfjGTfjGTfjGDbjGDbjGTfjGTfjGTfkGTfjGTfjGDbjGDbkGTfjGDbjGDbjGTfjGTfjGDbjGTfjGDbjGTfjGDbjGTfjGTfjGDbjGDbjGDbjGDbjGTfjGTbjGDbjGDbjGDbjGDbjGTfjGTfjGTfjGDbjGDbjGTfjGDbjGDbjGTfjGTbkGTfxxbwzAAAALXRSTlMA4DABMOD8cOBwDltwi+bFLMlk54/BDVPk/I61/u2cFaBw9nqO1izBIQE+Becdo6eEAAABBElEQVQ4y91SCVYCMQwt4kxbcEHFfUFFcMGf9P6nM0kXEPEAmtdJs+cnU+f+He2fIIEAYuVgkmvDfTG9Rh+6LoRBUBqE7gi09l9eAeeTrZJoFd5uQWfj4VbPvRqwmj9zfzP6CYpyi48F6HW5A3WuMHsEPsfvO8cSkMPTe+pfRr/MTckdg+4enqL3PkYf/YFIqiiP8VAw6EZkH6QEO0xJLUhmdbY5+TjHkCVohprlcpzlnJw9GlNkrZC16uCmWIaAZItNrT4xV0T2yxrIWoNKy4rdVXOq0MycWp4r43FOMQWciibMYaOHCXKSPhapqTupGFAnwHoVuUUZq7jaSkqDb0/uD9MXqvJMDtU7lL0AAAAASUVORK5CYII=\";\n\n/** A ready-to-interpolate `<link rel=\"icon\">` carrying the reddoor mark. */\nexport const FAVICON_LINK = `<link rel=\"icon\" type=\"image/png\" href=\"data:image/png;base64,${REDDOOR_FAVICON_PNG_BASE64}\" />`;\n","import type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport type { ReportRow } from \"../reports/airtable/reports.js\";\nimport { isPendingApproval } from \"../reports/airtable/reports.js\";\nimport type { SubmissionRow } from \"../reports/airtable/submissions.js\";\nimport { relativeTimeFromNow } from \"./relative-time.js\";\nimport { escapeHtml, safeUrl } from \"../util/html.js\";\nimport { FAVICON_LINK } from \"./favicon.js\";\n\nfunction scoreTile(label: string, value: number | null): string {\n const display = value === null ? \"—\" : String(value);\n return `<div class=\"tile\"><div class=\"tile-value\">${escapeHtml(display)}</div><div class=\"tile-label\">${escapeHtml(label)}</div></div>`;\n}\n\nfunction healthTile(label: string, value: number | null, sub: string | null): string {\n const display = value === null ? \"—\" : String(value);\n const subLine = sub ? `<div class=\"tile-sub\">${escapeHtml(sub)}</div>` : \"\";\n return `<div class=\"tile\"><div class=\"tile-value\">${escapeHtml(display)}</div><div class=\"tile-label\">${escapeHtml(label)}</div>${subLine}</div>`;\n}\n\nfunction depsSub(majorBehind: number | null): string | null {\n if (majorBehind === null || majorBehind === 0) return null;\n return `${majorBehind} major behind`;\n}\n\nfunction securityTotal(site: WebsiteRow): number | null {\n const parts = [\n site.securityVulnsCritical,\n site.securityVulnsHigh,\n site.securityVulnsModerate,\n site.securityVulnsLow,\n ];\n if (parts.every((p) => p === null)) return null;\n return parts.reduce<number>((sum, p) => sum + (p ?? 0), 0);\n}\n\nfunction securitySub(site: WebsiteRow): string | null {\n const total = securityTotal(site);\n if (total === null || total === 0) return null;\n const c = site.securityVulnsCritical ?? 0;\n const h = site.securityVulnsHigh ?? 0;\n const m = site.securityVulnsModerate ?? 0;\n const l = site.securityVulnsLow ?? 0;\n return `${c}C / ${h}H / ${m}M / ${l}L`;\n}\n\nfunction pendingRow(r: ReportRow): string {\n const type = escapeHtml(r.reportType);\n const period = r.period ? escapeHtml(r.period) : \"—\";\n return `<li><strong>${type}</strong> <span class=\"muted\">${period}</span> <button class=\"approve\" data-report-id=\"${escapeHtml(r.id)}\" data-approve-url=\"/api/reports/${encodeURIComponent(r.id)}/approve\">Approve</button></li>`;\n}\n\nfunction pendingSection(reports: ReportRow[]): string {\n const pending = reports.filter(isPendingApproval);\n if (pending.length === 0) return \"\";\n return `<div class=\"section pending\">\n <h2>Pending your yes (${pending.length})</h2>\n <ul class=\"pending-list\">${pending.map(pendingRow).join(\"\")}</ul>\n </div>`;\n}\n\nfunction reportRow(r: ReportRow): string {\n const date = r.completedOn ? escapeHtml(r.completedOn) : \"—\";\n const type = escapeHtml(r.reportType);\n const id = escapeHtml(r.reportId);\n const link = r.renderedHtmlAttachment\n ? `<a href=\"${escapeHtml(safeUrl(r.renderedHtmlAttachment.url))}\">view</a>`\n : `<span class=\"muted\">no attachment</span>`;\n const action = isPendingApproval(r)\n ? `<button class=\"approve\" data-report-id=\"${escapeHtml(r.id)}\" data-approve-url=\"/api/reports/${encodeURIComponent(r.id)}/approve\">Approve</button>`\n : \"\";\n return `<tr><td>${date}</td><td>${type}</td><td><code>${id}</code></td><td>${link}</td><td>${action}</td></tr>`;\n}\n\nfunction submissionRow(s: SubmissionRow): string {\n const when = s.submittedAt ? escapeHtml(relativeTimeFromNow(s.submittedAt)) : \"—\";\n const type = escapeHtml(s.formType);\n const who = escapeHtml(s.name || \"(no name)\");\n const email = escapeHtml(s.email || \"\");\n const message = escapeHtml(s.message ?? \"\");\n const status = escapeHtml(s.status);\n const id = escapeHtml(s.id);\n const url = `/api/submissions/${encodeURIComponent(s.id)}/status`;\n const btn = (label: string, action: string) =>\n `<button class=\"subm-status\" data-id=\"${id}\" data-status=\"${action}\" data-url=\"${url}\">${label}</button>`;\n return `<li class=\"subm-item\">\n <div class=\"subm-head\"><strong>${type}</strong> · ${who} <span class=\"muted\">${email}</span> <span class=\"pill subm-${status}\">${status}</span> <span class=\"muted\">${when}</span></div>\n ${message ? `<div class=\"subm-msg\">${message}</div>` : \"\"}\n <div class=\"subm-actions\">${btn(\"Read\", \"read\")}${btn(\"Archive\", \"archived\")}${btn(\"Spam\", \"spam\")}</div>\n </li>`;\n}\n\nfunction submissionsSection(submissions: SubmissionRow[]): string {\n if (submissions.length === 0) return \"\";\n const recent = [...submissions]\n .sort((a, b) => (b.submittedAt ?? \"\").localeCompare(a.submittedAt ?? \"\"))\n .slice(0, 25);\n return `<div class=\"section submissions\">\n <h2>Form submissions (${submissions.length})</h2>\n <ul class=\"subm-list\">${recent.map(submissionRow).join(\"\")}</ul>\n </div>`;\n}\n\nconst STYLES = `\n:root { color-scheme: light dark; }\nbody { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }\n@media (prefers-color-scheme: dark) { body { color: #e8e8e8; background: #111; } a { color: #6cb6ff; } }\nh1 { margin: 0 0 0.25rem; font-size: 1.75rem; }\n.meta { color: #666; margin-bottom: 2rem; }\n.meta a { color: inherit; }\n.audited { color: #999; font-size: 0.85rem; margin-bottom: 1.5rem; }\n.section { margin: 2rem 0; }\n.section h2 { font-size: 1.1rem; margin: 0 0 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #666; }\n.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.75rem; }\n.tile { padding: 1rem; border: 1px solid #ddd; border-radius: 6px; text-align: center; }\n@media (prefers-color-scheme: dark) { .tile { border-color: #333; } }\n.tile-value { font-size: 2rem; font-weight: 600; }\n.tile-label { font-size: 0.85rem; color: #666; margin-top: 0.25rem; }\n.tile-sub { font-size: 0.75rem; color: #999; margin-top: 0.15rem; }\ntable { width: 100%; border-collapse: collapse; }\nth, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid #eee; }\n@media (prefers-color-scheme: dark) { th, td { border-color: #2a2a2a; } }\n.muted { color: #999; }\n.empty { color: #999; padding: 1rem; border: 1px dashed #ccc; border-radius: 6px; text-align: center; }\nbutton.approve { font: inherit; padding: 0.35rem 0.85rem; border: 1px solid #2c7; border-radius: 6px; background: #2c7; color: #fff; cursor: pointer; }\nbutton.approve:disabled { opacity: 0.6; cursor: default; }\n.pending-list { list-style: none; padding: 0; margin: 0; }\n.pending-list li { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-bottom: 1px solid #eee; }\n.pill { font-size: 0.75rem; padding: 0.1rem 0.5rem; border-radius: 999px; font-weight: 700; }\n.subm-list { list-style: none; padding: 0; margin: 0; }\n.subm-item { padding: 0.6rem 0; border-bottom: 1px solid #eee; }\n@media (prefers-color-scheme: dark) { .subm-item { border-color: #2a2a2a; } }\n.subm-head { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }\n.subm-msg { margin: 0.35rem 0; white-space: pre-wrap; }\n.subm-actions { display: flex; gap: 0.4rem; }\nbutton.subm-status { font: inherit; padding: 0.25rem 0.7rem; border: 1px solid #888; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; }\nbutton.subm-status:disabled { opacity: 0.6; cursor: default; }\n.pill.subm-new { background: #e8f0fe; color: #1a56db; }\n.pill.subm-read { background: #f0f0f0; color: #555; }\n.pill.subm-archived { background: #eee; color: #888; }\n.pill.subm-spam { background: #fdecea; color: #b00; }\n`;\n\n/**\n * Render the per-site dashboard as a single HTML document. Pure function:\n * no Airtable access, no env reads, no I/O. The Netlify function handler\n * fetches data, then hands it here. Easier to unit-test, easier to render\n * a static preview from CLI later.\n */\nexport function renderSiteDashboardHtml(\n site: WebsiteRow,\n reports: ReportRow[],\n submissions: SubmissionRow[] = [],\n): string {\n const name = escapeHtml(site.name);\n const urlSafe = safeUrl(site.url);\n const allScoresNull =\n site.pScore === null && site.rScore === null && site.bpScore === null && site.seoScore === null;\n\n const scoresSection = allScoresNull\n ? `<div class=\"empty\">No lighthouse data yet — run <code>reddoor-maint audit --write-airtable</code> from the site checkout.</div>`\n : `<div class=\"tiles\">\n ${scoreTile(\"Performance\", site.pScore)}\n ${scoreTile(\"Accessibility\", site.rScore)}\n ${scoreTile(\"Best Practices\", site.bpScore)}\n ${scoreTile(\"SEO\", site.seoScore)}\n </div>`;\n\n const secTotal = securityTotal(site);\n const allHealthNull =\n site.a11yViolations === null && site.depsDrifted === null && secTotal === null;\n const healthSection = allHealthNull\n ? `<div class=\"empty\">No health data yet — run <code>reddoor-maint audit --write-airtable</code> from the site checkout.</div>`\n : `<div class=\"tiles\">\n ${healthTile(\"Accessibility issues\", site.a11yViolations, null)}\n ${healthTile(\"Dependency updates\", site.depsDrifted, depsSub(site.depsMajorBehind))}\n ${healthTile(\"Security alerts\", secTotal, securitySub(site))}\n </div>`;\n\n const auditedLine = site.lastLighthouseAuditAt\n ? `<div class=\"audited\">Last audited ${escapeHtml(relativeTimeFromNow(site.lastLighthouseAuditAt))}</div>`\n : \"\";\n\n // The report-history TABLE is the only place the \"recent 6\" slice belongs:\n // long enough to show a quarter of monthly reports plus the latest testing\n // report, short enough to keep the page a single scroll. The pending list +\n // approve buttons above intentionally see the FULL `reports` set — an OLD\n // pending report that falls outside this slice must still be approvable\n // (and must not disagree with the fleet banner, which counts ALL reports).\n const recentReports = [...reports]\n .sort((a, b) => (b.completedOn ?? \"\").localeCompare(a.completedOn ?? \"\"))\n .slice(0, 6);\n const reportsSection =\n recentReports.length === 0\n ? `<div class=\"empty\">No reports yet.</div>`\n : `<table>\n <thead><tr><th>Completed</th><th>Type</th><th>ID</th><th>Report</th><th></th></tr></thead>\n <tbody>${recentReports.map(reportRow).join(\"\")}</tbody>\n </table>`;\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n ${FAVICON_LINK}\n <title>${name} — Reddoor maintenance</title>\n <style>${STYLES}</style>\n</head>\n<body>\n <h1>${name}</h1>\n <div class=\"meta\"><a href=\"${escapeHtml(urlSafe)}\">${escapeHtml(site.url)}</a></div>\n ${auditedLine}\n ${pendingSection(reports)}\n ${submissionsSection(submissions)}\n\n <div class=\"section\">\n <h2>Lighthouse</h2>\n ${scoresSection}\n </div>\n\n <div class=\"section\">\n <h2>Site Health</h2>\n ${healthSection}\n </div>\n\n <div class=\"section\">\n <h2>Reports</h2>\n ${reportsSection}\n </div>\n <script>\n document.querySelectorAll(\"button.approve\").forEach((b) => {\n b.addEventListener(\"click\", async () => {\n b.disabled = true;\n try {\n const res = await fetch(b.dataset.approveUrl, { method: \"POST\" });\n b.textContent = res.ok ? \"Approved\" : \"Failed\";\n if (!res.ok) b.disabled = false;\n } catch {\n // Network rejection (offline, DNS, abort): mirror the !res.ok path so\n // the button doesn't sit permanently disabled reading \"Approve\".\n b.textContent = \"Failed\";\n b.disabled = false;\n }\n });\n });\n document.querySelectorAll(\"button.subm-status\").forEach((b) => {\n b.addEventListener(\"click\", async () => {\n b.disabled = true;\n try {\n const res = await fetch(b.dataset.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ status: b.dataset.status }),\n });\n b.textContent = res.ok ? \"✓\" : \"Failed\";\n if (!res.ok) b.disabled = false;\n } catch {\n b.textContent = \"Failed\";\n b.disabled = false;\n }\n });\n });\n </script>\n</body>\n</html>`;\n}\n","import type { WebsiteRow } from \"../reports/airtable/websites.js\";\n\nexport type OnboardingStatus = {\n score: number;\n total: 4;\n checks: {\n firstAudit: boolean;\n recipients: boolean;\n schedule: boolean;\n poc: boolean;\n };\n};\n\nfunction isNonEmpty(s: string | null | undefined): boolean {\n return typeof s === \"string\" && s.trim().length > 0;\n}\n\n/** Four-point onboarding signal for the fleet card. A site is \"fully onboarded\"\n * when it has been audited at least once, has a To-recipient for monthly\n * reports, has a maintenance schedule that isn't \"None\", and has a named POC. */\nexport function onboardingStatus(row: WebsiteRow): OnboardingStatus {\n const checks = {\n firstAudit: isNonEmpty(row.lastLighthouseAuditAt),\n recipients: isNonEmpty(row.reportRecipientsTo),\n schedule: row.maintenanceFreq !== \"None\",\n poc: isNonEmpty(row.pointOfContact),\n };\n const score = Object.values(checks).filter(Boolean).length;\n return { score, total: 4, checks };\n}\n","import type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { siteSlug } from \"../reports/airtable/websites.js\";\nimport type { CockpitModel, SiteCard, Tier, SubmissionEntry } from \"./fleet-cockpit.js\";\nimport { onboardingStatus } from \"./onboarding.js\";\nimport { relativeTimeFromNow } from \"./relative-time.js\";\nimport { escapeHtml, safeUrl } from \"../util/html.js\";\nimport { FAVICON_LINK } from \"./favicon.js\";\n\nconst DASH = \"—\";\n\nfunction scoreSpan(category: \"perf\" | \"a11y-lh\" | \"bp\" | \"seo\", value: number | null): string {\n const display = value === null ? DASH : String(value);\n return `<span class=\"score ${category}\">${escapeHtml(display)}</span>`;\n}\n\nfunction a11ySpan(value: number | null): string {\n const display = value === null ? DASH : String(value);\n return `<span class=\"metric a11y\">${escapeHtml(display)}</span>`;\n}\n\nfunction depsSpan(\n drifted: number | null,\n majorBehind: number | null,\n outdated: number | null,\n): string {\n if (drifted === null || majorBehind === null) {\n return `<span class=\"metric deps\">${DASH}</span>`;\n }\n // Declared-range drift vs baseline, plus the real outdated-install count when\n // it was determined (null = not checked this run → omit, don't imply clean).\n const driftPart = drifted === 0 ? \"0\" : `${drifted} drifted (${majorBehind} major)`;\n const display = outdated === null ? driftPart : `${driftPart} · ${outdated} outdated`;\n return `<span class=\"metric deps\">${escapeHtml(display)}</span>`;\n}\n\nfunction securitySpan(\n critical: number | null,\n high: number | null,\n moderate: number | null,\n low: number | null,\n): string {\n if (critical === null || high === null || moderate === null || low === null) {\n return `<span class=\"metric sec\">${DASH}</span>`;\n }\n const total = critical + high + moderate + low;\n const display = total === 0 ? \"0\" : `${critical}C/${high}H/${moderate}M/${low}L`;\n return `<span class=\"metric sec\">${escapeHtml(display)}</span>`;\n}\n\nfunction card(site: WebsiteRow): string {\n const name = escapeHtml(site.name);\n // The per-site dashboard at /s/<slug> is operator-only, gated by the shared\n // dashboard password (no per-site token). Cockpit visibility is Status-based;\n // the caller filters the fleet view.\n const href = `/s/${escapeHtml(siteSlug(site.name))}`;\n const onboarding = onboardingStatus(site);\n const audited = relativeTimeFromNow(site.lastLighthouseAuditAt);\n const safeSiteUrl = escapeHtml(safeUrl(site.url));\n const visibleUrl = escapeHtml(site.url);\n\n return `<article class=\"card\">\n <header class=\"card-head\">\n <a class=\"site\" href=\"${href}\">${name}</a>\n <a class=\"url\" href=\"${safeSiteUrl}\" target=\"_blank\" rel=\"noopener\">${visibleUrl}</a>\n <span class=\"setup\">Setup: <strong>${onboarding.score}/${onboarding.total}</strong></span>\n <span class=\"audited\">Audited: <strong>${escapeHtml(audited)}</strong></span>\n </header>\n <div class=\"card-metrics\">\n <span class=\"cluster lighthouse\">\n ${scoreSpan(\"perf\", site.pScore)}\n ${scoreSpan(\"a11y-lh\", site.rScore)}\n ${scoreSpan(\"bp\", site.bpScore)}\n ${scoreSpan(\"seo\", site.seoScore)}\n </span>\n <span class=\"cluster health\">\n <span class=\"metric-label\">a11y</span> ${a11ySpan(site.a11yViolations)}\n <span class=\"metric-label\">deps</span> ${depsSpan(site.depsDrifted, site.depsMajorBehind, site.depsOutdated)}\n <span class=\"metric-label\">sec</span> ${securitySpan(\n site.securityVulnsCritical,\n site.securityVulnsHigh,\n site.securityVulnsModerate,\n site.securityVulnsLow,\n )}\n </span>\n </div>\n </article>`;\n}\n\nconst STYLES = `\n:root { color-scheme: light dark; }\nbody { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 1100px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }\n@media (prefers-color-scheme: dark) { body { color: #e8e8e8; background: #111; } a { color: #6cb6ff; } }\nh1 { margin: 0 0 0.25rem; font-size: 1.75rem; }\n.meta { color: #666; margin-bottom: 1.5rem; }\n.empty { color: #999; padding: 2rem; text-align: center; border: 1px dashed #ccc; border-radius: 6px; }\n.cards { display: flex; flex-direction: column; gap: 0.75rem; }\n.card { border: 1px solid #e5e5e5; border-radius: 8px; padding: 0.9rem 1.1rem; }\n@media (prefers-color-scheme: dark) { .card { border-color: #2a2a2a; background: #181818; } }\n.card-head { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; align-items: baseline; }\n.card-head .site { font-weight: 600; font-size: 1.05rem; }\n.card-head .url { color: #666; font-size: 0.85rem; }\n.card-head .setup, .card-head .audited { color: #666; font-size: 0.85rem; }\n.card-head .setup { margin-left: auto; }\n.card-metrics { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; margin-top: 0.5rem; font-variant-numeric: tabular-nums; }\n.cluster { display: inline-flex; gap: 0.5rem; align-items: baseline; }\n.cluster.lighthouse .score { display: inline-block; min-width: 2.25rem; text-align: right; }\n.metric-label { color: #999; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }\n.metric { font-feature-settings: \"tnum\"; }\n.summary { display:flex; flex-wrap:wrap; gap:0.5rem 1.25rem; align-items:baseline; margin-bottom:0.5rem; }\n.summary .tier { font-weight:700; }\n.summary .heads { color:#666; font-size:0.9rem; }\n.filters { display:flex; flex-wrap:wrap; gap:0.4rem; margin-bottom:1.25rem; }\n.filters button { font:inherit; font-size:0.85rem; padding:0.25rem 0.7rem; border:1px solid #ccc; border-radius:999px; background:transparent; color:inherit; cursor:pointer; }\n.filters button[aria-pressed=\"true\"] { background:#1a1a1a; color:#fff; border-color:#1a1a1a; }\n@media (prefers-color-scheme: dark) { .filters button[aria-pressed=\"true\"] { background:#e8e8e8; color:#111; } }\ndetails.tier { margin:0.75rem 0; }\ndetails.tier > summary { cursor:pointer; font-weight:700; font-size:1.05rem; padding:0.35rem 0; list-style:none; }\n.approve-strip { border:1px solid #ffe08a; background:#fff8e1; border-radius:8px; padding:0.75rem 1rem; margin-bottom:1.25rem; }\n@media (prefers-color-scheme: dark) { .approve-strip { background:#241f00; border-color:#5a4d00; } }\n.approve-strip h2 { font-size:1rem; margin:0 0 0.5rem; }\n.approve-row { display:flex; flex-wrap:wrap; gap:0.5rem 1rem; align-items:center; padding:0.25rem 0; }\n.pill { font-size:0.75rem; padding:0.1rem 0.5rem; border-radius:999px; font-weight:700; }\n.pill.attention { background:#fdecea; color:#b00; }\n.pill.watch { background:#fff4e5; color:#a65a00; }\n.pill.healthy { background:#e8f5e9; color:#1b7a2f; }\n.chips { display:flex; flex-wrap:wrap; gap:0.4rem; margin-top:0.5rem; }\n.chip { font-size:0.8rem; padding:0.1rem 0.5rem; border-radius:6px; background:#f0f0f0; }\n@media (prefers-color-scheme: dark) { .chip { background:#222; } }\n.chip.critical { background:#fdecea; color:#b00; }\n.badge { font-weight:700; color:#C00; font-size:0.72rem; margin-right:0.25rem; }\n.all-clear { background:#e8f5e9; color:#1b7a2f; padding:0.6rem 1rem; border-radius:8px; margin-bottom:1.25rem; font-weight:600; }\n@media (prefers-color-scheme: dark) { .all-clear { background:#10240f; color:#7fce85; } }\n`;\n\nconst TIER_META: Record<Tier, { emoji: string; label: string; open: boolean }> = {\n attention: { emoji: \"🔴\", label: \"Needs attention\", open: true },\n watch: { emoji: \"🟡\", label: \"Watch\", open: false },\n healthy: { emoji: \"🟢\", label: \"Healthy\", open: false },\n};\n\nconst FILTERS = [\n \"all\",\n \"vulns\",\n \"lighthouse\",\n \"delivery\",\n \"prs\",\n \"ci\",\n \"stale\",\n \"pending\",\n \"submissions\",\n] as const;\n\nfunction summaryBar(model: CockpitModel): string {\n const s = model.summary;\n const heads = [\n `${s.criticalHighVulns} critical/high vuln${s.criticalHighVulns === 1 ? \"\" : \"s\"}`,\n `${s.lighthouseBelowFloor} Lighthouse<75`,\n `${s.deliveryFailures} delivery`,\n `${s.renovateFailing} PRs failing`,\n `${s.ciRed} CI red`,\n `${s.pending} pending`,\n `${s.newSubmissions ?? 0} new`,\n ].join(\" · \");\n const chips = FILTERS.map(\n (f) =>\n `<button type=\"button\" data-filter=\"${f}\" aria-pressed=\"${f === \"all\" ? \"true\" : \"false\"}\">${f}</button>`,\n ).join(\"\");\n return `<div class=\"summary\">\n <span class=\"tier\">🔴 ${s.attention} needs attention</span>\n <span class=\"tier\">🟡 ${s.watch} watch</span>\n <span class=\"tier\">🟢 ${s.healthy} healthy</span>\n </div>\n <div class=\"summary heads\">${escapeHtml(heads)}</div>\n <div class=\"filters\">${chips}</div>`;\n}\n\n/** Affirmative all-clear when nothing is on the 🔴 tier (spec §5.2/§12) — so a\n * healthy or empty fleet reads as \"all clear\", not three bare \"None.\" rows. */\nfunction allClearBanner(model: CockpitModel): string {\n if (model.summary.attention > 0) return \"\";\n const msg =\n model.cards.length === 0\n ? \"No sites on the fleet view yet.\"\n : \"All clear — nothing needs your attention.\";\n return `<div class=\"all-clear\">✓ ${escapeHtml(msg)}</div>`;\n}\n\nfunction approveStrip(model: CockpitModel): string {\n if (model.pending.length === 0) return \"\";\n const rows = model.pending\n .map((p) => {\n const href = `/s/${escapeHtml(p.slug)}`;\n const url = `/api/reports/${encodeURIComponent(p.reportId)}/approve`;\n return `<div class=\"approve-row\" data-signal=\"pending\">\n <strong>${escapeHtml(p.siteName)}</strong>\n <span class=\"muted\">${escapeHtml(p.reportType)} ${escapeHtml(p.period)}</span>\n <button class=\"approve\" data-report-id=\"${escapeHtml(p.reportId)}\" data-approve-url=\"${url}\">Approve</button>\n <a href=\"${href}\">open ▸</a>\n </div>`;\n })\n .join(\"\");\n return `<section class=\"approve-strip\" data-tier=\"pending\">\n <h2>Approve (${model.pending.length}) — your daily yes</h2>\n ${rows}\n </section>`;\n}\n\nfunction submissionsStrip(model: CockpitModel): string {\n const subs: SubmissionEntry[] = model.submissions ?? [];\n if (subs.length === 0) return \"\";\n const rows = subs\n .map((sub) => {\n const href = `/s/${escapeHtml(sub.slug)}`;\n const when = sub.submittedAt ? escapeHtml(relativeTimeFromNow(sub.submittedAt)) : \"\";\n const who = escapeHtml(sub.name || sub.email);\n return `<div class=\"approve-row\" data-signal=\"submissions\">\n <strong>${escapeHtml(sub.siteName)}</strong>\n <span class=\"muted\">${escapeHtml(sub.formType)} — ${who}</span>\n <span class=\"muted\">${when}</span>\n <a href=\"${href}\">open ▸</a>\n </div>`;\n })\n .join(\"\");\n return `<section class=\"approve-strip subm-strip\" data-tier=\"submissions\">\n <h2>📥 New submissions (${subs.length})</h2>\n ${rows}\n </section>`;\n}\n\nfunction submBadge(c: SiteCard): string {\n const n = c.newSubmissions ?? 0;\n return n > 0 ? `<span class=\"chip\">📥 ${n} new</span>` : \"\";\n}\n\nconst PILL_LABEL: Record<Tier, string> = { attention: \"failing\", watch: \"watch\", healthy: \"ok\" };\n\nfunction attentionBadge(status?: string): string {\n if (status === \"new\") return `<span class=\"badge\">NEW</span>`;\n if (status === \"worse\") return `<span class=\"badge\">WORSE</span>`;\n return \"\";\n}\n\nfunction chips(c: SiteCard): string {\n const items = c.items.map((it) => {\n const cls = it.severity === \"critical\" ? \"chip critical\" : \"chip\";\n return `<span class=\"${cls}\">${attentionBadge(it.status)}${escapeHtml(it.title)}</span>`;\n });\n for (const reason of c.watchReasons)\n items.push(`<span class=\"chip\">${escapeHtml(reason)}</span>`);\n return items.length ? `<div class=\"chips\">${items.join(\"\")}</div>` : \"\";\n}\n\n/** Space-separated signal tags for the client filter. Attention-item kinds\n * (\"vulns\"/\"lighthouse\"/\"delivery\"/\"prs\" from renovate/\"ci\") plus the structured\n * watch signals (\"lighthouse\" for a sub-floor-band score, \"stale\" for an old\n * commit) — so a watch-band Lighthouse card still matches the \"lighthouse\" filter. */\nfunction signalsAttr(c: SiteCard): string {\n const kinds = new Set<string>();\n for (const it of c.items) {\n kinds.add(it.kind === \"vuln\" ? \"vulns\" : it.kind === \"renovate\" ? \"prs\" : it.kind);\n }\n for (const sig of c.watchSignals) kinds.add(sig);\n return [...kinds].join(\" \");\n}\n\nfunction cockpitCard(c: SiteCard): string {\n const base = card(c.site); // existing header + metrics markup\n const pill = `<span class=\"pill ${c.tier}\">${PILL_LABEL[c.tier]}</span>`;\n const extra = `${pill}${chips(c)}${submBadge(c)}`;\n const opening = `<article class=\"card\" data-signals=\"${signalsAttr(c)}\">`;\n // Inject the pill + chips before the article's closing tag, and add the filter\n // hook. Function replacers so a `$` in escaped chip text can't be read as a\n // String.replace special ($&, $1, …).\n return base\n .replace('<article class=\"card\">', () => opening)\n .replace(\"</article>\", () => `${extra}</article>`);\n}\n\nconst FILTER_SCRIPT = `<script>\n(function(){\n var btns = document.querySelectorAll('.filters button');\n var cards = document.querySelectorAll('.cards .card');\n var details = document.querySelectorAll('details.tier');\n btns.forEach(function(b){\n b.addEventListener('click', function(){\n var f = b.getAttribute('data-filter');\n btns.forEach(function(x){ x.setAttribute('aria-pressed', x===b ? 'true':'false'); });\n // \"pending\" lives on the approve strip, not on tier cards — just jump to it,\n // never hide the triage cards (else the whole board blanks).\n if (f === 'pending') { var s = document.querySelector('.approve-strip'); if (s) s.scrollIntoView({behavior:'smooth'}); return; }\n if (f === 'submissions') { var ss = document.querySelector('[data-tier=\"submissions\"]'); if (ss) ss.scrollIntoView({behavior:'smooth'}); return; }\n if (f !== 'all') details.forEach(function(d){ d.open = true; });\n cards.forEach(function(c){\n var sig = (c.getAttribute('data-signals')||'').split(' ');\n c.style.display = (f==='all' || sig.indexOf(f)!==-1) ? '' : 'none';\n });\n });\n });\n // approve buttons: mirror the per-site dashboard's inline POST.\n document.querySelectorAll('button.approve').forEach(function(b){\n b.addEventListener('click', async function(){\n b.disabled = true; b.textContent = 'Approving…';\n try { var res = await fetch(b.dataset.approveUrl, { method: 'POST' });\n b.textContent = res.ok ? 'Approved ✓' : 'Failed'; }\n catch(e){ b.textContent = 'Failed'; b.disabled = false; }\n });\n });\n})();\n</script>`;\n\n/**\n * Render the fleet cockpit as a single HTML document. Pure function: no Airtable\n * access, no env reads, no I/O. The Netlify function handler builds the\n * CockpitModel (visible-site filter, tiering, NEW/WORSE badging, pending list)\n * and hands it here. Renders the doc shell + summary bar + filter chips + pinned\n * approve strip + three <details> tier sections of cards.\n */\nexport function renderCockpitHtml(model: CockpitModel): string {\n const total = model.cards.length;\n const tiers: Tier[] = [\"attention\", \"watch\", \"healthy\"];\n const sections = tiers\n .map((tier) => {\n const cards = model.cards.filter((c) => c.tier === tier);\n const meta = TIER_META[tier];\n const body =\n cards.length === 0\n ? `<div class=\"empty\">None.</div>`\n : `<div class=\"cards\">${cards.map(cockpitCard).join(\"\")}</div>`;\n return `<details class=\"tier\" data-tier=\"${tier}\"${meta.open ? \" open\" : \"\"}>\n <summary>${meta.emoji} ${meta.label} (${cards.length})</summary>\n ${body}\n </details>`;\n })\n .join(\"\");\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n ${FAVICON_LINK}\n <title>Reddoor maintenance — fleet cockpit</title>\n <style>${STYLES}</style>\n</head>\n<body>\n <h1>Reddoor fleet cockpit</h1>\n <div class=\"meta\">${total} site${total === 1 ? \"\" : \"s\"} on the Reddoor stack.</div>\n ${summaryBar(model)}\n ${allClearBanner(model)}\n ${approveStrip(model)}\n ${submissionsStrip(model)}\n ${sections}\n ${FILTER_SCRIPT}\n</body>\n</html>`;\n}\n","import { timingSafeEqual } from \"node:crypto\";\n\n/**\n * Verify an `Authorization: Basic <base64>` header against the configured\n * dashboard password. Username is intentionally ignored — operators may\n * type anything when the browser prompts; only the password gates entry.\n *\n * Returns false for any of:\n * - missing/empty Authorization header\n * - non-Basic auth scheme\n * - malformed base64 or payload (no colon to split user:password)\n * - wrong password\n * - expected password missing (DASHBOARD_PASSWORD not configured)\n *\n * Wrong-password compare is constant-time; BYTE lengths are checked first\n * (timingSafeEqual throws a RangeError on a buffer-length mismatch, and the\n * length itself doesn't leak — operator's password length is fixed per deploy).\n * Comparing JS-string lengths instead of byte lengths could let an equal-char\n * but unequal-byte password (a multibyte char) reach timingSafeEqual and throw,\n * turning a wrong password into an uncaught 500.\n */\nexport function verifyBasicAuth(\n authHeader: string | null | undefined,\n expectedPassword: string | null,\n): boolean {\n if (!authHeader || !expectedPassword) return false;\n // RFC 7235: scheme is case-insensitive.\n const match = /^basic\\s+(.+)$/i.exec(authHeader.trim());\n if (!match) return false;\n let decoded: string;\n try {\n decoded = Buffer.from(match[1]!, \"base64\").toString(\"utf-8\");\n } catch {\n return false;\n }\n // Base64-decoding never throws in Node, but a payload of garbage may\n // produce a string with no colon. user:password form is required.\n const colonIdx = decoded.indexOf(\":\");\n if (colonIdx === -1) return false;\n const provided = decoded.slice(colonIdx + 1);\n // Compare BYTE lengths, not JS-string lengths: timingSafeEqual compares the\n // underlying buffers and throws a RangeError if they differ in byte length.\n // Two strings can share a JS length but differ in UTF-8 byte length (e.g. a\n // multibyte char), so a JS-length guard would let mismatched buffers through\n // and crash the handler with a 500.\n const a = Buffer.from(provided, \"utf-8\");\n const b = Buffer.from(expectedPassword, \"utf-8\");\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n"],"mappings":"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","names":["join","spawn","join","readFile","join","join","eslint","readFile","spawn","readFile","join","readFile","join","readFile","join","spawn","readFile","writeFile","mkdtemp","rm","join","readJsonMaybe","readFile","spawn","mkdtemp","join","writeFile","rm","spawn","readFile","writeFile","join","readFile","join","writeFile","commit","stat","join","exists","stat","spawn","join","commit","join","readFile","writeFile","join","join","readFile","writeFile","join","spawn","join","spawn","join","readFile","writeFile","join","glob","SCRIPT_BLOCK","SCRIPT_BLOCK","SCRIPT_BLOCK","SCRIPT_BLOCK","IGNORE","glob","join","readFile","writeFile","spawn","writeFile","join","join","spawn","commit","writeFile","join","commit","writeFile","join","rm","stat","join","exists","stat","spawn","join","commit","rm","stat","join","existsSync","dirname","join","exists","stat","spawn","join","commit","mkdir","writeFile","dirname","join","join","commit","mkdir","dirname","writeFile","readFile","readFile","mkdir","writeFile","dirname","readFile","existsSync","dirname","join","fileURLToPath","mapRow","mapRow","mapRow","dirname","join","readFileSync","join","join","dirname","readFileSync","ymd","readFileSync","JWT","ymd","mkdir","dirname","writeFile","MONTHS","STYLES","chips"]}
|