@reddoorla/maintenance 0.56.0 → 0.58.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 (71) hide show
  1. package/dist/airtable-E5F5FLGD.js +9 -0
  2. package/dist/{announce-JEVCDDKL.js → announce-JO67XZLZ.js} +3 -3
  3. package/dist/{bump-deps-O3SOLQYY.js → bump-deps-6D4VCAFQ.js} +2 -2
  4. package/dist/{chunk-JQG222CI.js → chunk-5E3OW6YN.js} +2 -2
  5. package/dist/{chunk-VXSX2FHO.js → chunk-HPNGCRTK.js} +13 -3
  6. package/dist/chunk-HPNGCRTK.js.map +1 -0
  7. package/dist/{chunk-J3OPWFZE.js → chunk-IDRBAF2H.js} +2 -2
  8. package/dist/{chunk-5WL3ET24.js → chunk-IJW4DS2S.js} +3 -3
  9. package/dist/{chunk-DIVB6ZF7.js → chunk-J4ECCFRT.js} +7 -3
  10. package/dist/chunk-J4ECCFRT.js.map +1 -0
  11. package/dist/{chunk-PF6BNYGE.js → chunk-MKSZBODM.js} +2 -2
  12. package/dist/{chunk-CLVTVDOU.js → chunk-PKIYAWA4.js} +8 -1
  13. package/dist/chunk-PKIYAWA4.js.map +1 -0
  14. package/dist/chunk-PZQ3WED2.js +14 -0
  15. package/dist/chunk-PZQ3WED2.js.map +1 -0
  16. package/dist/{chunk-IMIR7SC2.js → chunk-RYECSIEA.js} +2 -2
  17. package/dist/{chunk-W3G7XFBZ.js → chunk-S7WKPRID.js} +2 -2
  18. package/dist/cli/bin.js +13 -13
  19. package/dist/cli/commands/audit.js +5 -5
  20. package/dist/{convert-to-pnpm-UR64X3OX.js → convert-to-pnpm-JYUUUDDP.js} +2 -2
  21. package/dist/{digest-YTO73BEO.js → digest-RUQ42QXL.js} +10 -5
  22. package/dist/digest-RUQ42QXL.js.map +1 -0
  23. package/dist/{github-signals-YD64R2AF.js → github-signals-JXHRHDAG.js} +8 -14
  24. package/dist/github-signals-JXHRHDAG.js.map +1 -0
  25. package/dist/index.d.ts +12 -0
  26. package/dist/index.js +9 -5
  27. package/dist/index.js.map +1 -1
  28. package/dist/{init-JITQXFVL.js → init-6LJU72F2.js} +2 -2
  29. package/dist/{launch-HXHEL6ID.js → launch-6ZB3QA2I.js} +7 -7
  30. package/dist/{lighthouse-airtable-4FKYKFH6.js → lighthouse-airtable-FSGOUVMQ.js} +3 -3
  31. package/dist/{onboard-TCQOP2W4.js → onboard-UM6UKX2V.js} +2 -2
  32. package/dist/{orchestrate-RMSBHLQK.js → orchestrate-GEHLKE64.js} +9 -5
  33. package/dist/{renovate-dispatch-ZGLXNPEK.js → renovate-dispatch-L5HWCNRY.js} +50 -8
  34. package/dist/renovate-dispatch-L5HWCNRY.js.map +1 -0
  35. package/dist/{report-SD3CNBOQ.js → report-3YVSMRFE.js} +5 -5
  36. package/dist/{self-updating-CIUMOMYI.js → self-updating-J7U7WIP3.js} +4 -4
  37. package/dist/{svelte-codemods-BYRJAU7I.js → svelte-codemods-BW4M4CDF.js} +2 -2
  38. package/dist/{sync-configs-JPRLAW5J.js → sync-configs-ZYKZCP4C.js} +2 -2
  39. package/dist/{upgrade-ERA45EVR.js → upgrade-UAXREMED.js} +2 -2
  40. package/dist/{websites-X5DLC2PC.js → websites-EEZVZXQT.js} +4 -2
  41. package/dist/{write-audits-to-airtable-CE4POBAF.js → write-audits-to-airtable-LUFY424L.js} +4 -4
  42. package/package.json +1 -1
  43. package/dist/airtable-2BJX4ATJ.js +0 -9
  44. package/dist/chunk-CLVTVDOU.js.map +0 -1
  45. package/dist/chunk-DIVB6ZF7.js.map +0 -1
  46. package/dist/chunk-VXSX2FHO.js.map +0 -1
  47. package/dist/digest-YTO73BEO.js.map +0 -1
  48. package/dist/github-signals-YD64R2AF.js.map +0 -1
  49. package/dist/renovate-dispatch-ZGLXNPEK.js.map +0 -1
  50. /package/dist/{airtable-2BJX4ATJ.js.map → airtable-E5F5FLGD.js.map} +0 -0
  51. /package/dist/{announce-JEVCDDKL.js.map → announce-JO67XZLZ.js.map} +0 -0
  52. /package/dist/{bump-deps-O3SOLQYY.js.map → bump-deps-6D4VCAFQ.js.map} +0 -0
  53. /package/dist/{chunk-JQG222CI.js.map → chunk-5E3OW6YN.js.map} +0 -0
  54. /package/dist/{chunk-J3OPWFZE.js.map → chunk-IDRBAF2H.js.map} +0 -0
  55. /package/dist/{chunk-5WL3ET24.js.map → chunk-IJW4DS2S.js.map} +0 -0
  56. /package/dist/{chunk-PF6BNYGE.js.map → chunk-MKSZBODM.js.map} +0 -0
  57. /package/dist/{chunk-IMIR7SC2.js.map → chunk-RYECSIEA.js.map} +0 -0
  58. /package/dist/{chunk-W3G7XFBZ.js.map → chunk-S7WKPRID.js.map} +0 -0
  59. /package/dist/{convert-to-pnpm-UR64X3OX.js.map → convert-to-pnpm-JYUUUDDP.js.map} +0 -0
  60. /package/dist/{init-JITQXFVL.js.map → init-6LJU72F2.js.map} +0 -0
  61. /package/dist/{launch-HXHEL6ID.js.map → launch-6ZB3QA2I.js.map} +0 -0
  62. /package/dist/{lighthouse-airtable-4FKYKFH6.js.map → lighthouse-airtable-FSGOUVMQ.js.map} +0 -0
  63. /package/dist/{onboard-TCQOP2W4.js.map → onboard-UM6UKX2V.js.map} +0 -0
  64. /package/dist/{orchestrate-RMSBHLQK.js.map → orchestrate-GEHLKE64.js.map} +0 -0
  65. /package/dist/{report-SD3CNBOQ.js.map → report-3YVSMRFE.js.map} +0 -0
  66. /package/dist/{self-updating-CIUMOMYI.js.map → self-updating-J7U7WIP3.js.map} +0 -0
  67. /package/dist/{svelte-codemods-BYRJAU7I.js.map → svelte-codemods-BW4M4CDF.js.map} +0 -0
  68. /package/dist/{sync-configs-JPRLAW5J.js.map → sync-configs-ZYKZCP4C.js.map} +0 -0
  69. /package/dist/{upgrade-ERA45EVR.js.map → upgrade-UAXREMED.js.map} +0 -0
  70. /package/dist/{websites-X5DLC2PC.js.map → websites-EEZVZXQT.js.map} +0 -0
  71. /package/dist/{write-audits-to-airtable-CE4POBAF.js.map → write-audits-to-airtable-LUFY424L.js.map} +0 -0
@@ -0,0 +1,9 @@
1
+ import {
2
+ fromAirtableBase
3
+ } from "./chunk-5E3OW6YN.js";
4
+ import "./chunk-U5SZ4FOP.js";
5
+ import "./chunk-PKIYAWA4.js";
6
+ export {
7
+ fromAirtableBase
8
+ };
9
+ //# sourceMappingURL=airtable-E5F5FLGD.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  fetchGaUsers,
3
3
  fetchSearch
4
- } from "./chunk-W3G7XFBZ.js";
4
+ } from "./chunk-S7WKPRID.js";
5
5
  import {
6
6
  queueDraft
7
7
  } from "./chunk-6LIWDIXU.js";
@@ -20,7 +20,7 @@ import "./chunk-U5SZ4FOP.js";
20
20
  import {
21
21
  listWebsites,
22
22
  siteSlug
23
- } from "./chunk-CLVTVDOU.js";
23
+ } from "./chunk-PKIYAWA4.js";
24
24
  import {
25
25
  openBase,
26
26
  readAirtableConfig
@@ -168,4 +168,4 @@ export {
168
168
  formatAnnounceResult,
169
169
  runAnnounceCommand
170
170
  };
171
- //# sourceMappingURL=announce-JEVCDDKL.js.map
171
+ //# sourceMappingURL=announce-JO67XZLZ.js.map
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-4NAOR44V.js";
6
6
  import {
7
7
  resolveSites
8
- } from "./chunk-J3OPWFZE.js";
8
+ } from "./chunk-IDRBAF2H.js";
9
9
  import {
10
10
  bumpDeps
11
11
  } from "./chunk-D3DQFC32.js";
@@ -54,4 +54,4 @@ async function runBumpDepsCommand(site, opts) {
54
54
  export {
55
55
  runBumpDepsCommand
56
56
  };
57
- //# sourceMappingURL=bump-deps-O3SOLQYY.js.map
57
+ //# sourceMappingURL=bump-deps-6D4VCAFQ.js.map
@@ -5,7 +5,7 @@ import {
5
5
  ACTIVE_STATUSES,
6
6
  listWebsites,
7
7
  siteSlug
8
- } from "./chunk-CLVTVDOU.js";
8
+ } from "./chunk-PKIYAWA4.js";
9
9
 
10
10
  // src/inventory/airtable.ts
11
11
  function fromAirtableBase(base, opts = {}) {
@@ -46,4 +46,4 @@ function fromAirtableBase(base, opts = {}) {
46
46
  export {
47
47
  fromAirtableBase
48
48
  };
49
- //# sourceMappingURL=chunk-JQG222CI.js.map
49
+ //# sourceMappingURL=chunk-5E3OW6YN.js.map
@@ -18,7 +18,7 @@ import {
18
18
  listWebsites,
19
19
  siteSlug,
20
20
  updateLaunched
21
- } from "./chunk-CLVTVDOU.js";
21
+ } from "./chunk-PKIYAWA4.js";
22
22
  import {
23
23
  openBase,
24
24
  readAirtableConfig
@@ -62,6 +62,13 @@ async function prepareHeaderImage(bytes, options = {}) {
62
62
  // src/reports/send/orchestrate.ts
63
63
  var FROM_ADDRESS = "Reddoor Reports <reports@reddoorla.com>";
64
64
  var REPLY_TO = "info@reddoorla.com";
65
+ var GLOBAL_REPORT_CC = "info@reddoorla.com";
66
+ function withGlobalCc(perSiteCc, to) {
67
+ const cc = [...perSiteCc ?? []];
68
+ const present = new Set([...cc, ...to].map((a) => a.toLowerCase()));
69
+ if (!present.has(GLOBAL_REPORT_CC.toLowerCase())) cc.push(GLOBAL_REPORT_CC);
70
+ return cc;
71
+ }
65
72
  var MONTHS = [
66
73
  "January",
67
74
  "February",
@@ -222,7 +229,8 @@ async function sendOne(client, base, site, report) {
222
229
  // Resend returns the original message id rather than sending a duplicate.
223
230
  idempotencyKey: `report:${report.id}`
224
231
  };
225
- if (cc) payload.cc = cc;
232
+ const finalCc = withGlobalCc(cc, to);
233
+ if (finalCc.length > 0) payload.cc = finalCc;
226
234
  let result;
227
235
  try {
228
236
  result = await client.send(payload);
@@ -262,8 +270,10 @@ function isProbablyEmail(s) {
262
270
  }
263
271
 
264
272
  export {
273
+ GLOBAL_REPORT_CC,
274
+ withGlobalCc,
265
275
  sendApprovedReports,
266
276
  parseAddresses,
267
277
  isProbablyEmail
268
278
  };
269
- //# sourceMappingURL=chunk-VXSX2FHO.js.map
279
+ //# sourceMappingURL=chunk-HPNGCRTK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/maintenance-email/header-image.ts","../src/reports/send/orchestrate.ts"],"sourcesContent":["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 { 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 { announcementSiteExtras } from \"../announcement-email/template.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\";\nimport { checklistFor, isChecklistComplete } from \"../checklist.js\";\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\nconst REPLY_TO = \"info@reddoorla.com\";\n\n/** Operations inbox CC'd on every outgoing report so there's always an internal\n * copy on file alongside the client recipients. */\nexport const GLOBAL_REPORT_CC = \"info@reddoorla.com\";\n\n/**\n * Append {@link GLOBAL_REPORT_CC} to a site's per-site CC list. The per-site CC is\n * passed through unchanged (preserving prior behavior); the global address is added\n * only when it isn't already present in the CC or To lists (case-insensitive), so a\n * report is never double-addressed to the ops inbox. Returns the final CC list (may\n * be empty if the global address is already the sole To recipient — the caller omits\n * an empty CC).\n */\nexport function withGlobalCc(perSiteCc: string[] | null, to: string[]): string[] {\n const cc = [...(perSiteCc ?? [])];\n const present = new Set([...cc, ...to].map((a) => a.toLowerCase()));\n if (!present.has(GLOBAL_REPORT_CC.toLowerCase())) cc.push(GLOBAL_REPORT_CC);\n return cc;\n}\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 // Hard checklist gate: a Maintenance/Testing report whose operator checklist isn't\n // fully checked must never go out — even if \"Approved to send\" was ticked directly in\n // Airtable, bypassing the dashboard's approve gate. Throw so the report is skipped and\n // `Sent at` stays null (at-least-once retry preserved), exactly like the other sendOne\n // guards. Launch/Announcement have an empty checklist → vacuously complete, never gated.\n if (!isChecklistComplete(report)) {\n const items = checklistFor(report.reportType);\n const done = items.filter((i) => report.checklist[i.field] === true).length;\n throw new Error(\n `Report ${report.reportId} checklist incomplete — ${done}/${items.length} items checked`,\n );\n }\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 // Announcement-only: re-derive cadence + improvements from the site row so the SENT email\n // keeps its cadence copy + improvement callouts. Without this the send-time re-render drops\n // them entirely (they're not stored on the Reports row). Ignored by the other templates.\n ...(report.reportType === \"Announcement\" ? announcementSiteExtras(site) : {}),\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 // Inline attachments: the per-site header (every template renders it) plus only the bundled\n // images the rendered HTML actually references. The green check (cid:rd-check-png) appears in\n // every report's checklist/score rows EXCEPT Launch; the blurred-tests image\n // (cid:rd-blurred-tests-jpg) appears ONLY in the Maintenance email. Attaching a bundled image\n // the template doesn't reference leaves a dangling inline part that some mail clients surface as\n // a stray downloadable attachment — so gate each on its cid actually appearing in `html`. This\n // self-corrects if a template's image usage changes; there's no per-report-type list to keep in\n // sync. Self-contained inline (no external CDN, no image-blocked broken icons in webmail).\n const attachments: InlineAttachment[] = [\n toInlineAttachment({\n bytes: header.bytes,\n filename: `${cidName}.jpg`,\n contentType: header.contentType,\n cid: cidName,\n }),\n ];\n for (const img of [bundled.check, bundled.blurred]) {\n if (html.includes(`cid:${img.cid}`)) {\n attachments.push(\n toInlineAttachment({\n bytes: img.bytes,\n filename: img.filename,\n contentType: img.contentType,\n cid: img.cid,\n }),\n );\n }\n }\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 // 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 // Always CC the ops inbox (info@reddoorla.com), in addition to any per-site CC.\n const finalCc = withGlobalCc(cc, to);\n if (finalCc.length > 0) payload.cc = finalCc;\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"],"mappings":"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","names":[]}
@@ -30,7 +30,7 @@ async function resolveSites(input) {
30
30
  }
31
31
  if (input.fleet === "airtable") {
32
32
  const { openBase, readAirtableConfig } = await import("./client-EPVO3FEH.js");
33
- const { fromAirtableBase } = await import("./airtable-2BJX4ATJ.js");
33
+ const { fromAirtableBase } = await import("./airtable-E5F5FLGD.js");
34
34
  const base = openBase(readAirtableConfig());
35
35
  const provider = fromAirtableBase(base, input.workdir ? { workdir: input.workdir } : {});
36
36
  return provider();
@@ -61,4 +61,4 @@ async function resolveSites(input) {
61
61
  export {
62
62
  resolveSites
63
63
  };
64
- //# sourceMappingURL=chunk-J3OPWFZE.js.map
64
+ //# sourceMappingURL=chunk-IDRBAF2H.js.map
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  hasRealScores,
3
3
  lighthouseScoresFromResult
4
- } from "./chunk-IMIR7SC2.js";
4
+ } from "./chunk-RYECSIEA.js";
5
5
  import {
6
6
  normalizeSecurityAdvisory,
7
7
  siteSlug,
8
8
  updateAuditFields
9
- } from "./chunk-CLVTVDOU.js";
9
+ } from "./chunk-PKIYAWA4.js";
10
10
 
11
11
  // src/audits/a11y-airtable.ts
12
12
  function hasA11yCounts(result) {
@@ -200,4 +200,4 @@ export {
200
200
  formatFleetWriteSummary,
201
201
  writeFleetAuditsToAirtable
202
202
  };
203
- //# sourceMappingURL=chunk-5WL3ET24.js.map
203
+ //# sourceMappingURL=chunk-IJW4DS2S.js.map
@@ -25,6 +25,9 @@ function mapRollupState(state) {
25
25
  return "none";
26
26
  }
27
27
  }
28
+ function mapMergeable(state) {
29
+ return state === "MERGEABLE" || state === "CONFLICTING" ? state : "UNKNOWN";
30
+ }
28
31
  function makeGitHub(deps) {
29
32
  const spawn = deps.spawn ?? defaultSpawn;
30
33
  const env = { ...process.env, GH_TOKEN: deps.token };
@@ -146,7 +149,7 @@ function makeGitHub(deps) {
146
149
  if (!owner || !name || rest.length > 0) {
147
150
  throw new Error(`openPullRequests: expected "owner/repo", got "${repo}"`);
148
151
  }
149
- const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){pullRequests(states:OPEN,first:100,orderBy:{field:CREATED_AT,direction:DESC}){nodes{number title url headRefName commits(last:1){nodes{commit{statusCheckRollup{state}}}}}}}}";
152
+ const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){pullRequests(states:OPEN,first:100,orderBy:{field:CREATED_AT,direction:DESC}){nodes{number title url headRefName mergeable commits(last:1){nodes{commit{statusCheckRollup{state}}}}}}}}";
150
153
  const out = await gh([
151
154
  "api",
152
155
  "graphql",
@@ -164,7 +167,8 @@ function makeGitHub(deps) {
164
167
  title: n.title,
165
168
  url: n.url,
166
169
  headRef: n.headRefName,
167
- ciState: mapRollupState(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state)
170
+ ciState: mapRollupState(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state),
171
+ mergeable: mapMergeable(n.mergeable)
168
172
  }));
169
173
  },
170
174
  async defaultBranchStatus(repo) {
@@ -214,4 +218,4 @@ function makeGitHub(deps) {
214
218
  export {
215
219
  makeGitHub
216
220
  };
217
- //# sourceMappingURL=chunk-DIVB6ZF7.js.map
221
+ //# sourceMappingURL=chunk-J4ECCFRT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/github/gh.ts"],"sourcesContent":["import { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\n\n/** Aggregate CI state of a PR's head commit, normalized from GitHub's rollup. */\nexport type CiState = \"passing\" | \"failing\" | \"pending\" | \"none\";\n\n/** GitHub's computed mergeability of a PR. `UNKNOWN` is transient — GitHub is\n * still computing it (e.g. right after a push) — so it should be read as \"not\n * known to conflict\", never as conflicting. */\nexport type PrMergeable = \"MERGEABLE\" | \"CONFLICTING\" | \"UNKNOWN\";\n\n/** A minimal open-PR summary with its head-commit CI rollup state + mergeability. */\nexport type PullRequestSummary = {\n number: number;\n title: string;\n url: string;\n headRef: string;\n ciState: CiState;\n mergeable: PrMergeable;\n};\n\n/**\n * Reject a value before it's interpolated into a `gh api` URL path. The\n * `owner/repo` split methods already validate shape; this is the defense-in-depth\n * guard for the `branch` and file-`path` segments. An unexpected value (`..`, a\n * leading `/`, whitespace, or a URL-structural char like `?#%` or a backslash)\n * could otherwise retarget the endpoint (escape the intended path, smuggle a\n * query string, or traverse). Conservative by design — legit branch names like\n * `maint/self-updating-x` and paths like `.github/workflows/ci.yml` pass.\n *\n * Both branch refs (`maint/self-updating-x`) and file paths\n * (`.github/workflows/ci.yml`) legitimately contain `/`, so a single slash is\n * allowed; what's rejected is `..`, a leading `/`, whitespace, or a\n * URL-structural char (`?`, `#`, `%`, backslash) that could escape or retarget\n * the endpoint.\n */\nfunction assertUrlSegment(kind: \"branch\" | \"path\", value: string): void {\n const structural = /[\\s?#%\\\\]|\\.\\./;\n if (value.length === 0 || value.startsWith(\"/\") || structural.test(value)) {\n throw new Error(\n `unsafe ${kind} for gh api path (illegal characters or traversal): ${JSON.stringify(value)}`,\n );\n }\n}\n\n/** Map GitHub's `statusCheckRollup.state` enum to our normalized CiState. */\nfunction mapRollupState(state: string | null | undefined): CiState {\n switch (state) {\n case \"SUCCESS\":\n return \"passing\";\n case \"FAILURE\":\n case \"ERROR\":\n return \"failing\";\n case \"PENDING\":\n case \"EXPECTED\":\n return \"pending\";\n default:\n return \"none\"; // null/undefined = no checks reported\n }\n}\n\n/** Coerce GitHub's `mergeable` enum to our PrMergeable. Anything unexpected\n * (including the literal `UNKNOWN` GitHub returns while still computing) maps to\n * `UNKNOWN` — i.e. \"not known to conflict\". Only an explicit `CONFLICTING` is. */\nfunction mapMergeable(state: string | null | undefined): PrMergeable {\n return state === \"MERGEABLE\" || state === \"CONFLICTING\" ? state : \"UNKNOWN\";\n}\n\nexport type GitHub = {\n openPullRequest: (\n repo: string,\n pr: { head: string; base: string; title: string; body: string },\n ) => Promise<{ url: string }>;\n enableRepoAutoMerge: (repo: string) => Promise<void>;\n protectBranch: (repo: string, branch: string, requiredChecks: string[]) => Promise<void>;\n setRepoSecret: (repo: string, name: string, value: string) => Promise<void>;\n repoExists: (repo: string) => Promise<boolean>;\n defaultBranch: (repo: string) => Promise<string>;\n filesOnBranch: (repo: string, branch: string, paths: string[]) => Promise<string[]>;\n branchProtectionContexts: (repo: string, branch: string) => Promise<string[]>;\n secretExists: (repo: string, name: string) => Promise<boolean>;\n autoMergeEnabled: (repo: string) => Promise<boolean>;\n findOpenSelfUpdatingPR: (repo: string) => Promise<string | null>;\n /** All open PRs on a repo with each head commit's normalized CI rollup state. */\n openPullRequests: (repo: string) => Promise<PullRequestSummary[]>;\n /** The default branch's latest-commit date + normalized CI rollup, one query. */\n defaultBranchStatus: (repo: string) => Promise<{ ciState: CiState; lastCommitAt: string | null }>;\n /** Fire a `workflow_dispatch` for `<workflow>` (a filename like `renovate.yml`)\n * on `ref`. Requires the token's `actions:write` scope; a 404 (no such\n * workflow) or 403 (missing scope) surfaces as a thrown error. */\n dispatchWorkflow: (repo: string, workflow: string, ref: string) => Promise<void>;\n};\n\nexport function makeGitHub(deps: { token: string; spawn?: SpawnFn }): GitHub {\n const spawn = deps.spawn ?? defaultSpawn;\n const env = { ...process.env, GH_TOKEN: deps.token };\n\n async function gh(args: string[]): Promise<string> {\n const r = await spawn(\"gh\", args, { env, timeoutMs: 60_000 });\n if (r.code !== 0) throw new Error(`gh ${args[0]} failed (code ${r.code}): ${r.stderr.trim()}`);\n return r.stdout;\n }\n\n return {\n async openPullRequest(repo, pr) {\n const out = await gh([\n \"pr\",\n \"create\",\n \"--repo\",\n repo,\n \"--head\",\n pr.head,\n \"--base\",\n pr.base,\n \"--title\",\n pr.title,\n \"--body\",\n pr.body,\n ]);\n return { url: out.trim() };\n },\n async enableRepoAutoMerge(repo) {\n await gh([\"api\", \"-X\", \"PATCH\", `repos/${repo}`, \"-F\", \"allow_auto_merge=true\"]);\n },\n async protectBranch(repo, branch, requiredChecks) {\n assertUrlSegment(\"branch\", branch);\n const args = [\n \"api\",\n \"-X\",\n \"PUT\",\n `repos/${repo}/branches/${branch}/protection`,\n \"-H\",\n \"Accept: application/vnd.github+json\",\n \"-F\",\n \"required_status_checks[strict]=true\",\n ...requiredChecks.flatMap((c) => [\"-f\", `required_status_checks[contexts][]=${c}`]),\n \"-F\",\n \"enforce_admins=true\",\n \"-F\",\n \"required_pull_request_reviews=null\",\n \"-F\",\n \"restrictions=null\",\n ];\n await gh(args);\n },\n async setRepoSecret(repo, name, value) {\n await gh([\"secret\", \"set\", name, \"--repo\", repo, \"--body\", value]);\n },\n async repoExists(repo) {\n const r = await spawn(\"gh\", [\"api\", `repos/${repo}`], { env, timeoutMs: 60_000 });\n return r.code === 0;\n },\n async defaultBranch(repo) {\n const out = await gh([\"api\", `repos/${repo}`, \"--jq\", \".default_branch\"]);\n return out.trim();\n },\n // filesOnBranch and branchProtectionContexts call `spawn` directly (not the\n // throwing `gh()` helper) because a 404 is an expected, meaningful answer —\n // \"file/protection absent\" — not an error. The remaining readers use `gh()`\n // since a non-200 there is a genuine failure (e.g. missing token scope).\n async filesOnBranch(repo, branch, paths) {\n assertUrlSegment(\"branch\", branch);\n const present: string[] = [];\n for (const p of paths) {\n assertUrlSegment(\"path\", p);\n const r = await spawn(\"gh\", [`api`, `repos/${repo}/contents/${p}?ref=${branch}`], {\n env,\n timeoutMs: 60_000,\n });\n if (r.code === 0) present.push(p);\n }\n return present;\n },\n async branchProtectionContexts(repo, branch) {\n assertUrlSegment(\"branch\", branch);\n const r = await spawn(\n \"gh\",\n [\n \"api\",\n `repos/${repo}/branches/${branch}/protection`,\n \"--jq\",\n \".required_status_checks.contexts[]?\",\n ],\n { env, timeoutMs: 60_000 },\n );\n if (r.code !== 0) return []; // 404 = no protection configured\n return r.stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n },\n async secretExists(repo, name) {\n // per_page=100: the REST default of 30 would false-negative on a repo with >30 secrets,\n // wrongly reporting an existing secret absent (→ a needless overwrite).\n const out = await gh([\n \"api\",\n `repos/${repo}/actions/secrets?per_page=100`,\n \"--jq\",\n \".secrets[].name\",\n ]);\n return out\n .split(\"\\n\")\n .map((l) => l.trim())\n .includes(name);\n },\n async autoMergeEnabled(repo) {\n const out = await gh([\"api\", `repos/${repo}`, \"--jq\", \".allow_auto_merge\"]);\n return out.trim() === \"true\";\n },\n async findOpenSelfUpdatingPR(repo) {\n // per_page=100: with the REST default of 30, a repo with >30 open PRs (plausible under\n // Renovate) could page past the existing self-updating PR and open a duplicate.\n const out = await gh([\n \"api\",\n `repos/${repo}/pulls?state=open&per_page=100`,\n \"--jq\",\n '.[] | select(.head.ref | startswith(\"maint/self-updating-\")) | .html_url',\n ]);\n const first = out\n .split(\"\\n\")\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n return first ?? null;\n },\n async openPullRequests(repo) {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`openPullRequests: expected \"owner/repo\", got \"${repo}\"`);\n }\n const query =\n \"query($owner:String!,$name:String!){repository(owner:$owner,name:$name){\" +\n \"pullRequests(states:OPEN,first:100,orderBy:{field:CREATED_AT,direction:DESC}){nodes{number title url headRefName mergeable \" +\n \"commits(last:1){nodes{commit{statusCheckRollup{state}}}}}}}}\";\n const out = await gh([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `name=${name}`,\n ]);\n const parsed = JSON.parse(out) as {\n data?: {\n repository?: {\n pullRequests?: {\n nodes?: Array<{\n number: number;\n title: string;\n url: string;\n headRefName: string;\n mergeable?: string;\n commits?: {\n nodes?: Array<{ commit?: { statusCheckRollup?: { state?: string } } }>;\n };\n }>;\n };\n };\n };\n };\n const nodes = parsed.data?.repository?.pullRequests?.nodes ?? [];\n return nodes.map((n) => ({\n number: n.number,\n title: n.title,\n url: n.url,\n headRef: n.headRefName,\n ciState: mapRollupState(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state),\n mergeable: mapMergeable(n.mergeable),\n }));\n },\n async defaultBranchStatus(repo) {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`defaultBranchStatus: expected \"owner/repo\", got \"${repo}\"`);\n }\n const query =\n \"query($owner:String!,$name:String!){repository(owner:$owner,name:$name){\" +\n \"defaultBranchRef{target{... on Commit{committedDate statusCheckRollup{state}}}}}}\";\n const out = await gh([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `name=${name}`,\n ]);\n const parsed = JSON.parse(out) as {\n data?: {\n repository?: {\n defaultBranchRef?: {\n target?: { committedDate?: string; statusCheckRollup?: { state?: string } | null };\n } | null;\n };\n };\n };\n const target = parsed.data?.repository?.defaultBranchRef?.target;\n return {\n ciState: mapRollupState(target?.statusCheckRollup?.state),\n lastCommitAt: target?.committedDate ?? null,\n };\n },\n async dispatchWorkflow(repo, workflow, ref) {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`dispatchWorkflow: expected \"owner/repo\", got \"${repo}\"`);\n }\n // Every segment interpolates into the API path, so guard them all like the\n // other write methods do (defense in depth). `owner`/`name` are the most\n // operator-controlled (typed into Airtable's \"Git repo\"); `workflow` is a\n // constant today; `ref` is repo-sourced. A junk value like `repo?x=1` would\n // otherwise smuggle a query string past the bare two-part shape check.\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n assertUrlSegment(\"path\", workflow);\n assertUrlSegment(\"branch\", ref);\n await gh([\n \"api\",\n \"-X\",\n \"POST\",\n `repos/${owner}/${name}/actions/workflows/${workflow}/dispatches`,\n \"-f\",\n `ref=${ref}`,\n ]);\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  makeGitHub
3
- } from "./chunk-DIVB6ZF7.js";
3
+ } from "./chunk-J4ECCFRT.js";
4
4
  import {
5
5
  templatesByName
6
6
  } from "./chunk-CASCWQQX.js";
@@ -159,4 +159,4 @@ async function selfUpdating(site, deps = {}) {
159
159
  export {
160
160
  selfUpdating
161
161
  };
162
- //# sourceMappingURL=chunk-PF6BNYGE.js.map
162
+ //# sourceMappingURL=chunk-MKSZBODM.js.map
@@ -76,6 +76,7 @@ function mapRow(rec) {
76
76
  securityVulnsHigh: f["Security Vulns High"] ?? null,
77
77
  securityVulnsModerate: f["Security Vulns Moderate"] ?? null,
78
78
  securityVulnsLow: f["Security Vulns Low"] ?? null,
79
+ securityAutoFixAttempts: f["Security Auto-Fix Attempts"] ?? null,
79
80
  lastSecurityAuditAt: f["Last security audit at"] ?? null,
80
81
  securityAdvisories: parseSecurityAdvisories(f["Security advisories"]),
81
82
  certDaysRemaining: f["Cert days remaining"] ?? null,
@@ -221,6 +222,11 @@ async function updateDepsCounts(base, recordId, counts) {
221
222
  async function updateSecurityCounts(base, recordId, counts) {
222
223
  await base(WEBSITES_TABLE).update([{ id: recordId, fields: securityFields(counts) }]);
223
224
  }
225
+ async function updateAutoFixAttempts(base, recordId, attempts) {
226
+ await base(WEBSITES_TABLE).update([
227
+ { id: recordId, fields: { "Security Auto-Fix Attempts": attempts } }
228
+ ]);
229
+ }
224
230
  async function updateAuditFields(base, recordId, audits) {
225
231
  const fields = {};
226
232
  if (audits.scores) Object.assign(fields, scoreFields(audits.scores));
@@ -267,8 +273,9 @@ export {
267
273
  updateA11yCounts,
268
274
  updateDepsCounts,
269
275
  updateSecurityCounts,
276
+ updateAutoFixAttempts,
270
277
  updateAuditFields,
271
278
  updateGitHubSignals,
272
279
  updateLaunched
273
280
  };
274
- //# sourceMappingURL=chunk-CLVTVDOU.js.map
281
+ //# sourceMappingURL=chunk-PKIYAWA4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/airtable/websites.ts"],"sourcesContent":["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\n/**\n * Per-site notification routing. When present on a `maintenance` site, the form\n * notification is addressed by the value of a submission field (`field`, read from\n * `extraFields`) — e.g. route a contact form's `interest` to a different recipient\n * per option, always CC-ing a shared address. Absent (`null`) → the site keeps the\n * default single-POC behavior. Recipients live HERE (server-side Airtable config),\n * never supplied by the submitting site, so the ingest can't be turned into an open\n * relay.\n */\nexport type NotifyRouting = {\n /** The `extraFields` key whose value selects a route, e.g. \"interest\". */\n field: string;\n /** Field-value → recipient address(es). */\n routes: Record<string, string | string[]>;\n /** Recipient(s) when the value matches no route. */\n default?: string | string[];\n /** Address(es) CC'd on every routed (maintenance) send. */\n cc?: string[];\n};\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 /** ISO timestamp of the last draft run where THIS site's GA/Search enrichment\n * ERRORED (vs a legitimate \"not configured\" skip). Set by drafting on a soft-fail,\n * cleared (null) on a clean enrichment, so the per-site analytics-failure signal\n * self-heals. Null when the operator-added `Analytics soft-fail at` column is absent. */\n analyticsSoftFailAt: 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 /** Count of consecutive nightly Renovate auto-fix dispatches for the CURRENT\n * critical/high vuln episode that have NOT yet cleared it. Owned by\n * `renovate-dispatch`: +1 per real dispatch, reset to 0 when vulns clear.\n * Null = field absent / never dispatched → reads as 0. At/above\n * AUTO_FIX_EXHAUSTED_CYCLES the vuln renders as \"auto-fix failed\". */\n securityAutoFixAttempts: number | null;\n /** ISO timestamp the security audit last ran — gates freshness of the Security Updates auto-tick\n * (clean counts only auto-tick when recent). */\n lastSecurityAuditAt: string | null;\n /** The known advisories behind the counts (severity-sorted, capped), so the dashboard can show\n * WHICH packages are vulnerable, not just the totals. null = never audited / unparseable;\n * empty array = audited clean. Written alongside the counts by the security audit. */\n securityAdvisories: SecurityAdvisory[] | null;\n /** Domain/DNS/SSL probe (the `domain` audit). `certDaysRemaining` is days until the TLS cert\n * expires (null = unresolved or no usable cert); `domainCheckedAt` is when it last ran. */\n certDaysRemaining: number | null;\n domainCheckedAt: string | null;\n /** Deployed-URL browser probe (the `browser` audit): cross-engine render OK, mobile render OK,\n * internal-links OK + broken count, and when it last ran (one timestamp gates all three). */\n crossbrowserOk: boolean | null;\n mobileOk: boolean | null;\n linksOk: boolean | null;\n brokenLinks: number | null;\n browserCheckedAt: string | 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 /** Per-site Mailchimp (newsletter). Both must be set for the direct add;\n * blank → skipped. The API key is `key-dc` format; dc is derived from it. */\n mailchimpApiKey: string | null;\n mailchimpAudienceId: 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 notifyRouting: NotifyRouting | 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 * Parse the Websites `Notify Routing` JSON into a NotifyRouting, defensively: a\n * non-string, blank, malformed-JSON, or wrong-shape value yields null (the site\n * then keeps default single-POC routing) — never throws. Mirrors the pipeline's\n * \"a bad Airtable string degrades quietly\" rule.\n */\nexport function parseNotifyRouting(raw: unknown): NotifyRouting | null {\n if (typeof raw !== \"string\") return null;\n const trimmed = raw.trim();\n if (!trimmed) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n const o = parsed as Record<string, unknown>;\n if (typeof o.field !== \"string\" || !o.field.trim()) return null;\n if (!o.routes || typeof o.routes !== \"object\" || Array.isArray(o.routes)) return null;\n const routing: NotifyRouting = {\n field: o.field,\n routes: o.routes as Record<string, string | string[]>,\n };\n if (o.default !== undefined) routing.default = o.default as string | string[];\n if (Array.isArray(o.cc)) routing.cc = o.cc.filter((x): x is string => typeof x === \"string\");\n return routing;\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\nconst FREQUENCIES: readonly Frequency[] = [\"None\", \"Monthly\", \"Quarterly\", \"Yearly\"];\n\n/** Coerce an Airtable single-select value to a known Frequency. An unrecognized value — a\n * renamed / typo'd / whitespace option — falls back to \"None\" (its section is simply omitted)\n * rather than flowing a bogus string downstream, which the announcement would otherwise render\n * as \"We do this undefined.\" into a client email. */\nfunction toFrequency(raw: unknown): Frequency {\n return typeof raw === \"string\" && (FREQUENCIES as readonly string[]).includes(raw)\n ? (raw as Frequency)\n : \"None\";\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: toFrequency(f[\"maintenence freq\"]),\n testingFreq: toFrequency(f[\"testing freq\"]),\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 analyticsSoftFailAt: (f[\"Analytics soft-fail at\"] 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 securityAutoFixAttempts: (f[\"Security Auto-Fix Attempts\"] as number | undefined) ?? null,\n lastSecurityAuditAt: (f[\"Last security audit at\"] as string | undefined) ?? null,\n securityAdvisories: parseSecurityAdvisories(f[\"Security advisories\"]),\n certDaysRemaining: (f[\"Cert days remaining\"] as number | undefined) ?? null,\n domainCheckedAt: (f[\"Domain checked at\"] as string | undefined) ?? null,\n crossbrowserOk:\n typeof f[\"Crossbrowser OK\"] === \"boolean\" ? (f[\"Crossbrowser OK\"] as boolean) : null,\n mobileOk: typeof f[\"Mobile OK\"] === \"boolean\" ? (f[\"Mobile OK\"] as boolean) : null,\n linksOk: typeof f[\"Links OK\"] === \"boolean\" ? (f[\"Links OK\"] as boolean) : null,\n brokenLinks: typeof f[\"Broken links\"] === \"number\" ? (f[\"Broken links\"] as number) : null,\n browserCheckedAt: (f[\"Browser checked at\"] as string | 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 notifyRouting: parseNotifyRouting(f[\"Notify Routing\"]),\n mailchimpApiKey: trimToNull(f[\"Mailchimp API Key\"]),\n mailchimpAudienceId: trimToNull(f[\"Mailchimp Audience ID\"]),\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 };\nexport type Severity = \"low\" | \"moderate\" | \"high\" | \"critical\";\n/** One known vulnerability behind the security counts, as persisted/rendered. */\nexport type SecurityAdvisory = {\n module: string;\n severity: Severity;\n title: string;\n cves: string[];\n url: string | null;\n};\nexport type DomainResult = { certDaysRemaining: number | null; checkedAt: string };\nexport type BrowserAuditFields = {\n desktopOk: boolean;\n mobileOk: boolean;\n linksOk: boolean;\n brokenLinks: number;\n checkedAt: string;\n};\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\n/** Critical first → low last; the persist cap and the dashboard list both lean on this. */\nexport const SEVERITY_RANK: Record<Severity, number> = {\n critical: 0,\n high: 1,\n moderate: 2,\n low: 3,\n};\n/** Cap persisted advisories so the JSON field can't blow up on a badly-neglected site. */\nconst MAX_PERSISTED_ADVISORIES = 25;\n\n/** Coerce one untrusted value into a SecurityAdvisory, or null if it isn't shaped like one.\n * Shared by the read path (`parseSecurityAdvisories`) and the write path so both validate\n * identically. A missing module or unrecognized severity drops the entry entirely. */\nexport function normalizeSecurityAdvisory(raw: unknown): SecurityAdvisory | null {\n if (!raw || typeof raw !== \"object\") return null;\n const e = raw as Record<string, unknown>;\n const module = typeof e[\"module\"] === \"string\" ? (e[\"module\"] as string) : null;\n const severity = e[\"severity\"];\n if (module === null) return null;\n if (\n severity !== \"low\" &&\n severity !== \"moderate\" &&\n severity !== \"high\" &&\n severity !== \"critical\"\n )\n return null;\n const cves = Array.isArray(e[\"cves\"])\n ? (e[\"cves\"] as unknown[]).filter((c): c is string => typeof c === \"string\")\n : [];\n return {\n module,\n severity,\n title: typeof e[\"title\"] === \"string\" ? (e[\"title\"] as string) : \"\",\n cves,\n url: typeof e[\"url\"] === \"string\" ? (e[\"url\"] as string) : null,\n };\n}\n\n/** Parse the `Security advisories` JSON cell. null when absent/blank/unparseable/not-an-array\n * (treated as \"never audited\"); a valid array (possibly empty = audited clean) otherwise. */\nexport function parseSecurityAdvisories(raw: unknown): SecurityAdvisory[] | null {\n if (typeof raw !== \"string\" || raw.trim() === \"\") return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!Array.isArray(parsed)) return null;\n return parsed.map(normalizeSecurityAdvisory).filter((a): a is SecurityAdvisory => a !== null);\n}\n\nfunction securityAdvisoryFields(advisories: SecurityAdvisory[]): FieldSet {\n const capped = [...advisories]\n .sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])\n .slice(0, MAX_PERSISTED_ADVISORIES);\n // Always write — an empty array (\"[]\") clears a stale list when a once-vulnerable site goes clean.\n return { \"Security advisories\": JSON.stringify(capped) };\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 // Stamp freshness alongside the counts so the Security Updates auto-tick can require a recent\n // audit (a clean count from months ago must not silently keep ticking the box).\n \"Last security audit at\": new Date().toISOString(),\n };\n}\n\nfunction domainFields(result: DomainResult): FieldSet {\n const fields: FieldSet = { \"Domain checked at\": result.checkedAt };\n // Write the cert days UNCONDITIONALLY: a null (unresolved / no usable cert) must CLEAR any\n // previously-good value. Leaving a stale number in place — next to a freshly-stamped \"Domain\n // checked at\" — false-passes the Domain/DNS/SSL auto-tick (domainEvidence reads the stale\n // non-null value as a current pass) for a site that's actually down. null clears the cell in\n // Airtable; FieldSet's type doesn't model null, hence the cast through a widened record.\n (fields as Record<string, number | null>)[\"Cert days remaining\"] = result.certDaysRemaining;\n return fields;\n}\n\nfunction browserFields(r: BrowserAuditFields): FieldSet {\n return {\n \"Crossbrowser OK\": r.desktopOk,\n \"Mobile OK\": r.mobileOk,\n \"Links OK\": r.linksOk,\n \"Broken links\": r.brokenLinks,\n \"Browser checked at\": r.checkedAt,\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/**\n * Record (or clear) the per-site GA/Search enrichment health on the `Analytics\n * soft-fail at` column. `at` is an ISO timestamp when the site's last draft had a\n * GA/Search soft-failure, or `null` to clear it after a clean enrichment. The\n * caller (drafting) swallows errors: this column is operator-added, so until it\n * exists Airtable throws UNKNOWN_FIELD_NAME — which must not break a draft.\n */\nexport async function updateAnalyticsHealth(\n base: AirtableBase,\n recordId: string,\n at: string | null,\n): Promise<void> {\n const fields: Record<string, string | null> = { \"Analytics soft-fail at\": at };\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: fields as FieldSet }]);\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/** Persist a site's auto-fix attempt counter. Its own one-field writer so the\n * nightly Renovate dispatch can update it without touching the audit's counts. */\nexport async function updateAutoFixAttempts(\n base: AirtableBase,\n recordId: string,\n attempts: number,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([\n { id: recordId, fields: { \"Security Auto-Fix Attempts\": attempts } },\n ]);\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 securityAdvisories?: SecurityAdvisory[];\n domain?: DomainResult;\n browser?: BrowserAuditFields;\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 // Separate slice (not folded into `security`) so the advisory list and the counts can be\n // written independently, but in practice the security audit supplies both together.\n if (audits.securityAdvisories)\n Object.assign(fields, securityAdvisoryFields(audits.securityAdvisories));\n if (audits.domain) Object.assign(fields, domainFields(audits.domain));\n if (audits.browser) Object.assign(fields, browserFields(audits.browser));\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"],"mappings":"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","names":[]}
@@ -0,0 +1,14 @@
1
+ // src/alerts/renovate.ts
2
+ var RENOVATE_HEAD_PREFIXES = ["renovate/", "renovate-"];
3
+ function isRenovatePR(pr) {
4
+ return RENOVATE_HEAD_PREFIXES.some((p) => pr.headRef.startsWith(p));
5
+ }
6
+ function isFailingRenovatePR(pr) {
7
+ return isRenovatePR(pr) && pr.ciState === "failing";
8
+ }
9
+
10
+ export {
11
+ isRenovatePR,
12
+ isFailingRenovatePR
13
+ };
14
+ //# sourceMappingURL=chunk-PZQ3WED2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alerts/renovate.ts"],"sourcesContent":["import type { PullRequestSummary } from \"../github/gh.js\";\n\n/**\n * Renovate's default `branchPrefix` is `renovate/`, so every update branch on a\n * default-config repo is `renovate/<topic>` — grouped majors included (they're\n * `renovate/<group-slug>`, still slash-prefixed, not a separate shape). We also\n * match a bare `renovate-` prefix to defensively catch a repo that sets a custom\n * non-slash `branchPrefix`; the tradeoff is that a human branch named `renovate-foo`\n * is misclassified — an acceptable bias for an alerting tool that should prefer\n * over-matching to silently missing a broken update.\n */\nconst RENOVATE_HEAD_PREFIXES = [\"renovate/\", \"renovate-\"];\n\nexport function isRenovatePR(pr: Pick<PullRequestSummary, \"headRef\">): boolean {\n return RENOVATE_HEAD_PREFIXES.some((p) => pr.headRef.startsWith(p));\n}\n\nexport function isFailingRenovatePR(pr: PullRequestSummary): boolean {\n return isRenovatePR(pr) && pr.ciState === \"failing\";\n}\n"],"mappings":";AAWA,IAAM,yBAAyB,CAAC,aAAa,WAAW;AAEjD,SAAS,aAAa,IAAkD;AAC7E,SAAO,uBAAuB,KAAK,CAAC,MAAM,GAAG,QAAQ,WAAW,CAAC,CAAC;AACpE;AAEO,SAAS,oBAAoB,IAAiC;AACnE,SAAO,aAAa,EAAE,KAAK,GAAG,YAAY;AAC5C;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  siteSlug
3
- } from "./chunk-CLVTVDOU.js";
3
+ } from "./chunk-PKIYAWA4.js";
4
4
 
5
5
  // src/audits/lighthouse-airtable.ts
6
6
  import { readFile } from "fs/promises";
@@ -48,4 +48,4 @@ export {
48
48
  lighthouseScoresFromResult,
49
49
  resolveSlugFromCwd
50
50
  };
51
- //# sourceMappingURL=chunk-IMIR7SC2.js.map
51
+ //# sourceMappingURL=chunk-RYECSIEA.js.map
@@ -17,7 +17,7 @@ import {
17
17
  import {
18
18
  siteSlug,
19
19
  updateAnalyticsHealth
20
- } from "./chunk-CLVTVDOU.js";
20
+ } from "./chunk-PKIYAWA4.js";
21
21
  import {
22
22
  defaultCredentialsPath
23
23
  } from "./chunk-6VAL7XJT.js";
@@ -441,4 +441,4 @@ export {
441
441
  fetchGaUsers,
442
442
  fetchSearch
443
443
  };
444
- //# sourceMappingURL=chunk-W3G7XFBZ.js.map
444
+ //# sourceMappingURL=chunk-S7WKPRID.js.map
package/dist/cli/bin.js CHANGED
@@ -104,7 +104,7 @@ cli.command("sync-configs [site]", "Sync canonical configs into a site.").option
104
104
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
105
105
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
106
106
  async (site, opts) => runOrExit(
107
- async () => (await import("../sync-configs-JPRLAW5J.js")).runSyncConfigsCommand(site, opts),
107
+ async () => (await import("../sync-configs-ZYKZCP4C.js")).runSyncConfigsCommand(site, opts),
108
108
  opts
109
109
  )
110
110
  );
@@ -113,7 +113,7 @@ cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>",
113
113
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
114
114
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
115
115
  async (site, opts) => runOrExit(
116
- async () => (await import("../bump-deps-O3SOLQYY.js")).runBumpDepsCommand(site, opts),
116
+ async () => (await import("../bump-deps-6D4VCAFQ.js")).runBumpDepsCommand(site, opts),
117
117
  opts
118
118
  )
119
119
  );
@@ -122,7 +122,7 @@ cli.command(
122
122
  "Bootstrap a repo to keep itself updated (CI + Renovate + auto-merge)."
123
123
  ).option("--dry", "List what would be enabled without writing or opening PRs").option("--fleet <inventory>", 'Inventory file (.json or .mjs/.js), or "airtable"').option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
124
124
  async (site, opts) => runOrExit(
125
- async () => (await import("../self-updating-CIUMOMYI.js")).runSelfUpdatingCommand(site, opts),
125
+ async () => (await import("../self-updating-J7U7WIP3.js")).runSelfUpdatingCommand(site, opts),
126
126
  opts
127
127
  )
128
128
  );
@@ -131,7 +131,7 @@ cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to
131
131
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
132
132
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
133
133
  async (upgrade, site, opts) => runOrExit(
134
- async () => (await import("../upgrade-ERA45EVR.js")).runUpgradeCommand(upgrade, site, opts),
134
+ async () => (await import("../upgrade-UAXREMED.js")).runUpgradeCommand(upgrade, site, opts),
135
135
  opts
136
136
  )
137
137
  );
@@ -143,7 +143,7 @@ cli.command(
143
143
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
144
144
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
145
145
  async (site, opts) => runOrExit(
146
- async () => (await import("../convert-to-pnpm-UR64X3OX.js")).runConvertToPnpmCommand(site, opts),
146
+ async () => (await import("../convert-to-pnpm-JYUUUDDP.js")).runConvertToPnpmCommand(site, opts),
147
147
  opts
148
148
  )
149
149
  );
@@ -152,7 +152,7 @@ cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an alre
152
152
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
153
153
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
154
154
  async (site, opts) => runOrExit(
155
- async () => (await import("../svelte-codemods-BYRJAU7I.js")).runSvelteCodemodsCommand(site, opts),
155
+ async () => (await import("../svelte-codemods-BW4M4CDF.js")).runSvelteCodemodsCommand(site, opts),
156
156
  opts
157
157
  )
158
158
  );
@@ -164,7 +164,7 @@ cli.command(
164
164
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
165
165
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
166
166
  async (site, opts) => runOrExit(
167
- async () => (await import("../onboard-TCQOP2W4.js")).runOnboardCommand(site, opts),
167
+ async () => (await import("../onboard-UM6UKX2V.js")).runOnboardCommand(site, opts),
168
168
  opts
169
169
  )
170
170
  );
@@ -175,14 +175,14 @@ cli.command(
175
175
  "--fleet <inventory>",
176
176
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
177
177
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
178
- async (site, opts) => runOrExit(async () => (await import("../init-JITQXFVL.js")).runInitCommand(site, opts), opts)
178
+ async (site, opts) => runOrExit(async () => (await import("../init-6LJU72F2.js")).runInitCommand(site, opts), opts)
179
179
  );
180
180
  cli.command(
181
181
  "launch <site>",
182
182
  "Bootstrap + first-audit a site, then draft its launch email for approval."
183
183
  ).action(
184
184
  async (site, opts) => runOrExit(
185
- async () => (await import("../launch-HXHEL6ID.js")).runLaunchCommand(site, opts),
185
+ async () => (await import("../launch-6ZB3QA2I.js")).runLaunchCommand(site, opts),
186
186
  opts
187
187
  )
188
188
  );
@@ -191,7 +191,7 @@ cli.command(
191
191
  "Draft the monthly-report announcement email for maintenance sites (all, or one) for approval."
192
192
  ).action(
193
193
  async (site, opts) => runOrExit(
194
- async () => (await import("../announce-JEVCDDKL.js")).runAnnounceCommand(site, opts),
194
+ async () => (await import("../announce-JO67XZLZ.js")).runAnnounceCommand(site, opts),
195
195
  opts
196
196
  )
197
197
  );
@@ -206,7 +206,7 @@ cli.command("report [site]", "Draft or send maintenance/testing reports.").optio
206
206
  "Email the operator one daily digest of reports ready for approval (skips when empty)."
207
207
  ).action(
208
208
  async (site, opts) => runOrExit(
209
- async () => (await import("../report-SD3CNBOQ.js")).runReportCommand(site, opts),
209
+ async () => (await import("../report-3YVSMRFE.js")).runReportCommand(site, opts),
210
210
  opts
211
211
  )
212
212
  );
@@ -215,7 +215,7 @@ cli.command(
215
215
  "Sweep the fleet for GitHub signals (Renovate-failing/CI/last-commit) and write Airtable."
216
216
  ).option("--fleet", "Run across every site in the Airtable inventory.").option("--write-airtable", "Write each site's signals back to its Websites row.").action(
217
217
  async (opts) => runOrExit(
218
- async () => (await import("../github-signals-YD64R2AF.js")).runGitHubSignalsCommand({
218
+ async () => (await import("../github-signals-JXHRHDAG.js")).runGitHubSignalsCommand({
219
219
  fleet: opts.fleet,
220
220
  writeAirtable: opts.writeAirtable
221
221
  }),
@@ -233,7 +233,7 @@ cli.command(
233
233
  "Trigger Renovate on fleet sites the security sweep flagged with critical/high vulns."
234
234
  ).option("--fleet", "Run across every active, repo-backed site in the Airtable inventory.").action(
235
235
  async (opts) => runOrExit(
236
- async () => (await import("../renovate-dispatch-ZGLXNPEK.js")).runRenovateDispatchCommand({
236
+ async () => (await import("../renovate-dispatch-L5HWCNRY.js")).runRenovateDispatchCommand({
237
237
  fleet: opts.fleet
238
238
  }),
239
239
  opts
@@ -5,7 +5,7 @@ import {
5
5
  } from "../../chunk-4NAOR44V.js";
6
6
  import {
7
7
  resolveSites
8
- } from "../../chunk-J3OPWFZE.js";
8
+ } from "../../chunk-IDRBAF2H.js";
9
9
  import {
10
10
  ALL_AUDIT_NAMES,
11
11
  runOneAudit
@@ -215,9 +215,9 @@ ${skipNotice}`;
215
215
  let writeBackFailed = false;
216
216
  if (opts.writeAirtable !== void 0) {
217
217
  const { openBase, readAirtableConfig } = await import("../../client-EPVO3FEH.js");
218
- const { listWebsites } = await import("../../websites-X5DLC2PC.js");
218
+ const { listWebsites } = await import("../../websites-EEZVZXQT.js");
219
219
  if (opts.fleet !== void 0) {
220
- const { writeFleetAuditsToAirtable, formatFleetWriteSummary } = await import("../../write-audits-to-airtable-CE4POBAF.js");
220
+ const { writeFleetAuditsToAirtable, formatFleetWriteSummary } = await import("../../write-audits-to-airtable-LUFY424L.js");
221
221
  const base = openBase(readAirtableConfig());
222
222
  const websites = await listWebsites(base);
223
223
  const fleetWrite = await writeFleetAuditsToAirtable({ base, websites, results });
@@ -226,8 +226,8 @@ ${skipNotice}`;
226
226
 
227
227
  ${formatFleetWriteSummary(fleetWrite)}`;
228
228
  } else {
229
- const { resolveSlugFromCwd } = await import("../../lighthouse-airtable-4FKYKFH6.js");
230
- const { writeAuditsToAirtable } = await import("../../write-audits-to-airtable-CE4POBAF.js");
229
+ const { resolveSlugFromCwd } = await import("../../lighthouse-airtable-FSGOUVMQ.js");
230
+ const { writeAuditsToAirtable } = await import("../../write-audits-to-airtable-LUFY424L.js");
231
231
  const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd(cwd);
232
232
  let writeSummary = null;
233
233
  await new Listr(