@reddoorla/maintenance 0.88.3 → 0.89.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.
Files changed (108) hide show
  1. package/dist/{announce-GKD5YJNB.js → announce-OJFAS5JT.js} +6 -6
  2. package/dist/{chunk-7UAYMAKR.js → chunk-3VQWSE3F.js} +4 -4
  3. package/dist/{chunk-XKDCR4UE.js → chunk-5PWB3JHJ.js} +2 -3
  4. package/dist/chunk-5PWB3JHJ.js.map +1 -0
  5. package/dist/{chunk-XSOINOPY.js → chunk-62NYED6E.js} +3 -3
  6. package/dist/{chunk-B23FFWU4.js → chunk-AEXJDCJN.js} +96 -19
  7. package/dist/{chunk-B23FFWU4.js.map → chunk-AEXJDCJN.js.map} +1 -1
  8. package/dist/{chunk-7FFUBF4I.js → chunk-AX22DH6S.js} +2 -2
  9. package/dist/{chunk-CEW5PEHZ.js → chunk-DXKF552B.js} +260 -5
  10. package/dist/chunk-DXKF552B.js.map +1 -0
  11. package/dist/{chunk-6I6L3CMX.js → chunk-KGUKL3CV.js} +2 -2
  12. package/dist/{chunk-ULHPALI5.js → chunk-NDCOOI3V.js} +2 -2
  13. package/dist/{chunk-GQDDWYU2.js → chunk-QBAAZY7W.js} +1 -1
  14. package/dist/{chunk-GQDDWYU2.js.map → chunk-QBAAZY7W.js.map} +1 -1
  15. package/dist/{chunk-6UCVWYDA.js → chunk-QLIUDTY6.js} +4 -4
  16. package/dist/{chunk-XP5WM2LV.js → chunk-SKI23ZZH.js} +4 -4
  17. package/dist/chunk-SVAAONYB.js +21 -0
  18. package/dist/chunk-SVAAONYB.js.map +1 -0
  19. package/dist/{chunk-DGZWFI2G.js → chunk-VBEKL445.js} +17 -8
  20. package/dist/chunk-VBEKL445.js.map +1 -0
  21. package/dist/{chunk-WPL5GERE.js → chunk-ZOCJBJQV.js} +2 -2
  22. package/dist/{chunk-J3WEZGIN.js → chunk-ZRI5VRPG.js} +4 -4
  23. package/dist/cli/bin.js +22 -16
  24. package/dist/cli/bin.js.map +1 -1
  25. package/dist/cli/commands/audit.js +5 -5
  26. package/dist/{db-DXUUAMXT.js → db-DAJX7NNW.js} +50 -9
  27. package/dist/db-DAJX7NNW.js.map +1 -0
  28. package/dist/{digest-NBLECFM7.js → digest-T6TAZNLF.js} +11 -11
  29. package/dist/{digest-collectors-NM7WUWQP.js → digest-collectors-NI2M3UTC.js} +6 -6
  30. package/dist/{dump-EPL7AZFR.js → dump-QK7LNOGC.js} +14 -1
  31. package/dist/{dump-EPL7AZFR.js.map → dump-QK7LNOGC.js.map} +1 -1
  32. package/dist/{email-FMMEUPX6.js → email-LJQT5J7F.js} +6 -7
  33. package/dist/{email-FMMEUPX6.js.map → email-LJQT5J7F.js.map} +1 -1
  34. package/dist/{ensure-site-FU6527OU.js → ensure-site-NSNY2QHN.js} +2 -2
  35. package/dist/{fleet-state-R5LU7IC3.js → fleet-state-3LQUQHYD.js} +2 -2
  36. package/dist/{forms-notify-target-SGDGXXK5.js → forms-notify-target-LYSKME5L.js} +2 -2
  37. package/dist/freeze-A6MUCZU4.js +9 -0
  38. package/dist/{github-signals-DB25UWY4.js → github-signals-4YML6NY5.js} +4 -4
  39. package/dist/{health-mirror-GCDN7BJL.js → health-mirror-7LKC7Q5Z.js} +5 -5
  40. package/dist/index.d.ts +16 -4
  41. package/dist/index.js +22 -22
  42. package/dist/{init-32AWPS7A.js → init-NHKADHB6.js} +6 -6
  43. package/dist/{launch-52WUE4TZ.js → launch-VQJ4Z35C.js} +6 -6
  44. package/dist/{orchestrate-JNKCWU5K.js → orchestrate-6HROYTM6.js} +4 -4
  45. package/dist/{parity-NBYZUBFU.js → parity-623WNFZG.js} +3 -3
  46. package/dist/{pipeline-YIBIQ7XK.js → pipeline-MQJD4CKN.js} +6 -2
  47. package/dist/{preflight-IMTYDDD4.js → preflight-LIN5NCYU.js} +5 -5
  48. package/dist/{prismic-ci-MY2U6NN7.js → prismic-ci-PZ5LCPWT.js} +4 -4
  49. package/dist/{prismic-models-IJWZW2EO.js → prismic-models-6OYYZUR5.js} +3 -3
  50. package/dist/prospect/types.d.ts +8 -0
  51. package/dist/{prospect-audit-WJQI7V5V.js → prospect-audit-DBBXL7EG.js} +8 -6
  52. package/dist/prospect-audit-DBBXL7EG.js.map +1 -0
  53. package/dist/recipes/sync-configs.js +2 -2
  54. package/dist/{render-CVKE7IWC.js → render-P3SBUFVB.js} +3 -3
  55. package/dist/{render-from-row-DOYJDO2L.js → render-from-row-P5CWM3MQ.js} +3 -3
  56. package/dist/{report-2LM2SX67.js → report-2N4PXO3L.js} +30 -14
  57. package/dist/report-2N4PXO3L.js.map +1 -0
  58. package/dist/{report-mirror-AAK3MXHY.js → report-mirror-3ENU6OOK.js} +5 -5
  59. package/dist/{selftest-DIZ5JD2X.js → selftest-CUX2FIQD.js} +6 -6
  60. package/dist/{site-mirror-Q763PLGG.js → site-mirror-RQTEAID5.js} +5 -5
  61. package/dist/{sync-GBFV45WX.js → sync-ESP5ZQUU.js} +3 -3
  62. package/dist/{sync-configs-34JIA6KZ.js → sync-configs-LVBITPLU.js} +3 -3
  63. package/dist/usage-UHKU76VK.js +147 -0
  64. package/dist/usage-UHKU76VK.js.map +1 -0
  65. package/dist/{write-audits-to-airtable-OPBJXGS4.js → write-audits-to-airtable-K352LZQY.js} +3 -3
  66. package/dist/write-audits-to-airtable-K352LZQY.js.map +1 -0
  67. package/package.json +1 -1
  68. package/dist/chunk-CEW5PEHZ.js.map +0 -1
  69. package/dist/chunk-DGZWFI2G.js.map +0 -1
  70. package/dist/chunk-RUPPWAD6.js +0 -7
  71. package/dist/chunk-RUPPWAD6.js.map +0 -1
  72. package/dist/chunk-XKDCR4UE.js.map +0 -1
  73. package/dist/db-DXUUAMXT.js.map +0 -1
  74. package/dist/prospect-audit-WJQI7V5V.js.map +0 -1
  75. package/dist/report-2LM2SX67.js.map +0 -1
  76. /package/dist/{announce-GKD5YJNB.js.map → announce-OJFAS5JT.js.map} +0 -0
  77. /package/dist/{chunk-7UAYMAKR.js.map → chunk-3VQWSE3F.js.map} +0 -0
  78. /package/dist/{chunk-XSOINOPY.js.map → chunk-62NYED6E.js.map} +0 -0
  79. /package/dist/{chunk-7FFUBF4I.js.map → chunk-AX22DH6S.js.map} +0 -0
  80. /package/dist/{chunk-6I6L3CMX.js.map → chunk-KGUKL3CV.js.map} +0 -0
  81. /package/dist/{chunk-ULHPALI5.js.map → chunk-NDCOOI3V.js.map} +0 -0
  82. /package/dist/{chunk-6UCVWYDA.js.map → chunk-QLIUDTY6.js.map} +0 -0
  83. /package/dist/{chunk-XP5WM2LV.js.map → chunk-SKI23ZZH.js.map} +0 -0
  84. /package/dist/{chunk-WPL5GERE.js.map → chunk-ZOCJBJQV.js.map} +0 -0
  85. /package/dist/{chunk-J3WEZGIN.js.map → chunk-ZRI5VRPG.js.map} +0 -0
  86. /package/dist/{digest-NBLECFM7.js.map → digest-T6TAZNLF.js.map} +0 -0
  87. /package/dist/{digest-collectors-NM7WUWQP.js.map → digest-collectors-NI2M3UTC.js.map} +0 -0
  88. /package/dist/{ensure-site-FU6527OU.js.map → ensure-site-NSNY2QHN.js.map} +0 -0
  89. /package/dist/{fleet-state-R5LU7IC3.js.map → fleet-state-3LQUQHYD.js.map} +0 -0
  90. /package/dist/{forms-notify-target-SGDGXXK5.js.map → forms-notify-target-LYSKME5L.js.map} +0 -0
  91. /package/dist/{orchestrate-JNKCWU5K.js.map → freeze-A6MUCZU4.js.map} +0 -0
  92. /package/dist/{github-signals-DB25UWY4.js.map → github-signals-4YML6NY5.js.map} +0 -0
  93. /package/dist/{health-mirror-GCDN7BJL.js.map → health-mirror-7LKC7Q5Z.js.map} +0 -0
  94. /package/dist/{init-32AWPS7A.js.map → init-NHKADHB6.js.map} +0 -0
  95. /package/dist/{launch-52WUE4TZ.js.map → launch-VQJ4Z35C.js.map} +0 -0
  96. /package/dist/{parity-NBYZUBFU.js.map → orchestrate-6HROYTM6.js.map} +0 -0
  97. /package/dist/{pipeline-YIBIQ7XK.js.map → parity-623WNFZG.js.map} +0 -0
  98. /package/dist/{render-CVKE7IWC.js.map → pipeline-MQJD4CKN.js.map} +0 -0
  99. /package/dist/{preflight-IMTYDDD4.js.map → preflight-LIN5NCYU.js.map} +0 -0
  100. /package/dist/{prismic-ci-MY2U6NN7.js.map → prismic-ci-PZ5LCPWT.js.map} +0 -0
  101. /package/dist/{prismic-models-IJWZW2EO.js.map → prismic-models-6OYYZUR5.js.map} +0 -0
  102. /package/dist/{render-from-row-DOYJDO2L.js.map → render-P3SBUFVB.js.map} +0 -0
  103. /package/dist/{write-audits-to-airtable-OPBJXGS4.js.map → render-from-row-P5CWM3MQ.js.map} +0 -0
  104. /package/dist/{report-mirror-AAK3MXHY.js.map → report-mirror-3ENU6OOK.js.map} +0 -0
  105. /package/dist/{selftest-DIZ5JD2X.js.map → selftest-CUX2FIQD.js.map} +0 -0
  106. /package/dist/{site-mirror-Q763PLGG.js.map → site-mirror-RQTEAID5.js.map} +0 -0
  107. /package/dist/{sync-GBFV45WX.js.map → sync-ESP5ZQUU.js.map} +0 -0
  108. /package/dist/{sync-configs-34JIA6KZ.js.map → sync-configs-LVBITPLU.js.map} +0 -0
@@ -169,7 +169,7 @@ var defaultDeps = () => ({
169
169
  const { listWebsites, updatePrismicModels } = await import("./websites-D6WYFAES.js");
170
170
  const base = openBase(readAirtableConfig());
171
171
  const websites = await listWebsites(base);
172
- const { makeSiteMirror } = await import("./site-mirror-Q763PLGG.js");
172
+ const { makeSiteMirror } = await import("./site-mirror-RQTEAID5.js");
173
173
  const mirror = await makeSiteMirror();
174
174
  return {
175
175
  websites: websites.map((w) => ({ id: w.id, name: w.name })),
@@ -769,7 +769,7 @@ async function persistSweep(rows, deps) {
769
769
  let sink;
770
770
  let formatSummary;
771
771
  try {
772
- formatSummary = (await import("./write-audits-to-airtable-OPBJXGS4.js")).formatFleetWriteSummary;
772
+ formatSummary = (await import("./write-audits-to-airtable-K352LZQY.js")).formatFleetWriteSummary;
773
773
  sink = await deps.openVerdictSink();
774
774
  } catch (e) {
775
775
  return {
@@ -1022,4 +1022,4 @@ export {
1022
1022
  sweepRowWriteback,
1023
1023
  writeSweepToAirtable
1024
1024
  };
1025
- //# sourceMappingURL=prismic-models-IJWZW2EO.js.map
1025
+ //# sourceMappingURL=prismic-models-6OYYZUR5.js.map
@@ -281,6 +281,14 @@ type ProspectAuditResult = {
281
281
  * database's `business` COLUMN (kept as-is; a rename there would need its
282
282
  * own migration for no benefit) — map at that one boundary, not here. */
283
283
  businessName: string | null;
284
+ /** Which credential — which INSTRUMENT — produced the model stages: "api"
285
+ * is the metered Anthropic API (production), "subscription" is the
286
+ * claude -p path (dev, see claude-code.ts). A Claude Code harness is not
287
+ * the same instrument as a bare API call, so runs must be distinguishable
288
+ * after the fact even when probes were skipped and no engine name was
289
+ * recorded. Absent on rows persisted before 2026-08-31 — unknown, not
290
+ * "api". */
291
+ llmAuth: "api" | "subscription";
284
292
  generatedAt: string;
285
293
  scores: Scores;
286
294
  /** The crawl is the one fatal stage (see pipeline.ts): a crawl failure
@@ -55,8 +55,10 @@ function summarize(result, link, file, warnings, recovery, email) {
55
55
  "",
56
56
  scoreLine("Findability", result.scores.findability),
57
57
  scoreLine("Readability", result.scores.readability),
58
- scoreLine("Answers", result.scores.answers),
59
- scoreLine("AI Visibility", result.scores.aiVisibility)
58
+ scoreLine("Answers", result.scores.answers)
59
+ // No AI Visibility line: the score was removed from every presentation
60
+ // surface (web, print, email, here) — the measurement lives on as the
61
+ // report's receipts section, not as a number that reads as ours to move.
60
62
  ];
61
63
  for (const [name, stage] of [
62
64
  ["checks", result.checks],
@@ -95,7 +97,7 @@ async function runProspectAuditCommand(url, opts) {
95
97
  "No TURSO_DATABASE_URL, so the report cannot be saved or shared. Re-run with --out <file>, or set the Turso credentials."
96
98
  );
97
99
  }
98
- const { runProspectAudit } = await import("./pipeline-YIBIQ7XK.js");
100
+ const { runProspectAudit } = await import("./pipeline-MQJD4CKN.js");
99
101
  const onStage = (name, status, detail) => {
100
102
  if (status === "start") console.error(`\u2026 ${name}`);
101
103
  else if (status === "ok") console.error(`\u2713 ${name}`);
@@ -113,7 +115,7 @@ async function runProspectAuditCommand(url, opts) {
113
115
  },
114
116
  { ...opts.deps ?? {}, onStage }
115
117
  );
116
- const { renderProspectReport } = await import("./render-CVKE7IWC.js");
118
+ const { renderProspectReport } = await import("./render-P3SBUFVB.js");
117
119
  const html = renderProspectReport(result);
118
120
  const warnings = [];
119
121
  let file = null;
@@ -165,7 +167,7 @@ async function runProspectAuditCommand(url, opts) {
165
167
  let email = null;
166
168
  if (opts.email) {
167
169
  try {
168
- const { sendAuditEmail, parseProspectAuditRecipients } = await import("./email-FMMEUPX6.js");
170
+ const { sendAuditEmail, parseProspectAuditRecipients } = await import("./email-LJQT5J7F.js");
169
171
  const recipients = parseProspectAuditRecipients(process.env.PROSPECT_AUDIT_RECIPIENTS);
170
172
  email = await sendAuditEmail(result, { link, recipients, auditId, pdf });
171
173
  if (!email.sent) {
@@ -190,4 +192,4 @@ async function runProspectAuditCommand(url, opts) {
190
192
  export {
191
193
  runProspectAuditCommand
192
194
  };
193
- //# sourceMappingURL=prospect-audit-WJQI7V5V.js.map
195
+ //# sourceMappingURL=prospect-audit-DBBXL7EG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/commands/prospect-audit.ts","../src/prospect/report-url.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { hostnameOf, isHttpUrl, isPrivateOrLoopbackHost } from \"../../util/url.js\";\nimport { reportUrl, reportPrintUrl } from \"../../prospect/report-url.js\";\nimport type { ProspectAuditStatus } from \"../../db/prospect-audits.js\";\nimport type { PipelineDeps, StageName } from \"../../prospect/pipeline.js\";\nimport type { ProspectAuditResult } from \"../../prospect/types.js\";\nimport type { SendAuditEmailResult } from \"../../prospect/email.js\";\n\nexport type ProspectAuditCliOptions = {\n business?: string;\n /** Comma-separated competitor domains. */\n competitors?: string;\n /** cac sets this false for `--no-probes`. */\n probes?: boolean;\n out?: string;\n json?: boolean;\n /**\n * Email the internal sheet (scores, what wasn't measured and why, top\n * fixes, and the shareable link) to `PROSPECT_AUDIT_RECIPIENTS`, once the\n * report is rendered and the `--out`/Turso delivery attempts finish. A\n * missing/empty recipients var, or a Resend failure, is a warning — never a\n * crash — because by the time this runs the audit is already delivered (or\n * recovered) by the code above it.\n */\n email?: boolean;\n /** Test seam: injected pipeline deps. Never set from the CLI. */\n deps?: PipelineDeps;\n};\n\ntype RecoveryWrite = { htmlPath: string; jsonPath: string };\n\nfunction fail(message: string): { output: string; code: number } {\n return { output: message, code: 2 };\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nfunction scoreLine(label: string, value: number | null): string {\n return `${label.padEnd(14)} ${value === null ? \"not measured\" : String(value).padStart(3)}`;\n}\n\n/** Item 3: the `status` column exists to answer, without deserializing the\n * whole result_json, whether this was a clean run or a degraded one. crawl\n * is excluded — it's the one fatal stage (see pipeline.ts), so by the time a\n * ProspectAuditResult exists it was always ok. Every other stage's\n * StageResult already collapses \"failed\" and \"deliberately skipped\"\n * (--no-probes, the checks→analyze cascade) into the same `ok: false` — both\n * mean the report has a \"not measured\" section, so both count as partial\n * here too. */\nfunction auditStatus(result: ProspectAuditResult): ProspectAuditStatus {\n const allStagesOk =\n result.checks.ok && result.lighthouse.ok && result.analyze.ok && result.probes.ok;\n return allStagesOk ? \"complete\" : \"partial\";\n}\n\n/** Filesystem-safe, never empty. Collapses anything outside [a-z0-9.-] to a\n * single dash so a hostname becomes a sane filename fragment. */\nfunction slugify(s: string): string {\n const slug = s\n .toLowerCase()\n .replace(/[^a-z0-9.-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return slug || \"prospect\";\n}\n\n/**\n * Last-resort write when NEITHER `--out` nor the database persist made it —\n * the one situation a paid, already-run audit must never end in silently. Lands\n * in `reports/`, mirroring header-image.ts's own convention for CLI-written\n * artifacts (mkdir -p, resolved against cwd), rather than a bare file dropped\n * into whatever directory the operator happened to be standing in when they\n * ran the command — often unrelated to this repo at all in fleet-tool usage.\n */\nasync function writeRecoveryFiles(\n result: ProspectAuditResult,\n html: string,\n): Promise<RecoveryWrite> {\n const dir = resolve(\"reports\");\n await mkdir(dir, { recursive: true });\n const stamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const base = `prospect-audit-${slugify(hostnameOf(result.url))}-${stamp}`;\n const htmlPath = resolve(dir, `${base}.html`);\n const jsonPath = resolve(dir, `${base}.json`);\n await writeFile(htmlPath, html, \"utf-8\");\n await writeFile(jsonPath, JSON.stringify(result, null, 2), \"utf-8\");\n return { htmlPath, jsonPath };\n}\n\nfunction summarize(\n result: ProspectAuditResult,\n link: string | null,\n file: string | null,\n warnings: string[],\n recovery: RecoveryWrite | null,\n email: SendAuditEmailResult | null,\n): string {\n const lines = [\n `Prospect audit — ${result.businessName ?? result.url}`,\n \"\",\n scoreLine(\"Findability\", result.scores.findability),\n scoreLine(\"Readability\", result.scores.readability),\n scoreLine(\"Answers\", result.scores.answers),\n // No AI Visibility line: the score was removed from every presentation\n // surface (web, print, email, here) — the measurement lives on as the\n // report's receipts section, not as a number that reads as ours to move.\n ];\n for (const [name, stage] of [\n [\"checks\", result.checks],\n [\"lighthouse\", result.lighthouse],\n [\"analyze\", result.analyze],\n [\"probes\", result.probes],\n ] as const) {\n if (!stage.ok) lines.push(` ! ${name} not measured — ${stage.error}`);\n }\n if (file) lines.push(\"\", `Report written to ${file}`);\n if (link) lines.push(\"\", `Shareable link: ${link}`);\n if (email?.sent) {\n lines.push(\"\", `Emailed to ${email.recipients.join(\", \")} (${email.messageId})`);\n }\n for (const w of warnings) lines.push(\"\", `! ${w}`);\n if (recovery) {\n lines.push(\n \"\",\n \"Neither --out nor the database took this report — wrote a recovery copy instead:\",\n ` ${recovery.htmlPath}`,\n ` ${recovery.jsonPath}`,\n );\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Run one prospect audit end to end. Progress goes to stderr so `--json` stdout\n * stays pipeable. Persistence needs Turso; without it `--out` is mandatory,\n * because an audit nobody can read afterwards is just a bill.\n *\n * By the time `runProspectAudit` resolves, the money is already spent — only\n * the crawl is fatal, every other stage degrades rather than throwing. So the\n * `--out` write and the database persist below are each wrapped in their own\n * try/catch and attempted independently: a bad `--out` path must not cost a\n * working Turso persist (or vice versa), and if BOTH fail — or persist fails\n * with no `--out` given at all — a recovery copy goes to `reports/` rather\n * than the run ending with nothing anywhere. `--json` only changes the SHAPE\n * of stdout; it still runs the same `--out`/persist/recovery logic beneath it.\n */\nexport async function runProspectAuditCommand(\n url: string,\n opts: ProspectAuditCliOptions,\n): Promise<{ output: string; code: number }> {\n if (!isHttpUrl(url)) {\n return fail(`\"${url}\" is not a URL. Pass the full address, e.g. https://example.com`);\n }\n // #612 review: this validated only the SHAPE. The sole private-address guard\n // was in `triggerProspectAudit` — one layer at the far end of the chain — so\n // anyone running the CLI directly, or any future second caller, had none.\n // `crawlSite` now refuses too; this keeps the refusal a clean message instead\n // of a thrown crawl error.\n if (isPrivateOrLoopbackHost(new URL(url).hostname)) {\n return fail(`\"${url}\" is a private or loopback address — refusing to audit it.`);\n }\n const canPersist = Boolean(process.env.TURSO_DATABASE_URL);\n if (!canPersist && !opts.out) {\n return fail(\n \"No TURSO_DATABASE_URL, so the report cannot be saved or shared. Re-run with --out <file>, or set the Turso credentials.\",\n );\n }\n\n const { runProspectAudit } = await import(\"../../prospect/pipeline.js\");\n const onStage = (name: StageName, status: \"start\" | \"ok\" | \"fail\", detail?: string): void => {\n if (status === \"start\") console.error(`… ${name}`);\n else if (status === \"ok\") console.error(`✓ ${name}`);\n else console.error(`! ${name} — ${detail ?? \"failed\"}`);\n };\n\n const business = opts.business?.trim();\n const result = await runProspectAudit(\n url,\n {\n ...(business ? { business } : {}),\n ...(opts.competitors\n ? {\n competitors: opts.competitors\n .split(\",\")\n .map((c) => c.trim())\n .filter(Boolean),\n }\n : {}),\n ...(opts.probes === false ? { probes: false } : {}),\n },\n { ...(opts.deps ?? {}), onStage },\n );\n\n const { renderProspectReport } = await import(\"../../prospect/render.js\");\n const html = renderProspectReport(result);\n\n const warnings: string[] = [];\n\n let file: string | null = null;\n if (opts.out) {\n try {\n await writeFile(opts.out, html, \"utf-8\");\n file = opts.out;\n } catch (err) {\n warnings.push(`Could not write --out (${opts.out}): ${errorMessage(err)}`);\n }\n }\n\n let link: string | null = null;\n let token: string | null = null;\n let auditId: string | null = null;\n if (canPersist) {\n try {\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { createProspectAudit } = await import(\"../../db/prospect-audits.js\");\n const db = await openDb(readDbConfig());\n const created = await createProspectAudit(db, {\n url: result.url,\n // Map at the boundary: ProspectAuditResult.businessName is the field\n // name (Item 2 — it's a resolved NAME, not a description); the\n // `prospect_audits.business` column keeps its existing name, since\n // renaming a column needs its own migration for no benefit here.\n business: result.businessName,\n status: auditStatus(result),\n resultJson: JSON.stringify(result),\n });\n auditId = created.id;\n token = created.token;\n link = reportUrl(token);\n } catch (err) {\n warnings.push(`Could not save to the database: ${errorMessage(err)}`);\n }\n }\n\n const delivered = file !== null || link !== null;\n const recovery = delivered ? null : await writeRecoveryFiles(result, html);\n const code = delivered ? 0 : 1;\n\n // The audit is already delivered (or recovered) above — an email failure\n // must never take that away. Same independent try/catch shape as the\n // --out write and the Turso persist: record a warning, keep going, never\n // touch `code`. Dynamically imported for the same reason db/client.js and\n // prospect/render.js are above: it reaches `resend` (a devDependency) and\n // must stay out of bin.js's static import graph (see bin.ts's\n // central-dep-blocker comment).\n // The PDF leave-behind, printed from the website's print route — a document\n // designed for paper, not the interactive report with its evidence folded\n // away behind disclosures.\n //\n // Best-effort by design. Every other stage in this pipeline degrades rather\n // than throwing, and an attachment is not worth losing a delivered report\n // over: if this fails the email still goes, with the link, and a warning says\n // the PDF is missing. It also needs a token — with no persisted report there\n // is no page to print.\n let pdf: Buffer | null = null;\n if (opts.email && token) {\n try {\n const { renderReportPdf } = await import(\"../../prospect/pdf.js\");\n pdf = await renderReportPdf(reportPrintUrl(token));\n } catch (err) {\n warnings.push(`Could not attach the PDF: ${errorMessage(err)}`);\n }\n }\n\n let email: SendAuditEmailResult | null = null;\n if (opts.email) {\n try {\n const { sendAuditEmail, parseProspectAuditRecipients } =\n await import(\"../../prospect/email.js\");\n const recipients = parseProspectAuditRecipients(process.env.PROSPECT_AUDIT_RECIPIENTS);\n email = await sendAuditEmail(result, { link, recipients, auditId, pdf });\n if (!email.sent) {\n warnings.push(`Could not email the audit sheet: ${email.reason}`);\n }\n } catch (err) {\n warnings.push(`Could not email the audit sheet: ${errorMessage(err)}`);\n }\n }\n\n if (opts.json) {\n return {\n output: JSON.stringify(\n { result, out: file, link, token, recovery, warnings, email },\n null,\n 2,\n ),\n code,\n };\n }\n\n return { output: summarize(result, link, file, warnings, recovery, email), code };\n}\n","/** Where the prospect-facing report lives now: a real, branded route on the\n * marketing site rather than generated HTML on the ops app's domain. */\nexport const DEFAULT_REPORT_BASE_URL = \"https://reddoorla.com\";\n\n/**\n * Resolve the public report origin.\n *\n * Deliberately its own variable rather than reusing `DASHBOARD_BASE_URL`. That\n * one addresses operators and points at the ops app; this one addresses a\n * prospect and points at reddoorla.com. They are different audiences on\n * different domains, and sharing a variable would mean one of them silently\n * moving the day the other is repointed.\n */\nexport function resolveReportBaseUrl(raw: string | undefined): string {\n return (raw?.trim() || DEFAULT_REPORT_BASE_URL).replace(/\\/$/, \"\");\n}\n\n/** The link a prospect opens. */\nexport function reportUrl(token: string, raw = process.env.REPORT_BASE_URL): string {\n return `${resolveReportBaseUrl(raw)}/audit/${token}`;\n}\n\n/** The print-designed variant, which the runner renders to a PDF. Same token,\n * same guards — a separate document, not a stylesheet over the page. */\nexport function reportPrintUrl(token: string, raw = process.env.REPORT_BASE_URL): string {\n return `${reportUrl(token, raw)}/print`;\n}\n"],"mappings":"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","names":[]}
@@ -2,9 +2,9 @@ import {
2
2
  ALL_CONFIG_NAMES,
3
3
  isConfigName,
4
4
  syncConfigs
5
- } from "../chunk-7UAYMAKR.js";
6
- import "../chunk-73UFRZKY.js";
5
+ } from "../chunk-3VQWSE3F.js";
7
6
  import "../chunk-3G25KIWW.js";
7
+ import "../chunk-73UFRZKY.js";
8
8
  import "../chunk-XTK5VIQB.js";
9
9
  import "../chunk-XXTZBPUY.js";
10
10
  export {
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  renderProspectReport
3
- } from "./chunk-XKDCR4UE.js";
4
- import "./chunk-CEW5PEHZ.js";
3
+ } from "./chunk-5PWB3JHJ.js";
4
+ import "./chunk-DXKF552B.js";
5
5
  import "./chunk-BGTYPVLT.js";
6
6
  import "./chunk-RARREDQE.js";
7
7
  export {
8
8
  renderProspectReport
9
9
  };
10
- //# sourceMappingURL=render-CVKE7IWC.js.map
10
+ //# sourceMappingURL=render-P3SBUFVB.js.map
@@ -2,10 +2,10 @@ import {
2
2
  renderReportFromRow,
3
3
  requireLighthouse,
4
4
  windowDays
5
- } from "./chunk-XP5WM2LV.js";
5
+ } from "./chunk-SKI23ZZH.js";
6
6
  import "./chunk-GECESMVS.js";
7
- import "./chunk-5UINJNAJ.js";
8
7
  import "./chunk-KJGKZLCT.js";
8
+ import "./chunk-5UINJNAJ.js";
9
9
  import "./chunk-BGTYPVLT.js";
10
10
  import "./chunk-7NYN2P6I.js";
11
11
  import "./chunk-RARREDQE.js";
@@ -14,4 +14,4 @@ export {
14
14
  requireLighthouse,
15
15
  windowDays
16
16
  };
17
- //# sourceMappingURL=render-from-row-DOYJDO2L.js.map
17
+ //# sourceMappingURL=render-from-row-P5CWM3MQ.js.map
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  draftReportForSite,
6
6
  readGaConfig
7
- } from "./chunk-J3WEZGIN.js";
7
+ } from "./chunk-ZRI5VRPG.js";
8
8
  import {
9
9
  reportTier
10
10
  } from "./chunk-YFAXOD7Z.js";
@@ -22,8 +22,8 @@ import {
22
22
  openBase,
23
23
  readAirtableConfig
24
24
  } from "./chunk-HN6I2XWI.js";
25
- import "./chunk-5UINJNAJ.js";
26
25
  import "./chunk-KJGKZLCT.js";
26
+ import "./chunk-5UINJNAJ.js";
27
27
  import {
28
28
  escapeHtml
29
29
  } from "./chunk-BGTYPVLT.js";
@@ -78,19 +78,35 @@ function dashboardBaseUrl() {
78
78
  }
79
79
  async function runReportCommand(slug, opts) {
80
80
  if (opts.digest) {
81
- const { runDigest } = await import("./digest-NBLECFM7.js");
81
+ const { runDigest } = await import("./digest-T6TAZNLF.js");
82
82
  return runDigest({ baseUrl: dashboardBaseUrl() });
83
83
  }
84
84
  if (opts.sendReady) {
85
- const { sendApprovedReports } = await import("./orchestrate-JNKCWU5K.js");
86
- const { makeSiteMirror } = await import("./site-mirror-Q763PLGG.js");
87
- return sendApprovedReports({ siteMirror: await makeSiteMirror() });
85
+ const { sendApprovedReports } = await import("./orchestrate-6HROYTM6.js");
86
+ const { makeSiteMirror } = await import("./site-mirror-RQTEAID5.js");
87
+ const { mirrorWrite } = await import("./freeze-A6MUCZU4.js");
88
+ const { openDb, readDbConfig } = await import("./client-JYKPVCJX.js");
89
+ const { mirrorReportPatch } = await import("./fleet-state-3LQUQHYD.js");
90
+ return sendApprovedReports({
91
+ siteMirror: await makeSiteMirror(),
92
+ // stampSent's Turso shadow, routed through mirrorWrite so the freeze
93
+ // switch owns the semantics: strict rethrows and the send loop reds the
94
+ // run. Mirrors stampSent exactly — the 409 replay path leaves Airtable's
95
+ // `Resend message ID` untouched, so the shadow omits it there too.
96
+ reportSentMirror: (reportId, sentAt, messageId) => mirrorWrite(`stamp-sent ${reportId}`, async () => {
97
+ const db = await openDb(readDbConfig());
98
+ await mirrorReportPatch(db, reportId, {
99
+ sent_at: sentAt.toISOString(),
100
+ ...messageId !== null ? { resend_message_id: messageId } : {}
101
+ });
102
+ })
103
+ });
88
104
  }
89
105
  if (opts.rerender) {
90
106
  const { rerenderReport, formatRerenderResult } = await import("./rerender-3FM6DNRH.js");
91
- const { renderReportFromRow } = await import("./render-from-row-DOYJDO2L.js");
107
+ const { renderReportFromRow } = await import("./render-from-row-P5CWM3MQ.js");
92
108
  const { openDb, readDbConfig } = await import("./client-JYKPVCJX.js");
93
- const { getReportById, getSiteById, storeRenderedHtml } = await import("./fleet-state-R5LU7IC3.js");
109
+ const { getReportById, getSiteById, storeRenderedHtml } = await import("./fleet-state-3LQUQHYD.js");
94
110
  const { loadHeaderImage } = await import("./header-images-SQBCE5OD.js");
95
111
  const { fetchAttachmentBytes } = await import("./attachments-NLZIJ5NU.js");
96
112
  const db = await openDb(readDbConfig());
@@ -129,9 +145,9 @@ async function runReportCommand(slug, opts) {
129
145
  }
130
146
  async function runDueDraft() {
131
147
  const base = openBase(readAirtableConfig());
132
- const { makeScheduleMirrorBestEffort } = await import("./health-mirror-GCDN7BJL.js");
133
- const { makeReportMirror } = await import("./report-mirror-AAK3MXHY.js");
134
- const { makeSiteMirror } = await import("./site-mirror-Q763PLGG.js");
148
+ const { makeScheduleMirrorBestEffort } = await import("./health-mirror-7LKC7Q5Z.js");
149
+ const { makeReportMirror } = await import("./report-mirror-3ENU6OOK.js");
150
+ const { makeSiteMirror } = await import("./site-mirror-RQTEAID5.js");
135
151
  const result = await draftDueReports(
136
152
  base,
137
153
  /* @__PURE__ */ new Date(),
@@ -321,8 +337,8 @@ async function runSingleSiteDraft(slug, opts) {
321
337
  if (!site) {
322
338
  throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
323
339
  }
324
- const reportMirror = opts.previewOnly ? null : await (await import("./report-mirror-AAK3MXHY.js")).makeReportMirror();
325
- const siteMirror = opts.previewOnly ? null : await (await import("./site-mirror-Q763PLGG.js")).makeSiteMirror();
340
+ const reportMirror = opts.previewOnly ? null : await (await import("./report-mirror-3ENU6OOK.js")).makeReportMirror();
341
+ const siteMirror = opts.previewOnly ? null : await (await import("./site-mirror-RQTEAID5.js")).makeSiteMirror();
326
342
  const result = await draftReportForSite(opts.previewOnly ? null : base, site, opts.reportType, {
327
343
  previewOnly: opts.previewOnly,
328
344
  // Only forced on. Left undefined, draftReportForSite keeps its default
@@ -350,4 +366,4 @@ export {
350
366
  runReportCommand,
351
367
  writeNextDueDates
352
368
  };
353
- //# sourceMappingURL=report-2LM2SX67.js.map
369
+ //# sourceMappingURL=report-2N4PXO3L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alerts/analytics-health.ts","../src/cli/commands/report.ts"],"sourcesContent":["import { escapeHtml } from \"../util/html.js\";\n\n/**\n * The GA/Search enrichment outcome of one `report --due` draft run, aggregated\n * across the sites it drafted. `softFailedSites` is how many drafted sites had GA\n * OR Search enrichment ERROR (not \"not configured\" — that's a legitimate skip);\n * `configuredSites` is how many drafted sites had analytics configured at all\n * (the subject is set AND the site has a GA4 property or a search query). The ratio\n * is what distinguishes a fleet-wide subject outage from a single transient blip.\n */\nexport type AnalyticsRunHealth = {\n softFailedSites: number;\n configuredSites: number;\n};\n\nexport type AnalyticsAlert = { fire: boolean; reason: string };\n\n/** A lone failure never alerts — that's a per-site config issue, not the shared-\n * subject SPOF. Two-plus AND a majority of configured sites is the fleet signature. */\nconst MIN_FAILED_SITES = 2;\n\n/**\n * Decide whether a draft run's GA/Search soft-failures look FLEET-WIDE — i.e. the\n * one impersonated subject (`GA_SUBJECT`) lost access — rather than one site's\n * transient blip. PURE. Fires when at least {@link MIN_FAILED_SITES} configured\n * sites failed AND they are a majority (≥ half) of the analytics-configured sites\n * this run. With <2 configured sites it can't distinguish a SPOF from a one-off, so\n * it never fires (at fleet scale there are always many configured sites).\n */\nexport function assessAnalyticsAlert(h: AnalyticsRunHealth): AnalyticsAlert {\n const { softFailedSites, configuredSites } = h;\n const fire =\n configuredSites >= 2 &&\n softFailedSites >= MIN_FAILED_SITES &&\n softFailedSites * 2 >= configuredSites;\n const reason = fire\n ? `${softFailedSites} of ${configuredSites} analytics-configured sites had GA/Search enrichment fail this run — the shared GA_SUBJECT likely lost access (an offboarded user, revoked property access, or a botched role-account cutover). Reports were drafted with BLANK analytics.`\n : \"\";\n return { fire, reason };\n}\n\n/**\n * Compose the operator alert email for a fleet-wide analytics failure. PURE — the\n * caller (the `report --due` cron) sends it best-effort via Resend only when\n * {@link assessAnalyticsAlert} fires. `dashboardUrl` is the fleet homepage link.\n */\nexport function composeAnalyticsAlertEmail(\n h: AnalyticsRunHealth,\n dashboardUrl: string,\n): { subject: string; html: string } {\n const subject = `⚠ Fleet analytics enrichment failing — ${h.softFailedSites}/${h.configuredSites} sites`;\n const { reason } = assessAnalyticsAlert(h);\n const html = `<p><strong>${escapeHtml(reason)}</strong></p>\n<p>This usually means the Google Workspace user the service account impersonates can no longer read the GA4 / Search Console properties. Reports still send — but with blank analytics — until the subject is restored.</p>\n<p>Next step: follow the GA/Search subject runbook (<code>docs/runbooks/ga-search-role-account-cutover.md</code>) to restore or move the subject, then re-run <code>reddoor-maint report --due</code> and confirm the warning clears.</p>\n<p><a href=\"${escapeHtml(dashboardUrl)}\">Open the fleet dashboard →</a></p>`;\n return { subject, html };\n}\n","import { openBase, readAirtableConfig, type AirtableBase } from \"../../reports/airtable/client.js\";\nimport {\n listWebsites,\n siteSlug,\n updateNextDueDates,\n type WebsiteRow,\n} from \"../../reports/airtable/websites.js\";\nimport { listAllReports, type ReportRow } from \"../../reports/airtable/reports.js\";\nimport { findDueReports, nextDueDate, reportPeriodKey } from \"../../reports/due.js\";\nimport { draftReportForSite } from \"../../reports/draft.js\";\nimport { reportTier } from \"../../reports/queue.js\";\nimport { readGaConfig } from \"../../reports/ga/config.js\";\nimport {\n assessAnalyticsAlert,\n composeAnalyticsAlertEmail,\n type AnalyticsRunHealth,\n} from \"../../alerts/analytics-health.js\";\nimport type { ReportType } from \"../../reports/types.js\";\nimport type { ScheduleMirror } from \"../../audits/health-mirror.js\";\nimport type { ReportMirror } from \"../../reports/report-mirror.js\";\nimport type { SiteMirror } from \"../../db/site-mirror.js\";\nimport { operatorEmail } from \"../../util/operator.js\";\n\nexport type ReportCommandOptions = {\n due?: boolean;\n preview?: boolean;\n enrich?: boolean;\n sendReady?: boolean;\n digest?: boolean;\n rerender?: string;\n type?: string;\n cwd?: string;\n};\n\n/**\n * Summary line for a drafted report, reflecting the single-queue outcome. `queued === false`\n * means a higher-or-equal-tier report was already pending for the site, so this draft was\n * created but deliberately left OUT of the approve queue. A non-empty `supersededIds` means it\n * un-queued that many lower-tier drafts. `null` is the previewOnly path (no Airtable queue).\n */\nfunction draftLine(\n reportId: string | undefined,\n queued: boolean | null,\n supersededIds: string[],\n verb = \"drafted\",\n): string {\n const id = reportId ?? \"(unknown)\";\n if (queued === false) {\n return `• ${verb} but NOT queued: ${id} — a higher-or-equal-tier report is already pending approval`;\n }\n const sup =\n supersededIds.length > 0\n ? ` (superseded ${supersededIds.length} lower-tier draft${supersededIds.length > 1 ? \"s\" : \"\"})`\n : \"\";\n return `✓ ${verb}: ${id}${sup}`;\n}\n\n/**\n * Parse the single-site `--type` flag. Only Maintenance and Testing are draftable\n * this way — Launch has the `launch <site>` command and Announcement has\n * `announce <site>`, each with its own purpose-built flow. Case-insensitive;\n * defaults to Maintenance (the historical single-site behaviour). Throws an\n * exitCode-2 usage error on anything else. PURE.\n */\nexport function parseSingleSiteReportType(raw: string | undefined): ReportType {\n if (raw === undefined || raw.trim() === \"\") return \"Maintenance\";\n const norm = raw.trim().toLowerCase();\n if (norm === \"maintenance\") return \"Maintenance\";\n if (norm === \"testing\") return \"Testing\";\n const hint =\n norm === \"launch\"\n ? \" — use the `launch <site>` command\"\n : norm === \"announcement\"\n ? \" — use the `announce <site>` command\"\n : \"\";\n throw Object.assign(\n new Error(`--type must be Maintenance or Testing (got ${JSON.stringify(raw)})${hint}`),\n { exitCode: 2 },\n );\n}\n\n/** Dashboard origin for digest /s/<slug> links. DASHBOARD_BASE_URL overrides the\n * production default; the trailing slash (if any) is trimmed by runDigest. */\nfunction dashboardBaseUrl(): string {\n return process.env.DASHBOARD_BASE_URL?.trim() || \"https://reddoor-maintenance.netlify.app\";\n}\n\nexport async function runReportCommand(\n slug: string | undefined,\n opts: ReportCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (opts.digest) {\n const { runDigest } = await import(\"../../reports/digest.js\");\n return runDigest({ baseUrl: dashboardBaseUrl() });\n }\n\n if (opts.sendReady) {\n const { sendApprovedReports } = await import(\"../../reports/send/orchestrate.js\");\n // #539 Phase 5: a Launch send flips Status + stamps `Launched at` on the\n // Websites row; #643 retired the hourly sync, so these mirrors are the ONLY\n // way either write reaches Turso.\n const { makeSiteMirror } = await import(\"../../db/site-mirror.js\");\n const { mirrorWrite } = await import(\"../../db/freeze.js\");\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { mirrorReportPatch } = await import(\"../../db/fleet-state.js\");\n return sendApprovedReports({\n siteMirror: await makeSiteMirror(),\n // stampSent's Turso shadow, routed through mirrorWrite so the freeze\n // switch owns the semantics: strict rethrows and the send loop reds the\n // run. Mirrors stampSent exactly — the 409 replay path leaves Airtable's\n // `Resend message ID` untouched, so the shadow omits it there too.\n reportSentMirror: (reportId, sentAt, messageId) =>\n mirrorWrite(`stamp-sent ${reportId}`, async () => {\n const db = await openDb(readDbConfig());\n await mirrorReportPatch(db, reportId, {\n sent_at: sentAt.toISOString(),\n ...(messageId !== null ? { resend_message_id: messageId } : {}),\n });\n }),\n });\n }\n\n // Refresh ONE unsent report's stored body so the console preview reflects\n // commentary edited after drafting. Runs here rather than in a Netlify\n // function because rendering needs sharp — see rerender.ts.\n if (opts.rerender) {\n const { rerenderReport, formatRerenderResult } = await import(\"../../reports/send/rerender.js\");\n const { renderReportFromRow } = await import(\"../../reports/send/render-from-row.js\");\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { getReportById, getSiteById, storeRenderedHtml } =\n await import(\"../../db/fleet-state.js\");\n const { loadHeaderImage } = await import(\"../../db/header-images.js\");\n const { fetchAttachmentBytes } = await import(\"../../reports/airtable/attachments.js\");\n const db = await openDb(readDbConfig());\n const result = await rerenderReport(\n {\n getReport: (id) => getReportById(db, id),\n getSite: (id) => getSiteById(db, id),\n loadHeaderPlate: async (id) => (await loadHeaderImage(db, id))?.bytes ?? null,\n fetchAirtableHeader: async (url) => (await fetchAttachmentBytes(url)).bytes,\n render: (site, report, plate) => renderReportFromRow(site, report, plate),\n store: (id, html) => storeRenderedHtml(db, id, html),\n },\n opts.rerender,\n );\n // Exit 1 on anything but a render, so a dispatched run that refused is RED\n // rather than a green run that silently changed nothing.\n return { output: formatRerenderResult(result), code: result.status === \"rendered\" ? 0 : 1 };\n }\n\n if (opts.due) {\n return runDueDraft();\n }\n\n if (slug) {\n // Validate the type BEFORE any Airtable access so a bad --type fails fast (and\n // without needing credentials).\n const reportType = parseSingleSiteReportType(opts.type);\n return runSingleSiteDraft(slug, {\n previewOnly: Boolean(opts.preview),\n enrich: Boolean(opts.enrich),\n reportType,\n });\n }\n\n throw Object.assign(\n new Error(\n \"Usage: reddoor-maint report [<slug>] [--type <Maintenance|Testing>] [--due] [--preview] [--enrich] [--send-ready] [--digest]\",\n ),\n {\n exitCode: 2,\n },\n );\n}\n\nasync function runDueDraft(): Promise<{ output: string; code: number }> {\n const base = openBase(readAirtableConfig());\n // Phase 3 dual-write (#539): mirror real next-due writes into site_schedule.\n // Null when libSQL creds are absent — the Airtable path is unchanged.\n const { makeScheduleMirrorBestEffort } = await import(\"../../audits/health-mirror.js\");\n // Phase 5 dual-write (#539): mirror this batch's report writes — the created\n // rows, their bodies, and the queue flags. Unlike the schedule mirror this is\n // never null — creds-absent is reported on the REPORT_MIRROR line rather than\n // being indistinguishable from success, the failure mode that hid #585.\n const { makeReportMirror } = await import(\"../../reports/report-mirror.js\");\n const { makeSiteMirror } = await import(\"../../db/site-mirror.js\");\n const result = await draftDueReports(\n base,\n new Date(),\n await makeScheduleMirrorBestEffort(),\n await makeReportMirror(),\n await makeSiteMirror(),\n );\n await alertOnFleetAnalyticsFailure(result.health);\n return { output: result.output, code: result.code };\n}\n\n/** Best-effort: when a draft run's GA/Search soft-failures look FLEET-WIDE (the shared\n * GA_SUBJECT lost access — see assessAnalyticsAlert), email the operator one alert.\n * NEVER throws — a Resend or config hiccup must not fail the nightly draft cron; the\n * per-site `⚠ GA skipped` warnings + the run-output line still carry the signal. The\n * daily idempotency key dedupes multiple runs in one day (Resend dedupes 24h). */\nasync function alertOnFleetAnalyticsFailure(health: AnalyticsRunHealth): Promise<void> {\n if (!assessAnalyticsAlert(health).fire) return;\n try {\n const to = operatorEmail();\n const { subject, html } = composeAnalyticsAlertEmail(health, dashboardBaseUrl());\n const { defaultResendClient } = await import(\"../../reports/send/resend.js\");\n await defaultResendClient().send({\n from: \"Reddoor Reports <reports@reddoorla.com>\",\n to: [to],\n subject,\n html,\n idempotencyKey: `analytics-alert-${new Date().toISOString().slice(0, 10)}`,\n });\n console.warn(`⚠ ${subject} — operator alerted (${to})`);\n } catch (e) {\n console.warn(`⚠ analytics-failure alert send failed: ${(e as Error).message}`);\n }\n}\n\n/**\n * Write each site's code-computed next-maintenance / next-testing date back to Airtable\n * (date-only, or null when there's no schedule), so the \"next\" dates shown there derive\n * from the SAME `nextDueDate` the scheduler uses — replacing the old Airtable formula +\n * automation. Best-effort and per-site isolated: a missing `Next … at` column or one bad\n * row warns and is skipped, never aborting the nightly draft run.\n *\n * The diff-guard (#539 Phase 3): a site whose computed dates equal what its row\n * already holds is SKIPPED — before it, every one of the 44 sites got a nightly\n * write while only the handful whose schedule actually moved needed one (a\n * never-maintained site re-wrote null over null forever). This also scopes the\n * write to maintained sites by construction: no schedule → computed null →\n * equal to the stored null → skipped. Each real write dual-writes through\n * `scheduleMirror` into site_schedule (null mirror = Turso creds absent; the\n * hourly sync converges either way). The NEXT_DUE_WRITE line is observability,\n * not a CI gate — nothing greps it yet.\n */\nexport async function writeNextDueDates(\n base: AirtableBase,\n websites: WebsiteRow[],\n reports: ReportRow[],\n today: Date,\n scheduleMirror: ScheduleMirror | null = null,\n): Promise<void> {\n const ymd = (d: Date | null): string | null => (d ? d.toISOString().slice(0, 10) : null);\n let wrote = 0;\n let skipped = 0;\n let failed = 0;\n let mirrored = 0;\n let mirrorFailed = 0;\n let mirrorMissed = 0;\n for (const site of websites) {\n // The whole per-site body sits in ONE try — compute included — so a bad\n // row can only cost its own write, exactly the pre-diff-guard blast radius.\n try {\n const maintenanceAt = ymd(nextDueDate(site, reports, \"Maintenance\", today));\n const testingAt = ymd(nextDueDate(site, reports, \"Testing\", today));\n if (maintenanceAt === site.nextMaintenanceAt && testingAt === site.nextTestingAt) {\n skipped++;\n continue;\n }\n const fields = await updateNextDueDates(base, site.id, { maintenanceAt, testingAt });\n wrote++;\n if (scheduleMirror) {\n try {\n // false = the UPDATE matched no site_schedule row (site created in\n // Airtable since the last hourly import) — missed, not mirrored.\n if (await scheduleMirror(site.id, fields, today.toISOString())) mirrored++;\n else mirrorMissed++;\n } catch (e) {\n mirrorFailed++;\n console.warn(`⚠ [schedule-mirror] ${site.name}: ${(e as Error).message}`);\n }\n }\n } catch (e) {\n failed++;\n console.warn(`⚠ next-due write skipped for ${site.name}: ${(e as Error).message}`);\n }\n }\n // failed= keeps a write outage visible: without it wrote+skipped silently\n // undercounts, and a FULL outage prints wrote=0 skipped=0 — indistinguishable\n // from an empty fleet.\n // A null mirror writes nothing and throws nothing, so its only trace used to\n // be an ABSENT suffix — which reads exactly like a healthy run. `mirror=absent`\n // states it instead. Counters stay off in that case deliberately: `mirrored=0`\n // would claim a write that returned nothing, not one that was never attempted.\n const mirrorNote = scheduleMirror\n ? ` mirrored=${mirrored} mirror_failed=${mirrorFailed} mirror_missed=${mirrorMissed}`\n : \" mirror=absent\";\n console.log(`NEXT_DUE_WRITE wrote=${wrote} skipped=${skipped} failed=${failed}${mirrorNote}`);\n}\n\nexport async function draftDueReports(\n base: AirtableBase,\n today: Date,\n scheduleMirror: ScheduleMirror | null = null,\n /** #539 Phase 5: Turso write-through for rows this batch CREATES. Forwarded\n * to every draftReportForSite call — the nightly batch is the only unattended\n * creator of report rows, so a dropped pass-through leaves each new draft\n * invisible to the Turso-backed console. */\n reportMirror?: ReportMirror,\n /** #539 Phase 5: the Websites-row twin — drafting stamps `Analytics soft-fail\n * at` on the SITE row, a different Turso table from the report. */\n siteMirror?: SiteMirror,\n): Promise<{ output: string; code: number; health: AnalyticsRunHealth }> {\n const mirrorOpt = {\n ...(reportMirror ? { reportMirror } : {}),\n ...(siteMirror ? { siteMirror } : {}),\n };\n const websites = await listWebsites(base);\n // ONE unfiltered fetch for the whole fleet. Per-site queries can't be pushed to\n // Airtable anyway (linked-record fields aren't formula-filterable by record id),\n // and findDueReports + the period guard below match on siteId in memory.\n const reports = await listAllReports(base);\n\n // Refresh every site's code-owned next-due dates first, so they stay current even on\n // a run where nothing is due (the early return below).\n await writeNextDueDates(base, websites, reports, today, scheduleMirror);\n\n const due = findDueReports(websites, reports, today);\n\n // GA/Search enrichment is configured globally (the impersonation subject) AND\n // per-site (a GA4 property or a search query). `gaConfigured` is the global half;\n // the per-site half is checked as each draft runs, to build the fleet-wide\n // analytics-failure alert's denominator (see alertOnFleetAnalyticsFailure).\n const gaConfigured = readGaConfig() !== null;\n // Truthy (not `!== null`) to mirror fetchGaUsers/fetchSearch's own gate exactly\n // (`!siteRow.ga4PropertyId`), so an empty-string cell counts as not-configured in\n // BOTH places and can't inflate the alert denominator.\n const isAnalyticsConfigured = (s: WebsiteRow): boolean =>\n gaConfigured && Boolean(s.ga4PropertyId || s.searchQuery);\n\n if (due.length === 0) {\n return {\n output: \"No reports due.\",\n code: 0,\n health: { softFailedSites: 0, configuredSites: 0 },\n };\n }\n\n const lines: string[] = [];\n let softFailedSites = 0;\n let searchDefaultMisses = 0;\n let searchPropertiesMissing = 0;\n let gaConfiguredSites = 0;\n let skipped = 0;\n for (const item of due) {\n // Idempotency: a re-run must not re-draft a (site, type) already drafted this\n // recurrence. The dueDate's YYYY-MM is the stable per-cycle key. Match against the\n // reports we already fetched — no extra query on the hot path.\n const period = reportPeriodKey(item.dueDate);\n const existing = reports.find(\n (r) => r.siteId === item.site.id && r.reportType === item.reportType && r.period === period,\n );\n\n // A row already exists for THIS period. Two cases:\n // - Draft ready → truly done, skip (the idempotent re-run path).\n // - NOT ready → a crash between createDraft and setDraftReady wedged it: the\n // row exists (so we never re-draft) yet it's never sendable (listSendable\n // needs Draft ready). COMPLETE it in place instead of skipping forever —\n // re-render → re-upload the HTML → flip Draft ready on the EXISTING row.\n if (existing) {\n if (existing.draftReady) {\n skipped++;\n lines.push(`• skipped (already drafted ${period}): ${item.site.name} ${item.reportType}`);\n continue;\n }\n // A not-ready row is normally a crash between createDraft and setDraftReady — re-complete\n // it in place. BUT queueDraft also clears Draft ready on rows it supersedes/blocks, and\n // those must NOT be re-completed: doing so would re-render and APPEND a duplicate HTML\n // attachment every nightly run, only to be re-blocked. Distinguish the two: if a\n // higher-or-equal-tier report is still pending for this site, this row was intentionally\n // un-queued (not crashed) — skip it until the blocker is sent/approved or the month rolls.\n const blockedByPending = reports.some(\n (r) =>\n r.siteId === item.site.id &&\n r.id !== existing.id &&\n r.sentAt === null &&\n r.draftReady &&\n reportTier(r.reportType) >= reportTier(item.reportType),\n );\n if (blockedByPending) {\n skipped++;\n lines.push(\n `• skipped (superseded — a higher-or-equal-tier report is pending): ${item.site.name} ${item.reportType}`,\n );\n continue;\n }\n try {\n const result = await draftReportForSite(base, item.site, item.reportType, {\n period,\n completeRowId: existing.id,\n existingRow: existing,\n ...mirrorOpt,\n });\n existing.draftReady = result.queued === true;\n lines.push(\n draftLine(\n result.reportRow?.reportId ?? existing.reportId,\n result.queued,\n result.supersededIds,\n \"completed half-made draft\",\n ),\n );\n if (isAnalyticsConfigured(item.site)) gaConfiguredSites++;\n if (result.softFailures.length > 0) softFailedSites++;\n if (result.searchDefaultMissed) searchDefaultMisses++;\n if (result.searchPropertyMissing) searchPropertiesMissing++;\n } catch (e) {\n lines.push(`✗ failed: ${item.site.name} ${item.reportType} — ${(e as Error).message}`);\n }\n continue;\n }\n\n // Pile-up guard: don't accrue a fresh new-period draft every recurrence for a\n // site nobody ever approves. The period key follows the DUE month, so each\n // recurrence wants a new (later-period) draft — but if a PRIOR draft is still\n // pending approval, a new one just stacks. Skip creating the new one while an\n // earlier-period draft for this (site, type) sits ready-but-unsent.\n //\n // `r.draftReady` is load-bearing: a draft a higher tier SUPERSEDED has\n // draftReady=false and never gets a Sent at, so without this clause it would\n // match (sentAt null + earlier period) and block EVERY future draft for the\n // site forever. Pending-approval means draftReady=true AND sentAt=null.\n const pendingEarlier = reports.find(\n (r) =>\n r.siteId === item.site.id &&\n r.reportType === item.reportType &&\n r.draftReady &&\n r.sentAt === null &&\n r.period !== null &&\n r.period < period,\n );\n if (pendingEarlier) {\n skipped++;\n lines.push(\n `• skipped: ${item.site.name} ${item.reportType} already has an unsent ${pendingEarlier.period} draft pending approval`,\n );\n continue;\n }\n\n try {\n // Pass the SAME key the guard searches by, so the stamped Period always\n // matches a future run's reportPeriodKey(dueDate) — even if this run lags\n // into a later month than the dueDate.\n const result = await draftReportForSite(base, item.site, item.reportType, {\n period,\n ...mirrorOpt,\n });\n lines.push(draftLine(result.reportRow?.reportId, result.queued, result.supersededIds));\n // Keep the in-memory snapshot current so the guard's `.some()` check on the\n // NEXT iteration of this same run catches a row we JUST created — rather than\n // relying on findDueReports never emitting two items for the same (site, type).\n if (result.reportRow) reports.push(result.reportRow);\n // Count sites (not individual GA/Search failures) so a fleet-wide enrichment\n // outage is one obvious line at the bottom, not 200 buried console.warns.\n if (isAnalyticsConfigured(item.site)) gaConfiguredSites++;\n if (result.softFailures.length > 0) softFailedSites++;\n if (result.searchDefaultMissed) searchDefaultMisses++;\n if (result.searchPropertyMissing) searchPropertiesMissing++;\n } catch (e) {\n lines.push(`✗ failed: ${item.site.name} ${item.reportType} — ${(e as Error).message}`);\n }\n }\n if (skipped > 0) {\n lines.push(`• ${skipped} already drafted or pending this period`);\n }\n if (softFailedSites > 0) {\n lines.push(\n `⚠ ${softFailedSites} site${softFailedSites === 1 ? \"\" : \"s\"} had GA/Search enrichment fail — drafted with blank analytics; check the logs above`,\n );\n }\n if (searchDefaultMisses > 0) {\n lines.push(\n `⚑ ${searchDefaultMisses} site${searchDefaultMisses === 1 ? \"\" : \"s\"} returned no Search Console data for their name — set an explicit \"Search query\" in Airtable to track brand presence.`,\n );\n }\n if (searchPropertiesMissing > 0) {\n lines.push(\n `⚑ ${searchPropertiesMissing} site${searchPropertiesMissing === 1 ? \"\" : \"s\"} matched NO Search Console property — verify the domain property exists and the service account has access (a \"Search query\" change cannot fix this).`,\n );\n }\n return {\n output: lines.join(\"\\n\"),\n code: lines.some((l) => l.startsWith(\"✗\")) ? 1 : 0,\n health: { softFailedSites, configuredSites: gaConfiguredSites },\n };\n}\n\nasync function runSingleSiteDraft(\n slug: string,\n opts: { previewOnly: boolean; enrich: boolean; reportType: ReportType },\n): Promise<{ output: string; code: number }> {\n const base = openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const site = websites.find((w) => siteSlug(w.name) === slug);\n if (!site) {\n throw Object.assign(new Error(`No Websites row matched slug \"${slug}\"`), { exitCode: 2 });\n }\n // Phase 5 dual-write (#539) — but only on the path that actually creates a\n // row. A preview writes nothing to Airtable, so opening a libSQL handle for\n // it would be pure cost (and `report --preview` is the one draft path that\n // deliberately does no store IO at all).\n const reportMirror = opts.previewOnly\n ? null\n : await (await import(\"../../reports/report-mirror.js\")).makeReportMirror();\n const siteMirror = opts.previewOnly\n ? null\n : await (await import(\"../../db/site-mirror.js\")).makeSiteMirror();\n const result = await draftReportForSite(opts.previewOnly ? null : base, site, opts.reportType, {\n previewOnly: opts.previewOnly,\n // Only forced on. Left undefined, draftReportForSite keeps its default\n // (enrich iff there is a base), so the real drafting path is untouched.\n ...(opts.enrich ? { enrich: true } : {}),\n ...(reportMirror ? { reportMirror } : {}),\n ...(siteMirror ? { siteMirror } : {}),\n });\n if (opts.previewOnly) {\n return { output: `Preview written to ${result.htmlPath}`, code: 0 };\n }\n return {\n output: draftLine(\n result.reportRow?.reportId,\n result.queued,\n result.supersededIds,\n \"Draft created\",\n ),\n code: 0,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBA,IAAM,mBAAmB;AAUlB,SAAS,qBAAqB,GAAuC;AAC1E,QAAM,EAAE,iBAAiB,gBAAgB,IAAI;AAC7C,QAAM,OACJ,mBAAmB,KACnB,mBAAmB,oBACnB,kBAAkB,KAAK;AACzB,QAAM,SAAS,OACX,GAAG,eAAe,OAAO,eAAe,oPACxC;AACJ,SAAO,EAAE,MAAM,OAAO;AACxB;AAOO,SAAS,2BACd,GACA,cACmC;AACnC,QAAM,UAAU,oDAA0C,EAAE,eAAe,IAAI,EAAE,eAAe;AAChG,QAAM,EAAE,OAAO,IAAI,qBAAqB,CAAC;AACzC,QAAM,OAAO,cAAc,WAAW,MAAM,CAAC;AAAA;AAAA;AAAA,cAGjC,WAAW,YAAY,CAAC;AACpC,SAAO,EAAE,SAAS,KAAK;AACzB;;;ACjBA,SAAS,UACP,UACA,QACA,eACA,OAAO,WACC;AACR,QAAM,KAAK,YAAY;AACvB,MAAI,WAAW,OAAO;AACpB,WAAO,UAAK,IAAI,oBAAoB,EAAE;AAAA,EACxC;AACA,QAAM,MACJ,cAAc,SAAS,IACnB,gBAAgB,cAAc,MAAM,oBAAoB,cAAc,SAAS,IAAI,MAAM,EAAE,MAC3F;AACN,SAAO,UAAK,IAAI,KAAK,EAAE,GAAG,GAAG;AAC/B;AASO,SAAS,0BAA0B,KAAqC;AAC7E,MAAI,QAAQ,UAAa,IAAI,KAAK,MAAM,GAAI,QAAO;AACnD,QAAM,OAAO,IAAI,KAAK,EAAE,YAAY;AACpC,MAAI,SAAS,cAAe,QAAO;AACnC,MAAI,SAAS,UAAW,QAAO;AAC/B,QAAM,OACJ,SAAS,WACL,4CACA,SAAS,iBACP,8CACA;AACR,QAAM,OAAO;AAAA,IACX,IAAI,MAAM,8CAA8C,KAAK,UAAU,GAAG,CAAC,IAAI,IAAI,EAAE;AAAA,IACrF,EAAE,UAAU,EAAE;AAAA,EAChB;AACF;AAIA,SAAS,mBAA2B;AAClC,SAAO,QAAQ,IAAI,oBAAoB,KAAK,KAAK;AACnD;AAEA,eAAsB,iBACpB,MACA,MAC2C;AAC3C,MAAI,KAAK,QAAQ;AACf,UAAM,EAAE,UAAU,IAAI,MAAM,OAAO,sBAAyB;AAC5D,WAAO,UAAU,EAAE,SAAS,iBAAiB,EAAE,CAAC;AAAA,EAClD;AAEA,MAAI,KAAK,WAAW;AAClB,UAAM,EAAE,oBAAoB,IAAI,MAAM,OAAO,2BAAmC;AAIhF,UAAM,EAAE,eAAe,IAAI,MAAM,OAAO,2BAAyB;AACjE,UAAM,EAAE,YAAY,IAAI,MAAM,OAAO,sBAAoB;AACzD,UAAM,EAAE,QAAQ,aAAa,IAAI,MAAM,OAAO,sBAAoB;AAClE,UAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,2BAAyB;AACpE,WAAO,oBAAoB;AAAA,MACzB,YAAY,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA,MAKjC,kBAAkB,CAAC,UAAU,QAAQ,cACnC,YAAY,cAAc,QAAQ,IAAI,YAAY;AAChD,cAAM,KAAK,MAAM,OAAO,aAAa,CAAC;AACtC,cAAM,kBAAkB,IAAI,UAAU;AAAA,UACpC,SAAS,OAAO,YAAY;AAAA,UAC5B,GAAI,cAAc,OAAO,EAAE,mBAAmB,UAAU,IAAI,CAAC;AAAA,QAC/D,CAAC;AAAA,MACH,CAAC;AAAA,IACL,CAAC;AAAA,EACH;AAKA,MAAI,KAAK,UAAU;AACjB,UAAM,EAAE,gBAAgB,qBAAqB,IAAI,MAAM,OAAO,wBAAgC;AAC9F,UAAM,EAAE,oBAAoB,IAAI,MAAM,OAAO,+BAAuC;AACpF,UAAM,EAAE,QAAQ,aAAa,IAAI,MAAM,OAAO,sBAAoB;AAClE,UAAM,EAAE,eAAe,aAAa,kBAAkB,IACpD,MAAM,OAAO,2BAAyB;AACxC,UAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,6BAA2B;AACpE,UAAM,EAAE,qBAAqB,IAAI,MAAM,OAAO,2BAAuC;AACrF,UAAM,KAAK,MAAM,OAAO,aAAa,CAAC;AACtC,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,QACE,WAAW,CAAC,OAAO,cAAc,IAAI,EAAE;AAAA,QACvC,SAAS,CAAC,OAAO,YAAY,IAAI,EAAE;AAAA,QACnC,iBAAiB,OAAO,QAAQ,MAAM,gBAAgB,IAAI,EAAE,IAAI,SAAS;AAAA,QACzE,qBAAqB,OAAO,SAAS,MAAM,qBAAqB,GAAG,GAAG;AAAA,QACtE,QAAQ,CAAC,MAAM,QAAQ,UAAU,oBAAoB,MAAM,QAAQ,KAAK;AAAA,QACxE,OAAO,CAAC,IAAI,SAAS,kBAAkB,IAAI,IAAI,IAAI;AAAA,MACrD;AAAA,MACA,KAAK;AAAA,IACP;AAGA,WAAO,EAAE,QAAQ,qBAAqB,MAAM,GAAG,MAAM,OAAO,WAAW,aAAa,IAAI,EAAE;AAAA,EAC5F;AAEA,MAAI,KAAK,KAAK;AACZ,WAAO,YAAY;AAAA,EACrB;AAEA,MAAI,MAAM;AAGR,UAAM,aAAa,0BAA0B,KAAK,IAAI;AACtD,WAAO,mBAAmB,MAAM;AAAA,MAC9B,aAAa,QAAQ,KAAK,OAAO;AAAA,MACjC,QAAQ,QAAQ,KAAK,MAAM;AAAA,MAC3B;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,OAAO;AAAA,IACX,IAAI;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,IACZ;AAAA,EACF;AACF;AAEA,eAAe,cAAyD;AACtE,QAAM,OAAO,SAAS,mBAAmB,CAAC;AAG1C,QAAM,EAAE,6BAA6B,IAAI,MAAM,OAAO,6BAA+B;AAKrF,QAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,6BAAgC;AAC1E,QAAM,EAAE,eAAe,IAAI,MAAM,OAAO,2BAAyB;AACjE,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA,oBAAI,KAAK;AAAA,IACT,MAAM,6BAA6B;AAAA,IACnC,MAAM,iBAAiB;AAAA,IACvB,MAAM,eAAe;AAAA,EACvB;AACA,QAAM,6BAA6B,OAAO,MAAM;AAChD,SAAO,EAAE,QAAQ,OAAO,QAAQ,MAAM,OAAO,KAAK;AACpD;AAOA,eAAe,6BAA6B,QAA2C;AACrF,MAAI,CAAC,qBAAqB,MAAM,EAAE,KAAM;AACxC,MAAI;AACF,UAAM,KAAK,cAAc;AACzB,UAAM,EAAE,SAAS,KAAK,IAAI,2BAA2B,QAAQ,iBAAiB,CAAC;AAC/E,UAAM,EAAE,oBAAoB,IAAI,MAAM,OAAO,sBAA8B;AAC3E,UAAM,oBAAoB,EAAE,KAAK;AAAA,MAC/B,MAAM;AAAA,MACN,IAAI,CAAC,EAAE;AAAA,MACP;AAAA,MACA;AAAA,MACA,gBAAgB,oBAAmB,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,IAC1E,CAAC;AACD,YAAQ,KAAK,UAAK,OAAO,6BAAwB,EAAE,GAAG;AAAA,EACxD,SAAS,GAAG;AACV,YAAQ,KAAK,+CAA2C,EAAY,OAAO,EAAE;AAAA,EAC/E;AACF;AAmBA,eAAsB,kBACpB,MACA,UACA,SACA,OACA,iBAAwC,MACzB;AACf,QAAM,MAAM,CAAC,MAAmC,IAAI,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE,IAAI;AACnF,MAAI,QAAQ;AACZ,MAAI,UAAU;AACd,MAAI,SAAS;AACb,MAAI,WAAW;AACf,MAAI,eAAe;AACnB,MAAI,eAAe;AACnB,aAAW,QAAQ,UAAU;AAG3B,QAAI;AACF,YAAM,gBAAgB,IAAI,YAAY,MAAM,SAAS,eAAe,KAAK,CAAC;AAC1E,YAAM,YAAY,IAAI,YAAY,MAAM,SAAS,WAAW,KAAK,CAAC;AAClE,UAAI,kBAAkB,KAAK,qBAAqB,cAAc,KAAK,eAAe;AAChF;AACA;AAAA,MACF;AACA,YAAM,SAAS,MAAM,mBAAmB,MAAM,KAAK,IAAI,EAAE,eAAe,UAAU,CAAC;AACnF;AACA,UAAI,gBAAgB;AAClB,YAAI;AAGF,cAAI,MAAM,eAAe,KAAK,IAAI,QAAQ,MAAM,YAAY,CAAC,EAAG;AAAA,cAC3D;AAAA,QACP,SAAS,GAAG;AACV;AACA,kBAAQ,KAAK,4BAAuB,KAAK,IAAI,KAAM,EAAY,OAAO,EAAE;AAAA,QAC1E;AAAA,MACF;AAAA,IACF,SAAS,GAAG;AACV;AACA,cAAQ,KAAK,qCAAgC,KAAK,IAAI,KAAM,EAAY,OAAO,EAAE;AAAA,IACnF;AAAA,EACF;AAQA,QAAM,aAAa,iBACf,aAAa,QAAQ,kBAAkB,YAAY,kBAAkB,YAAY,KACjF;AACJ,UAAQ,IAAI,wBAAwB,KAAK,YAAY,OAAO,WAAW,MAAM,GAAG,UAAU,EAAE;AAC9F;AAEA,eAAsB,gBACpB,MACA,OACA,iBAAwC,MAKxC,cAGA,YACuE;AACvE,QAAM,YAAY;AAAA,IAChB,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,IACvC,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;AAAA,EACrC;AACA,QAAM,WAAW,MAAM,aAAa,IAAI;AAIxC,QAAM,UAAU,MAAM,eAAe,IAAI;AAIzC,QAAM,kBAAkB,MAAM,UAAU,SAAS,OAAO,cAAc;AAEtE,QAAM,MAAM,eAAe,UAAU,SAAS,KAAK;AAMnD,QAAM,eAAe,aAAa,MAAM;AAIxC,QAAM,wBAAwB,CAAC,MAC7B,gBAAgB,QAAQ,EAAE,iBAAiB,EAAE,WAAW;AAE1D,MAAI,IAAI,WAAW,GAAG;AACpB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,QAAQ,EAAE,iBAAiB,GAAG,iBAAiB,EAAE;AAAA,IACnD;AAAA,EACF;AAEA,QAAM,QAAkB,CAAC;AACzB,MAAI,kBAAkB;AACtB,MAAI,sBAAsB;AAC1B,MAAI,0BAA0B;AAC9B,MAAI,oBAAoB;AACxB,MAAI,UAAU;AACd,aAAW,QAAQ,KAAK;AAItB,UAAM,SAAS,gBAAgB,KAAK,OAAO;AAC3C,UAAM,WAAW,QAAQ;AAAA,MACvB,CAAC,MAAM,EAAE,WAAW,KAAK,KAAK,MAAM,EAAE,eAAe,KAAK,cAAc,EAAE,WAAW;AAAA,IACvF;AAQA,QAAI,UAAU;AACZ,UAAI,SAAS,YAAY;AACvB;AACA,cAAM,KAAK,mCAA8B,MAAM,MAAM,KAAK,KAAK,IAAI,IAAI,KAAK,UAAU,EAAE;AACxF;AAAA,MACF;AAOA,YAAM,mBAAmB,QAAQ;AAAA,QAC/B,CAAC,MACC,EAAE,WAAW,KAAK,KAAK,MACvB,EAAE,OAAO,SAAS,MAClB,EAAE,WAAW,QACb,EAAE,cACF,WAAW,EAAE,UAAU,KAAK,WAAW,KAAK,UAAU;AAAA,MAC1D;AACA,UAAI,kBAAkB;AACpB;AACA,cAAM;AAAA,UACJ,gFAAsE,KAAK,KAAK,IAAI,IAAI,KAAK,UAAU;AAAA,QACzG;AACA;AAAA,MACF;AACA,UAAI;AACF,cAAM,SAAS,MAAM,mBAAmB,MAAM,KAAK,MAAM,KAAK,YAAY;AAAA,UACxE;AAAA,UACA,eAAe,SAAS;AAAA,UACxB,aAAa;AAAA,UACb,GAAG;AAAA,QACL,CAAC;AACD,iBAAS,aAAa,OAAO,WAAW;AACxC,cAAM;AAAA,UACJ;AAAA,YACE,OAAO,WAAW,YAAY,SAAS;AAAA,YACvC,OAAO;AAAA,YACP,OAAO;AAAA,YACP;AAAA,UACF;AAAA,QACF;AACA,YAAI,sBAAsB,KAAK,IAAI,EAAG;AACtC,YAAI,OAAO,aAAa,SAAS,EAAG;AACpC,YAAI,OAAO,oBAAqB;AAChC,YAAI,OAAO,sBAAuB;AAAA,MACpC,SAAS,GAAG;AACV,cAAM,KAAK,kBAAa,KAAK,KAAK,IAAI,IAAI,KAAK,UAAU,WAAO,EAAY,OAAO,EAAE;AAAA,MACvF;AACA;AAAA,IACF;AAYA,UAAM,iBAAiB,QAAQ;AAAA,MAC7B,CAAC,MACC,EAAE,WAAW,KAAK,KAAK,MACvB,EAAE,eAAe,KAAK,cACtB,EAAE,cACF,EAAE,WAAW,QACb,EAAE,WAAW,QACb,EAAE,SAAS;AAAA,IACf;AACA,QAAI,gBAAgB;AAClB;AACA,YAAM;AAAA,QACJ,mBAAc,KAAK,KAAK,IAAI,IAAI,KAAK,UAAU,0BAA0B,eAAe,MAAM;AAAA,MAChG;AACA;AAAA,IACF;AAEA,QAAI;AAIF,YAAM,SAAS,MAAM,mBAAmB,MAAM,KAAK,MAAM,KAAK,YAAY;AAAA,QACxE;AAAA,QACA,GAAG;AAAA,MACL,CAAC;AACD,YAAM,KAAK,UAAU,OAAO,WAAW,UAAU,OAAO,QAAQ,OAAO,aAAa,CAAC;AAIrF,UAAI,OAAO,UAAW,SAAQ,KAAK,OAAO,SAAS;AAGnD,UAAI,sBAAsB,KAAK,IAAI,EAAG;AACtC,UAAI,OAAO,aAAa,SAAS,EAAG;AACpC,UAAI,OAAO,oBAAqB;AAChC,UAAI,OAAO,sBAAuB;AAAA,IACpC,SAAS,GAAG;AACV,YAAM,KAAK,kBAAa,KAAK,KAAK,IAAI,IAAI,KAAK,UAAU,WAAO,EAAY,OAAO,EAAE;AAAA,IACvF;AAAA,EACF;AACA,MAAI,UAAU,GAAG;AACf,UAAM,KAAK,UAAK,OAAO,yCAAyC;AAAA,EAClE;AACA,MAAI,kBAAkB,GAAG;AACvB,UAAM;AAAA,MACJ,UAAK,eAAe,QAAQ,oBAAoB,IAAI,KAAK,GAAG;AAAA,IAC9D;AAAA,EACF;AACA,MAAI,sBAAsB,GAAG;AAC3B,UAAM;AAAA,MACJ,UAAK,mBAAmB,QAAQ,wBAAwB,IAAI,KAAK,GAAG;AAAA,IACtE;AAAA,EACF;AACA,MAAI,0BAA0B,GAAG;AAC/B,UAAM;AAAA,MACJ,UAAK,uBAAuB,QAAQ,4BAA4B,IAAI,KAAK,GAAG;AAAA,IAC9E;AAAA,EACF;AACA,SAAO;AAAA,IACL,QAAQ,MAAM,KAAK,IAAI;AAAA,IACvB,MAAM,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,QAAG,CAAC,IAAI,IAAI;AAAA,IACjD,QAAQ,EAAE,iBAAiB,iBAAiB,kBAAkB;AAAA,EAChE;AACF;AAEA,eAAe,mBACb,MACA,MAC2C;AAC3C,QAAM,OAAO,SAAS,mBAAmB,CAAC;AAC1C,QAAM,WAAW,MAAM,aAAa,IAAI;AACxC,QAAM,OAAO,SAAS,KAAK,CAAC,MAAM,SAAS,EAAE,IAAI,MAAM,IAAI;AAC3D,MAAI,CAAC,MAAM;AACT,UAAM,OAAO,OAAO,IAAI,MAAM,iCAAiC,IAAI,GAAG,GAAG,EAAE,UAAU,EAAE,CAAC;AAAA,EAC1F;AAKA,QAAM,eAAe,KAAK,cACtB,OACA,OAAO,MAAM,OAAO,6BAAgC,GAAG,iBAAiB;AAC5E,QAAM,aAAa,KAAK,cACpB,OACA,OAAO,MAAM,OAAO,2BAAyB,GAAG,eAAe;AACnE,QAAM,SAAS,MAAM,mBAAmB,KAAK,cAAc,OAAO,MAAM,MAAM,KAAK,YAAY;AAAA,IAC7F,aAAa,KAAK;AAAA;AAAA;AAAA,IAGlB,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC;AAAA,IACtC,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,IACvC,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;AAAA,EACrC,CAAC;AACD,MAAI,KAAK,aAAa;AACpB,WAAO,EAAE,QAAQ,sBAAsB,OAAO,QAAQ,IAAI,MAAM,EAAE;AAAA,EACpE;AACA,SAAO;AAAA,IACL,QAAQ;AAAA,MACN,OAAO,WAAW;AAAA,MAClB,OAAO;AAAA,MACP,OAAO;AAAA,MACP;AAAA,IACF;AAAA,IACA,MAAM;AAAA,EACR;AACF;","names":[]}
@@ -1,11 +1,11 @@
1
- import {
2
- TURSO_IS_AUTHORITATIVE
3
- } from "./chunk-RUPPWAD6.js";
4
1
  import {
5
2
  mirrorReportInsert,
6
3
  mirrorReportPatch,
7
4
  storeRenderedHtml
8
- } from "./chunk-GQDDWYU2.js";
5
+ } from "./chunk-QBAAZY7W.js";
6
+ import {
7
+ TURSO_IS_AUTHORITATIVE
8
+ } from "./chunk-SVAAONYB.js";
9
9
  import "./chunk-P6AORG32.js";
10
10
  import "./chunk-F3JWOPTG.js";
11
11
  import "./chunk-VAD43MOR.js";
@@ -52,4 +52,4 @@ async function makeReportMirror(open = () => openDb(readDbConfig()), strict = TU
52
52
  export {
53
53
  makeReportMirror
54
54
  };
55
- //# sourceMappingURL=report-mirror-AAK3MXHY.js.map
55
+ //# sourceMappingURL=report-mirror-3ENU6OOK.js.map
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  buildReportDataForSite,
3
3
  scoresFromRow
4
- } from "./chunk-ULHPALI5.js";
4
+ } from "./chunk-NDCOOI3V.js";
5
5
  import {
6
6
  operatorEmail
7
7
  } from "./chunk-5ML7FMBD.js";
8
- import "./chunk-J3WEZGIN.js";
8
+ import "./chunk-ZRI5VRPG.js";
9
9
  import "./chunk-YFAXOD7Z.js";
10
10
  import {
11
11
  ELIGIBLE_STATUSES
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  isProbablyEmail,
15
15
  parseAddresses
16
- } from "./chunk-DGZWFI2G.js";
16
+ } from "./chunk-VBEKL445.js";
17
17
  import "./chunk-F3JWOPTG.js";
18
18
  import "./chunk-VAD43MOR.js";
19
19
  import {
@@ -26,12 +26,12 @@ import {
26
26
  import {
27
27
  prepareHeaderImage,
28
28
  renderReportEmail
29
- } from "./chunk-XP5WM2LV.js";
29
+ } from "./chunk-SKI23ZZH.js";
30
30
  import "./chunk-GECESMVS.js";
31
+ import "./chunk-KJGKZLCT.js";
31
32
  import {
32
33
  applyReportTypeHeadline
33
34
  } from "./chunk-5UINJNAJ.js";
34
- import "./chunk-KJGKZLCT.js";
35
35
  import "./chunk-BGTYPVLT.js";
36
36
  import {
37
37
  listWebsites,
@@ -172,4 +172,4 @@ async function runSelftestCommand(kind, site, opts) {
172
172
  export {
173
173
  runSelftestCommand
174
174
  };
175
- //# sourceMappingURL=selftest-DIZ5JD2X.js.map
175
+ //# sourceMappingURL=selftest-CUX2FIQD.js.map
@@ -1,11 +1,11 @@
1
- import {
2
- TURSO_IS_AUTHORITATIVE
3
- } from "./chunk-RUPPWAD6.js";
4
1
  import {
5
2
  mirrorHealthFields,
6
3
  mirrorSiteFields,
7
4
  mirrorSiteInsert
8
- } from "./chunk-GQDDWYU2.js";
5
+ } from "./chunk-QBAAZY7W.js";
6
+ import {
7
+ TURSO_IS_AUTHORITATIVE
8
+ } from "./chunk-SVAAONYB.js";
9
9
  import "./chunk-P6AORG32.js";
10
10
  import "./chunk-F3JWOPTG.js";
11
11
  import "./chunk-VAD43MOR.js";
@@ -59,4 +59,4 @@ async function makeSiteMirror(open = () => openDb(readDbConfig()), strict = TURS
59
59
  export {
60
60
  makeSiteMirror
61
61
  };
62
- //# sourceMappingURL=site-mirror-Q763PLGG.js.map
62
+ //# sourceMappingURL=site-mirror-RQTEAID5.js.map
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  checkFleetParity,
3
3
  formatParityResult
4
- } from "./chunk-7FFUBF4I.js";
5
- import "./chunk-GQDDWYU2.js";
4
+ } from "./chunk-AX22DH6S.js";
5
+ import "./chunk-QBAAZY7W.js";
6
6
  import {
7
7
  formatReapSummary,
8
8
  importFleetState
@@ -43,4 +43,4 @@ export {
43
43
  formatSyncResult,
44
44
  syncFleetState
45
45
  };
46
- //# sourceMappingURL=sync-GBFV45WX.js.map
46
+ //# sourceMappingURL=sync-ESP5ZQUU.js.map
@@ -15,12 +15,12 @@ import {
15
15
  isConfigName,
16
16
  mergeGitignore,
17
17
  syncConfigs
18
- } from "./chunk-7UAYMAKR.js";
18
+ } from "./chunk-3VQWSE3F.js";
19
+ import "./chunk-3G25KIWW.js";
19
20
  import {
20
21
  ALL_TEMPLATES,
21
22
  templatesByName
22
23
  } from "./chunk-73UFRZKY.js";
23
- import "./chunk-3G25KIWW.js";
24
24
  import "./chunk-Z3JXLULD.js";
25
25
  import "./chunk-RARREDQE.js";
26
26
  import "./chunk-XTK5VIQB.js";
@@ -113,4 +113,4 @@ async function runSyncConfigsCommand(site, opts) {
113
113
  export {
114
114
  runSyncConfigsCommand
115
115
  };
116
- //# sourceMappingURL=sync-configs-34JIA6KZ.js.map
116
+ //# sourceMappingURL=sync-configs-LVBITPLU.js.map
@@ -0,0 +1,147 @@
1
+ // src/db/usage.ts
2
+ var CUMULATIVE_METRICS = ["rows_read", "rows_written", "bytes_synced"];
3
+ var LEVEL_METRICS = ["storage_bytes"];
4
+ var CAPACITY_METRICS = ["databases", "locations", "groups"];
5
+ var QUOTA_KEY = {
6
+ rows_read: "rowsRead",
7
+ rows_written: "rowsWritten",
8
+ bytes_synced: "bytesSynced",
9
+ storage_bytes: "storage",
10
+ databases: "databases",
11
+ locations: "locations",
12
+ groups: "groups"
13
+ };
14
+ var ELAPSED_FLOOR_MS = 6 * 60 * 60 * 1e3;
15
+ var DEFAULT_THRESHOLD_PCT = 50;
16
+ var pct = (used, quota) => quota > 0 ? used / quota * 100 : 0;
17
+ var fmt = (n) => `${n.toFixed(2)}%`;
18
+ function assessUsage(input) {
19
+ const threshold = input.thresholdPct ?? DEFAULT_THRESHOLD_PCT;
20
+ const cycleMs = Math.max(1, input.cycleEnd.getTime() - input.cycleStart.getTime());
21
+ const rawElapsedMs = input.now.getTime() - input.cycleStart.getTime();
22
+ const elapsedMs = Math.min(cycleMs, Math.max(ELAPSED_FLOOR_MS, rawElapsedMs));
23
+ const elapsedFraction = elapsedMs / cycleMs;
24
+ const fields = [];
25
+ const rows = [];
26
+ let measured = 0;
27
+ const atCapacity = [];
28
+ const candidates = [];
29
+ const consider = (name, value) => candidates.push({ name, value });
30
+ const all = [
31
+ ...CUMULATIVE_METRICS.map((m) => ({ metric: m, kind: "cumulative" })),
32
+ ...LEVEL_METRICS.map((m) => ({ metric: m, kind: "level" })),
33
+ ...CAPACITY_METRICS.map((m) => ({ metric: m, kind: "capacity" }))
34
+ ];
35
+ for (const { metric, kind } of all) {
36
+ const quota = input.quotas[QUOTA_KEY[metric]];
37
+ const used = input.usage[metric] ?? 0;
38
+ if (typeof quota !== "number" || !Number.isFinite(quota) || quota <= 0) {
39
+ fields.push(`${metric}=unmeasured`);
40
+ rows.push(` ${metric.padEnd(14)} ${String(used).padStart(15)} / (no published quota)`);
41
+ continue;
42
+ }
43
+ measured += 1;
44
+ const now = pct(used, quota);
45
+ fields.push(`${metric}=${fmt(now)}`);
46
+ if (kind === "cumulative") {
47
+ const projected = now / elapsedFraction;
48
+ fields.push(`${metric}_proj=${fmt(projected)}`);
49
+ consider(`${metric}_proj`, projected);
50
+ rows.push(
51
+ ` ${metric.padEnd(14)} ${String(used).padStart(15)} / ${String(quota).padEnd(13)} ${fmt(now).padStart(8)} \u2192 ${fmt(projected)} by cycle end`
52
+ );
53
+ } else if (kind === "level") {
54
+ consider(metric, now);
55
+ rows.push(
56
+ ` ${metric.padEnd(14)} ${String(used).padStart(15)} / ${String(quota).padEnd(13)} ${fmt(now).padStart(8)}`
57
+ );
58
+ } else {
59
+ if (used >= quota) atCapacity.push(metric);
60
+ rows.push(
61
+ ` ${metric.padEnd(14)} ${String(used).padStart(15)} / ${String(quota).padEnd(13)} ${fmt(now).padStart(8)} (ceiling)`
62
+ );
63
+ }
64
+ }
65
+ const blocked = input.blockedReads ? "reads" : input.blockedWrites ? "writes" : "none";
66
+ const worst = candidates.reduce(
67
+ (acc, c) => acc === null || c.value > acc.value ? c : acc,
68
+ null
69
+ );
70
+ const worstLabel = worst ? `${worst.name}:${fmt(worst.value)}` : "none";
71
+ let verdict;
72
+ let code;
73
+ if (measured === 0) {
74
+ verdict = "unmeasurable";
75
+ code = 1;
76
+ } else if (blocked !== "none") {
77
+ verdict = "blocked";
78
+ code = 1;
79
+ } else if (worst && worst.value >= threshold) {
80
+ verdict = "over-threshold";
81
+ code = 1;
82
+ } else {
83
+ verdict = "ok";
84
+ code = 0;
85
+ }
86
+ const marker = [
87
+ "FLEET_DB_USAGE",
88
+ `plan=${input.plan}`,
89
+ `elapsed=${fmt(elapsedFraction * 100)}`,
90
+ ...fields,
91
+ `measured=${measured}`,
92
+ `worst=${worstLabel}`,
93
+ `threshold=${fmt(threshold)}`,
94
+ `blocked=${blocked}`,
95
+ `at_capacity=${atCapacity.length ? atCapacity.join(",") : "none"}`,
96
+ `verdict=${verdict}`
97
+ ].join(" ");
98
+ return { lines: rows, marker, code };
99
+ }
100
+ var API = "https://api.turso.tech/v1";
101
+ async function collectUsage(deps) {
102
+ const doFetch = deps.fetchImpl ?? globalThis.fetch;
103
+ const get = async (path) => {
104
+ const url = `${API}${path}`;
105
+ const res = await doFetch(url, {
106
+ headers: { Authorization: `Bearer ${deps.token}` }
107
+ });
108
+ if (!res.ok) throw new Error(`Turso platform API ${res.status} for ${path}`);
109
+ return res.json();
110
+ };
111
+ let org = deps.org;
112
+ if (!org) {
113
+ const orgs = await get("/organizations");
114
+ org = orgs?.[0]?.slug;
115
+ if (!org) throw new Error("Turso platform API returned no organizations for this token");
116
+ }
117
+ const [usageRes, plansRes, subRes, dbsRes] = await Promise.all([
118
+ get(`/organizations/${org}/usage`),
119
+ get(`/organizations/${org}/plans`),
120
+ get(`/organizations/${org}/subscription`),
121
+ get(`/organizations/${org}/databases`)
122
+ ]);
123
+ const sub = subRes.subscription ?? {};
124
+ const plan = sub.plan ?? "unknown";
125
+ const quotas = plansRes.plans?.find((p) => p.name === plan)?.quotas ?? {};
126
+ const dbs = dbsRes.databases ?? [];
127
+ return {
128
+ plan,
129
+ quotas,
130
+ usage: usageRes.total ?? {},
131
+ cycleStart: new Date(sub.current_billing_period_start ?? deps.now.toISOString()),
132
+ cycleEnd: new Date(sub.current_billing_period_end ?? deps.now.toISOString()),
133
+ now: deps.now,
134
+ blockedReads: dbs.some((d) => d.block_reads === true),
135
+ blockedWrites: dbs.some((d) => d.block_writes === true)
136
+ };
137
+ }
138
+ export {
139
+ CAPACITY_METRICS,
140
+ CUMULATIVE_METRICS,
141
+ DEFAULT_THRESHOLD_PCT,
142
+ LEVEL_METRICS,
143
+ QUOTA_KEY,
144
+ assessUsage,
145
+ collectUsage
146
+ };
147
+ //# sourceMappingURL=usage-UHKU76VK.js.map