@reddoorla/maintenance 0.66.0 → 0.68.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 (107) hide show
  1. package/README.md +16 -0
  2. package/dist/airtable-WGQQ3KAD.js +9 -0
  3. package/dist/{announce-YPXVHNNP.js → announce-72R3LRNT.js} +9 -11
  4. package/dist/{announce-YPXVHNNP.js.map → announce-72R3LRNT.js.map} +1 -1
  5. package/dist/{bump-deps-INDDTDSG.js → bump-deps-QNM3H4GI.js} +2 -2
  6. package/dist/{chunk-CH6K2VMM.js → chunk-2E2X5BCG.js} +43 -3
  7. package/dist/chunk-2E2X5BCG.js.map +1 -0
  8. package/dist/{chunk-VKAYZVMI.js → chunk-45AUGDFR.js} +2 -1
  9. package/dist/chunk-45AUGDFR.js.map +1 -0
  10. package/dist/{chunk-SYTPVYIK.js → chunk-6JFOAPW4.js} +241 -4
  11. package/dist/chunk-6JFOAPW4.js.map +1 -0
  12. package/dist/{chunk-NMGGP5WN.js → chunk-6MT5W6UF.js} +2 -2
  13. package/dist/{chunk-XKSKHHSX.js → chunk-A5KYRGQA.js} +4 -4
  14. package/dist/{chunk-4P6UQG5L.js → chunk-C3PDS623.js} +6 -1
  15. package/dist/chunk-C3PDS623.js.map +1 -0
  16. package/dist/{chunk-JD3X3QMP.js → chunk-GKSXZIMX.js} +3 -3
  17. package/dist/{chunk-CWSQZ27M.js → chunk-H6YZBXBQ.js} +19 -13
  18. package/dist/chunk-H6YZBXBQ.js.map +1 -0
  19. package/dist/{chunk-XKKTK6WA.js → chunk-HIN3P4ZN.js} +23 -2
  20. package/dist/chunk-HIN3P4ZN.js.map +1 -0
  21. package/dist/chunk-MAYHLXCS.js +322 -0
  22. package/dist/chunk-MAYHLXCS.js.map +1 -0
  23. package/dist/{chunk-G35B5U6F.js → chunk-O6G4HHMF.js} +7 -9
  24. package/dist/{chunk-G35B5U6F.js.map → chunk-O6G4HHMF.js.map} +1 -1
  25. package/dist/{chunk-G2YRXV26.js → chunk-OTBA7DON.js} +2 -2
  26. package/dist/{chunk-PTPTVVY7.js → chunk-PH3AGF4Z.js} +2 -2
  27. package/dist/{chunk-6LIWDIXU.js → chunk-RINXPU3H.js} +2 -2
  28. package/dist/{chunk-YBYWNUT3.js → chunk-XWPZ46IN.js} +2 -2
  29. package/dist/{chunk-LISJ3K3R.js → chunk-YQMYQAJX.js} +2 -2
  30. package/dist/cli/bin.js +33 -15
  31. package/dist/cli/bin.js.map +1 -1
  32. package/dist/cli/commands/audit.js +8 -8
  33. package/dist/client-NVVQK3HH.js +11 -0
  34. package/dist/{convert-to-pnpm-4GD4667L.js → convert-to-pnpm-XH7NYRVK.js} +2 -2
  35. package/dist/{db-YBLQR2YY.js → db-3QW2XEXA.js} +3 -3
  36. package/dist/{digest-SIZUXT67.js → digest-J7TDG62Q.js} +14 -5
  37. package/dist/digest-J7TDG62Q.js.map +1 -0
  38. package/dist/ensure-site-2PF522VL.js +87 -0
  39. package/dist/ensure-site-2PF522VL.js.map +1 -0
  40. package/dist/forms/index.d.ts +13 -0
  41. package/dist/forms/index.js +28 -2
  42. package/dist/forms/index.js.map +1 -1
  43. package/dist/{github-signals-3O64PFWP.js → github-signals-PP7GWYIY.js} +7 -7
  44. package/dist/index.d.ts +19 -2
  45. package/dist/index.js +117 -39
  46. package/dist/index.js.map +1 -1
  47. package/dist/{init-KMWXJZEX.js → init-WLR35YGN.js} +2 -2
  48. package/dist/{launch-5KRCKFYM.js → launch-2HPQXWII.js} +10 -12
  49. package/dist/{launch-5KRCKFYM.js.map → launch-2HPQXWII.js.map} +1 -1
  50. package/dist/{lighthouse-airtable-5JICCZND.js → lighthouse-airtable-UBSXKHAQ.js} +3 -3
  51. package/dist/migrate-35DNNF3H.js +7 -0
  52. package/dist/{onboard-RCBXS5B3.js → onboard-X6KJ5Z2V.js} +2 -2
  53. package/dist/{orchestrate-O4KR6JME.js → orchestrate-PVCGBH4K.js} +7 -9
  54. package/dist/preflight-KWVYJ2DU.js +64 -0
  55. package/dist/preflight-KWVYJ2DU.js.map +1 -0
  56. package/dist/{renovate-dispatch-ABRSIT77.js → renovate-dispatch-LG4RJGDY.js} +2 -2
  57. package/dist/{report-LSGTMXQJ.js → report-ZBFMTT76.js} +8 -9
  58. package/dist/{report-LSGTMXQJ.js.map → report-ZBFMTT76.js.map} +1 -1
  59. package/dist/{self-updating-CPMY3HFY.js → self-updating-JTTFOJ7Q.js} +2 -2
  60. package/dist/{selftest-7Y5K4PYL.js → selftest-LFZPZRJP.js} +10 -12
  61. package/dist/{selftest-7Y5K4PYL.js.map → selftest-LFZPZRJP.js.map} +1 -1
  62. package/dist/{svelte-codemods-GJRTBLJE.js → svelte-codemods-KJLNH6FQ.js} +2 -2
  63. package/dist/{sync-configs-NYJNSIRN.js → sync-configs-6GBZLGRQ.js} +2 -2
  64. package/dist/{upgrade-LVYRDKDL.js → upgrade-TPTE26FG.js} +2 -2
  65. package/dist/{websites-5ETIXB7F.js → websites-6YTT5ZTO.js} +2 -2
  66. package/dist/{write-audits-to-airtable-GHR337N7.js → write-audits-to-airtable-IWOO4FFH.js} +4 -4
  67. package/package.json +1 -1
  68. package/dist/airtable-M4AQHRNV.js +0 -9
  69. package/dist/chunk-4P6UQG5L.js.map +0 -1
  70. package/dist/chunk-CH6K2VMM.js.map +0 -1
  71. package/dist/chunk-CWSQZ27M.js.map +0 -1
  72. package/dist/chunk-OCS7SZUZ.js +0 -243
  73. package/dist/chunk-OCS7SZUZ.js.map +0 -1
  74. package/dist/chunk-OGGRFBL4.js +0 -14
  75. package/dist/chunk-OGGRFBL4.js.map +0 -1
  76. package/dist/chunk-SYTPVYIK.js.map +0 -1
  77. package/dist/chunk-VKAYZVMI.js.map +0 -1
  78. package/dist/chunk-XKKTK6WA.js.map +0 -1
  79. package/dist/client-C2GOZD2D.js +0 -11
  80. package/dist/digest-SIZUXT67.js.map +0 -1
  81. package/dist/migrate-BFOG2OCP.js +0 -7
  82. /package/dist/{airtable-M4AQHRNV.js.map → airtable-WGQQ3KAD.js.map} +0 -0
  83. /package/dist/{bump-deps-INDDTDSG.js.map → bump-deps-QNM3H4GI.js.map} +0 -0
  84. /package/dist/{chunk-NMGGP5WN.js.map → chunk-6MT5W6UF.js.map} +0 -0
  85. /package/dist/{chunk-XKSKHHSX.js.map → chunk-A5KYRGQA.js.map} +0 -0
  86. /package/dist/{chunk-JD3X3QMP.js.map → chunk-GKSXZIMX.js.map} +0 -0
  87. /package/dist/{chunk-G2YRXV26.js.map → chunk-OTBA7DON.js.map} +0 -0
  88. /package/dist/{chunk-PTPTVVY7.js.map → chunk-PH3AGF4Z.js.map} +0 -0
  89. /package/dist/{chunk-6LIWDIXU.js.map → chunk-RINXPU3H.js.map} +0 -0
  90. /package/dist/{chunk-YBYWNUT3.js.map → chunk-XWPZ46IN.js.map} +0 -0
  91. /package/dist/{chunk-LISJ3K3R.js.map → chunk-YQMYQAJX.js.map} +0 -0
  92. /package/dist/{client-C2GOZD2D.js.map → client-NVVQK3HH.js.map} +0 -0
  93. /package/dist/{convert-to-pnpm-4GD4667L.js.map → convert-to-pnpm-XH7NYRVK.js.map} +0 -0
  94. /package/dist/{db-YBLQR2YY.js.map → db-3QW2XEXA.js.map} +0 -0
  95. /package/dist/{github-signals-3O64PFWP.js.map → github-signals-PP7GWYIY.js.map} +0 -0
  96. /package/dist/{init-KMWXJZEX.js.map → init-WLR35YGN.js.map} +0 -0
  97. /package/dist/{lighthouse-airtable-5JICCZND.js.map → lighthouse-airtable-UBSXKHAQ.js.map} +0 -0
  98. /package/dist/{migrate-BFOG2OCP.js.map → migrate-35DNNF3H.js.map} +0 -0
  99. /package/dist/{onboard-RCBXS5B3.js.map → onboard-X6KJ5Z2V.js.map} +0 -0
  100. /package/dist/{orchestrate-O4KR6JME.js.map → orchestrate-PVCGBH4K.js.map} +0 -0
  101. /package/dist/{renovate-dispatch-ABRSIT77.js.map → renovate-dispatch-LG4RJGDY.js.map} +0 -0
  102. /package/dist/{self-updating-CPMY3HFY.js.map → self-updating-JTTFOJ7Q.js.map} +0 -0
  103. /package/dist/{svelte-codemods-GJRTBLJE.js.map → svelte-codemods-KJLNH6FQ.js.map} +0 -0
  104. /package/dist/{sync-configs-NYJNSIRN.js.map → sync-configs-6GBZLGRQ.js.map} +0 -0
  105. /package/dist/{upgrade-LVYRDKDL.js.map → upgrade-TPTE26FG.js.map} +0 -0
  106. /package/dist/{websites-5ETIXB7F.js.map → websites-6YTT5ZTO.js.map} +0 -0
  107. /package/dist/{write-audits-to-airtable-GHR337N7.js.map → write-audits-to-airtable-IWOO4FFH.js.map} +0 -0
@@ -49,10 +49,27 @@ var MIGRATIONS = [
49
49
  );
50
50
  CREATE INDEX IF NOT EXISTS idx_fleet_events_ts ON fleet_events (ts);
51
51
  `
52
+ },
53
+ {
54
+ id: "0003_add_spam_score",
55
+ // SQLite `ADD COLUMN` has no `IF NOT EXISTS`, so it is NOT self-idempotent: if the
56
+ // `_migrations` marker is lost after the column is added (crash between statement
57
+ // and marker write), a re-run would throw `duplicate column name`. `migrate.ts`
58
+ // recognizes that error as already-applied and records the marker, keeping the
59
+ // runner idempotent (see `isAlreadyAppliedError`).
60
+ sql: `ALTER TABLE submissions ADD COLUMN spam_score REAL;`
61
+ },
62
+ {
63
+ id: "0004_add_spam_reason",
64
+ sql: `ALTER TABLE submissions ADD COLUMN spam_reason TEXT;`
52
65
  }
53
66
  ];
54
67
 
55
68
  // src/db/migrate.ts
69
+ function isAlreadyAppliedError(err) {
70
+ const message = err instanceof Error ? err.message : String(err);
71
+ return /duplicate column name/i.test(message);
72
+ }
56
73
  async function runMigrations(client) {
57
74
  await client.execute(
58
75
  "CREATE TABLE IF NOT EXISTS _migrations (id TEXT PRIMARY KEY, applied_at TEXT NOT NULL)"
@@ -62,7 +79,11 @@ async function runMigrations(client) {
62
79
  const ran = [];
63
80
  for (const m of MIGRATIONS) {
64
81
  if (applied.has(m.id)) continue;
65
- await client.executeMultiple(m.sql);
82
+ try {
83
+ await client.executeMultiple(m.sql);
84
+ } catch (err) {
85
+ if (!isAlreadyAppliedError(err)) throw err;
86
+ }
66
87
  await client.execute({
67
88
  sql: "INSERT INTO _migrations (id, applied_at) VALUES (?, ?)",
68
89
  args: [m.id, (/* @__PURE__ */ new Date()).toISOString()]
@@ -75,4 +96,4 @@ async function runMigrations(client) {
75
96
  export {
76
97
  runMigrations
77
98
  };
78
- //# sourceMappingURL=chunk-XKKTK6WA.js.map
99
+ //# sourceMappingURL=chunk-HIN3P4ZN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/migrations.ts","../src/db/migrate.ts"],"sourcesContent":["/** Ordered, append-only list of standard-SQL migration scripts. Each runs once,\n * tracked by `id` in the `_migrations` table (see migrate.ts). Statements use\n * IF NOT EXISTS so even a partial re-apply is safe. Never edit a shipped script —\n * add a new one. Every statement in a script MUST be independently idempotent:\n * `migrate.ts` runs each script via `executeMultiple`, which is NOT transactional,\n * so a mid-script failure can leave earlier statements applied and the id unrecorded\n * — a re-run then re-executes the whole script. Standard SQLite SQL only (no\n * Turso-specific syntax) so the host stays a connection-string swap. */\nexport type Migration = { id: string; sql: string };\n\nexport const MIGRATIONS: Migration[] = [\n {\n id: \"0001_init\",\n sql: `\n CREATE TABLE IF NOT EXISTS submissions (\n id TEXT PRIMARY KEY,\n submission_id INTEGER,\n site_id TEXT NOT NULL,\n form_type TEXT NOT NULL,\n name TEXT NOT NULL,\n email TEXT NOT NULL,\n phone TEXT,\n message TEXT,\n extra_fields TEXT,\n source_url TEXT,\n utm TEXT,\n submitted_at TEXT,\n status TEXT NOT NULL DEFAULT 'new',\n notify_status TEXT NOT NULL DEFAULT 'skipped',\n resend_message_id TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_submissions_site_submitted\n ON submissions (site_id, submitted_at DESC);\n CREATE INDEX IF NOT EXISTS idx_submissions_status\n ON submissions (status);\n CREATE TABLE IF NOT EXISTS spam_screenouts (\n site_id TEXT NOT NULL,\n date TEXT NOT NULL,\n honeypot INTEGER NOT NULL DEFAULT 0,\n too_fast INTEGER NOT NULL DEFAULT 0,\n marked_spam INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (site_id, date)\n );\n `,\n },\n {\n id: \"0002_fleet_events\",\n sql: `\n CREATE TABLE IF NOT EXISTS fleet_events (\n id TEXT PRIMARY KEY,\n ts TEXT NOT NULL,\n type TEXT NOT NULL,\n site_id TEXT,\n site_name TEXT,\n summary TEXT NOT NULL,\n data TEXT,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_fleet_events_ts ON fleet_events (ts);\n `,\n },\n {\n id: \"0003_add_spam_score\",\n // SQLite `ADD COLUMN` has no `IF NOT EXISTS`, so it is NOT self-idempotent: if the\n // `_migrations` marker is lost after the column is added (crash between statement\n // and marker write), a re-run would throw `duplicate column name`. `migrate.ts`\n // recognizes that error as already-applied and records the marker, keeping the\n // runner idempotent (see `isAlreadyAppliedError`).\n sql: `ALTER TABLE submissions ADD COLUMN spam_score REAL;`,\n },\n {\n id: \"0004_add_spam_reason\",\n sql: `ALTER TABLE submissions ADD COLUMN spam_reason TEXT;`,\n },\n];\n","import type { Client } from \"@libsql/client\";\nimport { MIGRATIONS } from \"./migrations.js\";\n\n/** True when an error means \"this DDL was already applied\" and re-applying is a\n * no-op we can safely ignore. `CREATE ... IF NOT EXISTS` never throws, but SQLite\n * `ALTER TABLE ... ADD COLUMN` has no `IF NOT EXISTS`, so a re-run after a lost\n * `_migrations` marker throws `duplicate column name: …`. Treating that as\n * already-applied keeps `runMigrations` idempotent for `ADD COLUMN` migrations —\n * without it a lost marker would boot-loop `openDb()` on every call. */\nfunction isAlreadyAppliedError(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return /duplicate column name/i.test(message);\n}\n\n/** Apply every not-yet-applied migration in order against a raw libSQL client,\n * tracking applied ids in `_migrations`. Idempotent: ids already present are\n * skipped, the DDL uses IF NOT EXISTS where it can, and an already-applied\n * `ADD COLUMN` (recognized by its error) is swallowed so the marker is still\n * recorded. Returns the ids applied this run. Runs on every openDb() — cheap\n * (one indexed SELECT) and the gate every fresh Turso database needs before its\n * first write. */\nexport async function runMigrations(client: Client): Promise<string[]> {\n await client.execute(\n \"CREATE TABLE IF NOT EXISTS _migrations (id TEXT PRIMARY KEY, applied_at TEXT NOT NULL)\",\n );\n const existing = await client.execute(\"SELECT id FROM _migrations\");\n const applied = new Set(existing.rows.map((r) => String(r.id)));\n const ran: string[] = [];\n for (const m of MIGRATIONS) {\n if (applied.has(m.id)) continue;\n try {\n await client.executeMultiple(m.sql);\n } catch (err) {\n // A re-run after a lost marker can re-execute a non-idempotent `ADD COLUMN`;\n // treat \"already applied\" as success so we still record the marker below and\n // never boot-loop. Any other error propagates unchanged.\n if (!isAlreadyAppliedError(err)) throw err;\n }\n await client.execute({\n sql: \"INSERT INTO _migrations (id, applied_at) VALUES (?, ?)\",\n args: [m.id, new Date().toISOString()],\n });\n ran.push(m.id);\n }\n return ran;\n}\n"],"mappings":";AAUO,IAAM,aAA0B;AAAA,EACrC;AAAA,IACE,IAAI;AAAA,IACJ,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA+BP;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaP;AAAA,EACA;AAAA,IACE,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMJ,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,KAAK;AAAA,EACP;AACF;;;ACjEA,SAAS,sBAAsB,KAAuB;AACpD,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,SAAO,yBAAyB,KAAK,OAAO;AAC9C;AASA,eAAsB,cAAc,QAAmC;AACrE,QAAM,OAAO;AAAA,IACX;AAAA,EACF;AACA,QAAM,WAAW,MAAM,OAAO,QAAQ,4BAA4B;AAClE,QAAM,UAAU,IAAI,IAAI,SAAS,KAAK,IAAI,CAAC,MAAM,OAAO,EAAE,EAAE,CAAC,CAAC;AAC9D,QAAM,MAAgB,CAAC;AACvB,aAAW,KAAK,YAAY;AAC1B,QAAI,QAAQ,IAAI,EAAE,EAAE,EAAG;AACvB,QAAI;AACF,YAAM,OAAO,gBAAgB,EAAE,GAAG;AAAA,IACpC,SAAS,KAAK;AAIZ,UAAI,CAAC,sBAAsB,GAAG,EAAG,OAAM;AAAA,IACzC;AACA,UAAM,OAAO,QAAQ;AAAA,MACnB,KAAK;AAAA,MACL,MAAM,CAAC,EAAE,KAAI,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,IACvC,CAAC;AACD,QAAI,KAAK,EAAE,EAAE;AAAA,EACf;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,322 @@
1
+ import {
2
+ ELIGIBLE_STATUSES,
3
+ reportPeriodKey
4
+ } from "./chunk-45AUGDFR.js";
5
+ import {
6
+ isProbablyEmail,
7
+ parseAddresses
8
+ } from "./chunk-H6YZBXBQ.js";
9
+ import {
10
+ listAllReports
11
+ } from "./chunk-6JFOAPW4.js";
12
+ import {
13
+ listWebsites,
14
+ siteSlug
15
+ } from "./chunk-C3PDS623.js";
16
+ import {
17
+ openBase,
18
+ readAirtableConfig
19
+ } from "./chunk-HN6I2XWI.js";
20
+
21
+ // src/reports/preflight.ts
22
+ var OPERATOR_DOMAINS = ["reddoorla.com", "tuckerlemos.com"];
23
+ var STALE_ANCHOR_DAYS = 396;
24
+ function domainOf(addr) {
25
+ const at = addr.lastIndexOf("@");
26
+ return at === -1 ? "" : addr.slice(at + 1).toLowerCase();
27
+ }
28
+ function isOperatorSite(site) {
29
+ if (!site.url) return false;
30
+ try {
31
+ const host = new URL(
32
+ /^[a-z][a-z0-9+.-]*:\/\//i.test(site.url) ? site.url : `https://${site.url}`
33
+ ).hostname.toLowerCase();
34
+ return OPERATOR_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+ function lastSentForType(reports, type) {
40
+ const sent = reports.filter((r) => r.reportType === type && r.sentAt !== null).map((r) => r.sentAt).sort();
41
+ return sent[sent.length - 1] ?? null;
42
+ }
43
+ function checkFrequency(site, which, reports, findings, now) {
44
+ const raw = which === "maintenance" ? site.maintenanceFreqRaw : site.testingFreqRaw;
45
+ const coerced = which === "maintenance" ? site.maintenanceFreq : site.testingFreq;
46
+ if (raw !== null && raw !== "" && coerced === "None" && raw.trim() !== "None") {
47
+ findings.push({
48
+ level: "fail",
49
+ check: "frequency-unrecognized",
50
+ message: `${which} frequency cell is '${raw}' \u2014 not an exact Monthly/Quarterly/Yearly/None, so the scheduler silently treats it as None and the site drops off the calendar; fix the Airtable value`
51
+ });
52
+ return;
53
+ }
54
+ if (coerced === "None") return;
55
+ const anchor = which === "maintenance" ? site.maintenanceDay : site.testingDay;
56
+ if (!anchor) return;
57
+ const type = which === "maintenance" ? "Maintenance" : "Testing";
58
+ const lastSent = lastSentForType(reports, type);
59
+ if (lastSent && new Date(lastSent).getTime() >= new Date(anchor).getTime()) return;
60
+ const ageDays = (now.getTime() - new Date(anchor).getTime()) / 864e5;
61
+ if (ageDays > STALE_ANCHOR_DAYS) {
62
+ findings.push({
63
+ level: "warn",
64
+ check: "anchor-stale",
65
+ message: `${which} day anchor is ${anchor} (>13 months old) and nothing newer has been sent \u2014 \`report --due\` will draft a back-dated overdue report; reset the anchor or clear the schedule`
66
+ });
67
+ }
68
+ }
69
+ function preflightSite(site, reports, type, now) {
70
+ const findings = [];
71
+ const scheduledForType = type === "Announcement" ? true : type === "Maintenance" ? site.maintenanceFreq !== "None" : site.testingFreq !== "None";
72
+ if (!scheduledForType) {
73
+ findings.push({
74
+ level: "info",
75
+ check: "not-scheduled",
76
+ message: `no ${type} schedule (frequency is None/blank) \u2014 send-requirement checks skipped; schedule + pending-draft checks still apply`
77
+ });
78
+ }
79
+ const explicitTo = parseAddresses(site.reportRecipientsTo);
80
+ const fallbackTo = parseAddresses(site.pointOfContact);
81
+ const to = scheduledForType ? explicitTo ?? fallbackTo ?? [] : [];
82
+ if (scheduledForType && to.length === 0) {
83
+ findings.push({
84
+ level: "fail",
85
+ check: "recipients-missing",
86
+ message: "no recipients \u2014 Report recipients (To) and point of contact are both empty; the send will throw"
87
+ });
88
+ }
89
+ for (const addr of to) {
90
+ if (!isProbablyEmail(addr)) {
91
+ findings.push({
92
+ level: "fail",
93
+ check: "recipients-malformed",
94
+ message: `recipient '${addr}' is malformed \u2014 use bare addresses only in Report recipients (To) / point of contact`
95
+ });
96
+ }
97
+ }
98
+ const cc = scheduledForType ? parseAddresses(site.reportRecipientsCc) ?? [] : [];
99
+ for (const addr of cc) {
100
+ if (!isProbablyEmail(addr)) {
101
+ findings.push({
102
+ level: "fail",
103
+ check: "recipients-malformed",
104
+ message: `CC '${addr}' is malformed \u2014 fix Report recipients (CC)`
105
+ });
106
+ }
107
+ }
108
+ if (!isOperatorSite(site)) {
109
+ const operatorAddrs = to.filter((a) => OPERATOR_DOMAINS.includes(domainOf(a)));
110
+ if (operatorAddrs.length > 0) {
111
+ const allOperator = operatorAddrs.length === to.length;
112
+ findings.push({
113
+ level: "warn",
114
+ check: "recipient-operator-address",
115
+ message: allOperator ? `resolved To is ONLY operator address(es) ${operatorAddrs.join(", ")} \u2014 probably a test-send leftover; the client will NOT receive this report` : `resolved To includes operator address(es) ${operatorAddrs.join(", ")} alongside the client's \u2014 confirm that's intentional (the ops inbox is already CC'd on every send)`
116
+ });
117
+ }
118
+ }
119
+ if (scheduledForType && explicitTo && fallbackTo) {
120
+ const a = [...explicitTo].map((x) => x.toLowerCase()).sort().join(",");
121
+ const b = [...fallbackTo].map((x) => x.toLowerCase()).sort().join(",");
122
+ if (a !== b) {
123
+ findings.push({
124
+ level: "info",
125
+ check: "to-override-shadows-contact",
126
+ message: `Report recipients (To) [${explicitTo.join(", ")}] overrides point of contact [${fallbackTo.join(", ")}] \u2014 the contact won't receive this unless also listed`
127
+ });
128
+ }
129
+ }
130
+ if (scheduledForType && !site.headerImage) {
131
+ findings.push({
132
+ level: "fail",
133
+ check: "header-image-missing",
134
+ message: "no Header image on the Websites row \u2014 the send will throw"
135
+ });
136
+ } else if (scheduledForType && site.headerImage && !site.headerImage.type.startsWith("image/")) {
137
+ findings.push({
138
+ level: "fail",
139
+ check: "header-image-not-image",
140
+ message: `Header image attachment is '${site.headerImage.type}' (${site.headerImage.filename}) \u2014 not a decodable image; the send throws in prepareHeaderImage`
141
+ });
142
+ }
143
+ const scores = [site.pScore, site.rScore, site.bpScore, site.seoScore];
144
+ if (scheduledForType && scores.some((s) => s === null)) {
145
+ if (type === "Announcement") {
146
+ findings.push({
147
+ level: "warn",
148
+ check: "scores-missing",
149
+ message: "Websites row is missing Lighthouse scores \u2014 `announce` will skip this site; run `audit lighthouse --write-airtable` first"
150
+ });
151
+ } else {
152
+ findings.push({
153
+ level: "fail",
154
+ check: "scores-missing",
155
+ message: "Websites row is missing Lighthouse scores \u2014 drafting will throw (scoresFromWebsite); run `audit lighthouse --write-airtable` first"
156
+ });
157
+ }
158
+ }
159
+ if (type === "Announcement" && site.status !== "maintenance") {
160
+ findings.push({
161
+ level: "info",
162
+ check: "status-not-maintenance",
163
+ message: `status is '${site.status ?? "(blank)"}' \u2014 \`announce --all\` only drafts for maintenance sites`
164
+ });
165
+ }
166
+ const pending = reports.filter((r) => r.draftReady && r.sentAt === null);
167
+ if (pending.length > 0) {
168
+ const currentPeriod = reportPeriodKey(now);
169
+ const isExpectedPayload = (r) => type !== "Announcement" && r.reportType === type && r.period === currentPeriod;
170
+ const expected = pending.filter(isExpectedPayload);
171
+ const stale = pending.filter((r) => !isExpectedPayload(r));
172
+ if (expected.length > 0) {
173
+ findings.push({
174
+ level: "info",
175
+ check: "pending-drafts",
176
+ message: `current-cycle ${type} draft is queued${expected[0].approvedToSend ? " and approved" : " (awaiting approval)"} \u2014 this is the report \`--send-ready\` will deliver`
177
+ });
178
+ }
179
+ if (stale.length > 0) {
180
+ const summary = stale.map(
181
+ (r) => `${r.reportType} ${r.period ?? "(no period)"}${r.approvedToSend ? " [APPROVED]" : ""}`
182
+ ).join(", ");
183
+ findings.push({
184
+ level: "warn",
185
+ check: "pending-drafts",
186
+ message: `${stale.length} unsent draft(s) queued: ${summary} \u2014 resolve before sending so the client doesn't get the wrong report first`
187
+ });
188
+ }
189
+ }
190
+ checkFrequency(site, "maintenance", reports, findings, now);
191
+ checkFrequency(site, "testing", reports, findings, now);
192
+ if (type === "Announcement") {
193
+ const anySent = lastSentForType(reports, "Maintenance") ?? lastSentForType(reports, "Testing");
194
+ if (!site.maintenanceDay && !site.testingDay && !anySent) {
195
+ findings.push({
196
+ level: "info",
197
+ check: "anchor-missing",
198
+ message: "no maintenance/testing day anchors and nothing ever sent \u2014 set both anchors to the announcement send date (the Sonder playbook) so recurrence starts from the announcement"
199
+ });
200
+ }
201
+ }
202
+ return { site: site.name, findings };
203
+ }
204
+ function preflightFleet(sites) {
205
+ if (sites.length < 3) return [];
206
+ const findings = [];
207
+ const allEmpty = (get, column) => {
208
+ if (sites.every((s) => get(s) === null || get(s) === "")) {
209
+ findings.push({
210
+ level: "warn",
211
+ check: "column-possibly-renamed",
212
+ message: `'${column}' is empty on all ${sites.length} selected sites \u2014 if that column was renamed in Airtable the code reads null silently; verify the column name`
213
+ });
214
+ }
215
+ };
216
+ allEmpty((s) => s.pointOfContact, "point of contact");
217
+ allEmpty((s) => s.maintenanceFreqRaw, "maintenence freq");
218
+ allEmpty((s) => s.headerImage, "Header image");
219
+ const byContact = /* @__PURE__ */ new Map();
220
+ for (const s of sites) {
221
+ const to = parseAddresses(s.reportRecipientsTo) ?? parseAddresses(s.pointOfContact) ?? [];
222
+ for (const addr of to) {
223
+ const key = addr.toLowerCase();
224
+ byContact.set(key, [...byContact.get(key) ?? [], s.name]);
225
+ }
226
+ }
227
+ for (const [addr, names] of byContact) {
228
+ if (names.length > 1 && !OPERATOR_DOMAINS.includes(domainOf(addr))) {
229
+ findings.push({
230
+ level: "info",
231
+ check: "duplicate-contact",
232
+ message: `${names.join(" and ")} resolve to the same recipient (${addr}) \u2014 fine if one person owns both; otherwise a copy-paste error on one row`
233
+ });
234
+ }
235
+ }
236
+ return findings;
237
+ }
238
+ async function preflight(deps) {
239
+ const base = deps?.base ?? openBase(readAirtableConfig());
240
+ const type = deps?.type ?? "Announcement";
241
+ const now = deps?.now ?? /* @__PURE__ */ new Date();
242
+ const websites = await listWebsites(base);
243
+ const selected = deps?.site ? websites.filter((w) => siteSlug(w.name) === siteSlug(deps.site)) : type === "Announcement" ? websites.filter((w) => w.status === "maintenance") : websites.filter((w) => w.status === null || ELIGIBLE_STATUSES.has(w.status));
244
+ const allReports = selected.length > 0 ? await listAllReports(base) : [];
245
+ const results = selected.map(
246
+ (site) => preflightSite(
247
+ site,
248
+ allReports.filter((r) => r.siteId === site.id),
249
+ type,
250
+ now
251
+ )
252
+ );
253
+ return { results, fleet: deps?.site ? [] : preflightFleet(selected) };
254
+ }
255
+ function approveBlockers(site, report) {
256
+ const findings = [];
257
+ const explicitTo = parseAddresses(site.reportRecipientsTo);
258
+ const fallbackTo = parseAddresses(site.pointOfContact);
259
+ const to = explicitTo ?? fallbackTo ?? [];
260
+ if (to.length === 0) {
261
+ findings.push({
262
+ level: "fail",
263
+ check: "recipients-missing",
264
+ message: "no recipients \u2014 Report recipients (To) and point of contact are both empty; the send will throw"
265
+ });
266
+ }
267
+ for (const addr of to) {
268
+ if (!isProbablyEmail(addr)) {
269
+ findings.push({
270
+ level: "fail",
271
+ check: "recipients-malformed",
272
+ message: `recipient '${addr}' is malformed \u2014 fix Report recipients (To) / point of contact in Airtable`
273
+ });
274
+ }
275
+ }
276
+ for (const addr of parseAddresses(site.reportRecipientsCc) ?? []) {
277
+ if (!isProbablyEmail(addr)) {
278
+ findings.push({
279
+ level: "fail",
280
+ check: "recipients-malformed",
281
+ message: `CC '${addr}' is malformed \u2014 fix Report recipients (CC) in Airtable`
282
+ });
283
+ }
284
+ }
285
+ if (!site.headerImage) {
286
+ findings.push({
287
+ level: "fail",
288
+ check: "header-image-missing",
289
+ message: "no Header image on the Websites row \u2014 the send will throw"
290
+ });
291
+ } else if (!site.headerImage.type.startsWith("image/")) {
292
+ findings.push({
293
+ level: "fail",
294
+ check: "header-image-not-image",
295
+ message: `Header image attachment is '${site.headerImage.type}' (${site.headerImage.filename}) \u2014 not a decodable image; the send throws in prepareHeaderImage`
296
+ });
297
+ }
298
+ if (report.lighthouse === null) {
299
+ findings.push({
300
+ level: "fail",
301
+ check: "report-scores-missing",
302
+ message: "the Reports row has no Lighthouse scores (one blank/non-numeric cell nulls all four) \u2014 the send will throw"
303
+ });
304
+ }
305
+ if (!isOperatorSite(site)) {
306
+ const operatorAddrs = to.filter((a) => OPERATOR_DOMAINS.includes(domainOf(a)));
307
+ if (operatorAddrs.length > 0 && operatorAddrs.length === to.length) {
308
+ findings.push({
309
+ level: "warn",
310
+ check: "recipient-operator-address",
311
+ message: `resolved To is ONLY operator address(es) ${operatorAddrs.join(", ")} \u2014 the client will NOT receive this report`
312
+ });
313
+ }
314
+ }
315
+ return findings;
316
+ }
317
+
318
+ export {
319
+ preflight,
320
+ approveBlockers
321
+ };
322
+ //# sourceMappingURL=chunk-MAYHLXCS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/preflight.ts"],"sourcesContent":["import { openBase, readAirtableConfig } from \"./airtable/client.js\";\nimport type { AirtableBase } from \"./airtable/client.js\";\nimport { listWebsites, siteSlug } from \"./airtable/websites.js\";\nimport type { WebsiteRow } from \"./airtable/websites.js\";\nimport { listAllReports } from \"./airtable/reports.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { parseAddresses, isProbablyEmail } from \"./send/orchestrate.js\";\nimport { ELIGIBLE_STATUSES, reportPeriodKey } from \"./due.js\";\nimport type { ReportType } from \"./types.js\";\n\nexport type PreflightLevel = \"fail\" | \"warn\" | \"info\";\n\nexport type PreflightFinding = {\n level: PreflightLevel;\n /** Stable machine-readable check id, e.g. \"recipients-missing\". */\n check: string;\n message: string;\n};\n\nexport type PreflightSiteResult = {\n site: string;\n findings: PreflightFinding[];\n};\n\n/** Domains whose addresses are the OPERATOR's, not a client's. A client site whose\n * resolved To contains one of these is almost always a test-send leftover — the\n * exact misconfiguration that would silently divert a client announcement. */\nconst OPERATOR_DOMAINS = [\"reddoorla.com\", \"tuckerlemos.com\"];\n\n/** Anchors older than this, WHEN the anchor is actually the schedule's base date\n * (no newer Sent-at — see nextDueDate's `lastSent ?? fallback`), mean the next\n * `report --due` run drafts a surprise back-dated report. */\nconst STALE_ANCHOR_DAYS = 396; // ~13 months\n\nfunction domainOf(addr: string): string {\n const at = addr.lastIndexOf(\"@\");\n return at === -1 ? \"\" : addr.slice(at + 1).toLowerCase();\n}\n\nfunction isOperatorSite(site: WebsiteRow): boolean {\n if (!site.url) return false;\n try {\n // Airtable `url` cells aren't guaranteed a scheme; retry with one before giving up.\n const host = new URL(\n /^[a-z][a-z0-9+.-]*:\\/\\//i.test(site.url) ? site.url : `https://${site.url}`,\n ).hostname.toLowerCase();\n return OPERATOR_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));\n } catch {\n return false;\n }\n}\n\nfunction lastSentForType(reports: ReportRow[], type: ReportType): string | null {\n const sent = reports\n .filter((r) => r.reportType === type && r.sentAt !== null)\n .map((r) => r.sentAt!)\n .sort();\n return sent[sent.length - 1] ?? null;\n}\n\nfunction checkFrequency(\n site: WebsiteRow,\n which: \"maintenance\" | \"testing\",\n reports: ReportRow[],\n findings: PreflightFinding[],\n now: Date,\n): void {\n // Validate the RAW Airtable cell, not the coerced Frequency: toFrequency maps any\n // unrecognized value (typo, trailing space, renamed option) to \"None\", so by the\n // time WebsiteRow is built the damage is invisible — the site just silently drops\n // off the schedule. This is the loud version.\n const raw = which === \"maintenance\" ? site.maintenanceFreqRaw : site.testingFreqRaw;\n const coerced = which === \"maintenance\" ? site.maintenanceFreq : site.testingFreq;\n if (raw !== null && raw !== \"\" && coerced === \"None\" && raw.trim() !== \"None\") {\n findings.push({\n level: \"fail\",\n check: \"frequency-unrecognized\",\n message: `${which} frequency cell is '${raw}' — not an exact Monthly/Quarterly/Yearly/None, so the scheduler silently treats it as None and the site drops off the calendar; fix the Airtable value`,\n });\n return;\n }\n if (coerced === \"None\") return;\n\n const anchor = which === \"maintenance\" ? site.maintenanceDay : site.testingDay;\n if (!anchor) return;\n // nextDueDate uses `lastSent ?? anchor`: once anything has been sent, the anchor\n // is inert and an old one is normal on every healthy mature site — only warn when\n // the anchor is genuinely the base date the scheduler would use.\n const type: ReportType = which === \"maintenance\" ? \"Maintenance\" : \"Testing\";\n const lastSent = lastSentForType(reports, type);\n if (lastSent && new Date(lastSent).getTime() >= new Date(anchor).getTime()) return;\n const ageDays = (now.getTime() - new Date(anchor).getTime()) / 86_400_000;\n if (ageDays > STALE_ANCHOR_DAYS) {\n findings.push({\n level: \"warn\",\n check: \"anchor-stale\",\n message: `${which} day anchor is ${anchor} (>13 months old) and nothing newer has been sent — \\`report --due\\` will draft a back-dated overdue report; reset the anchor or clear the schedule`,\n });\n }\n}\n\n/**\n * Pure per-site preflight: every check that, left unfixed, makes a report send fail,\n * go to the wrong inbox, or surprise the operator. Read-only over data already\n * fetched; no Airtable handle, no network — trivially testable.\n *\n * Mirrors the send/draft-time validation (recipients + header image in\n * send/orchestrate.ts, Websites-row scores in draft.ts) so problems surface BEFORE\n * draft/approve instead of exploding at `report --send-ready`, and adds the checks\n * send time cannot do: operator-address leftovers, To-override shadowing,\n * pending-draft races, schedule hygiene against the RAW Airtable values.\n */\nexport function preflightSite(\n site: WebsiteRow,\n reports: ReportRow[],\n type: ReportType,\n now: Date,\n): PreflightSiteResult {\n const findings: PreflightFinding[] = [];\n\n // A site that isn't on the checked calendar never drafts, so its send\n // requirements (recipients / header image / scores) can't hurt anyone —\n // checking them anyway would drown the fleet run in fails for legacy hosting\n // rows. Frequency hygiene and pending-draft checks still run: a typo'd cell\n // is exactly HOW a site falls off the calendar, and an already-drafted report\n // can still be approved + sent regardless of schedule.\n const scheduledForType =\n type === \"Announcement\"\n ? true\n : type === \"Maintenance\"\n ? site.maintenanceFreq !== \"None\"\n : site.testingFreq !== \"None\";\n if (!scheduledForType) {\n findings.push({\n level: \"info\",\n check: \"not-scheduled\",\n message: `no ${type} schedule (frequency is None/blank) — send-requirement checks skipped; schedule + pending-draft checks still apply`,\n });\n }\n\n // --- Recipients: same resolution order as the send path (To override, else contact).\n const explicitTo = parseAddresses(site.reportRecipientsTo);\n const fallbackTo = parseAddresses(site.pointOfContact);\n const to = scheduledForType ? (explicitTo ?? fallbackTo ?? []) : [];\n if (scheduledForType && to.length === 0) {\n findings.push({\n level: \"fail\",\n check: \"recipients-missing\",\n message:\n \"no recipients — Report recipients (To) and point of contact are both empty; the send will throw\",\n });\n }\n for (const addr of to) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `recipient '${addr}' is malformed — use bare addresses only in Report recipients (To) / point of contact`,\n });\n }\n }\n const cc = scheduledForType ? (parseAddresses(site.reportRecipientsCc) ?? []) : [];\n for (const addr of cc) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `CC '${addr}' is malformed — fix Report recipients (CC)`,\n });\n }\n }\n if (!isOperatorSite(site)) {\n const operatorAddrs = to.filter((a) => OPERATOR_DOMAINS.includes(domainOf(a)));\n if (operatorAddrs.length > 0) {\n const allOperator = operatorAddrs.length === to.length;\n findings.push({\n level: \"warn\",\n check: \"recipient-operator-address\",\n message: allOperator\n ? `resolved To is ONLY operator address(es) ${operatorAddrs.join(\", \")} — probably a test-send leftover; the client will NOT receive this report`\n : `resolved To includes operator address(es) ${operatorAddrs.join(\", \")} alongside the client's — confirm that's intentional (the ops inbox is already CC'd on every send)`,\n });\n }\n }\n if (scheduledForType && explicitTo && fallbackTo) {\n const a = [...explicitTo]\n .map((x) => x.toLowerCase())\n .sort()\n .join(\",\");\n const b = [...fallbackTo]\n .map((x) => x.toLowerCase())\n .sort()\n .join(\",\");\n if (a !== b) {\n findings.push({\n level: \"info\",\n check: \"to-override-shadows-contact\",\n message: `Report recipients (To) [${explicitTo.join(\", \")}] overrides point of contact [${fallbackTo.join(\", \")}] — the contact won't receive this unless also listed`,\n });\n }\n }\n\n // --- Hard requirements of the draft/send paths, surfaced early.\n if (scheduledForType && !site.headerImage) {\n findings.push({\n level: \"fail\",\n check: \"header-image-missing\",\n message: \"no Header image on the Websites row — the send will throw\",\n });\n } else if (scheduledForType && site.headerImage && !site.headerImage.type.startsWith(\"image/\")) {\n findings.push({\n level: \"fail\",\n check: \"header-image-not-image\",\n message: `Header image attachment is '${site.headerImage.type}' (${site.headerImage.filename}) — not a decodable image; the send throws in prepareHeaderImage`,\n });\n }\n const scores = [site.pScore, site.rScore, site.bpScore, site.seoScore];\n if (scheduledForType && scores.some((s) => s === null)) {\n if (type === \"Announcement\") {\n // announce() skips score-less sites (skipped-no-scores) rather than throwing.\n findings.push({\n level: \"warn\",\n check: \"scores-missing\",\n message:\n \"Websites row is missing Lighthouse scores — `announce` will skip this site; run `audit lighthouse --write-airtable` first\",\n });\n } else {\n // report <site> / report --due HARD-THROW here (scoresFromWebsite in draft.ts).\n findings.push({\n level: \"fail\",\n check: \"scores-missing\",\n message:\n \"Websites row is missing Lighthouse scores — drafting will throw (scoresFromWebsite); run `audit lighthouse --write-airtable` first\",\n });\n }\n }\n if (type === \"Announcement\" && site.status !== \"maintenance\") {\n findings.push({\n level: \"info\",\n check: \"status-not-maintenance\",\n message: `status is '${site.status ?? \"(blank)\"}' — \\`announce --all\\` only drafts for maintenance sites`,\n });\n }\n\n // --- Pending drafts. For an Announcement these always race (the queue tier rule\n // supersedes or blocks). For Maintenance/Testing, the CURRENT cycle's own draft\n // sitting ready on send day IS the payload — that's the steady state, not a\n // problem — so only genuinely stale/foreign drafts get the warn.\n const pending = reports.filter((r) => r.draftReady && r.sentAt === null);\n if (pending.length > 0) {\n const currentPeriod = reportPeriodKey(now);\n const isExpectedPayload = (r: ReportRow): boolean =>\n type !== \"Announcement\" && r.reportType === type && r.period === currentPeriod;\n const expected = pending.filter(isExpectedPayload);\n const stale = pending.filter((r) => !isExpectedPayload(r));\n if (expected.length > 0) {\n findings.push({\n level: \"info\",\n check: \"pending-drafts\",\n message: `current-cycle ${type} draft is queued${expected[0]!.approvedToSend ? \" and approved\" : \" (awaiting approval)\"} — this is the report \\`--send-ready\\` will deliver`,\n });\n }\n if (stale.length > 0) {\n const summary = stale\n .map(\n (r) =>\n `${r.reportType} ${r.period ?? \"(no period)\"}${r.approvedToSend ? \" [APPROVED]\" : \"\"}`,\n )\n .join(\", \");\n findings.push({\n level: \"warn\",\n check: \"pending-drafts\",\n message: `${stale.length} unsent draft(s) queued: ${summary} — resolve before sending so the client doesn't get the wrong report first`,\n });\n }\n }\n\n // --- Schedule hygiene (validates the RAW Airtable frequency cells).\n checkFrequency(site, \"maintenance\", reports, findings, now);\n checkFrequency(site, \"testing\", reports, findings, now);\n if (type === \"Announcement\") {\n const anySent = lastSentForType(reports, \"Maintenance\") ?? lastSentForType(reports, \"Testing\");\n if (!site.maintenanceDay && !site.testingDay && !anySent) {\n findings.push({\n level: \"info\",\n check: \"anchor-missing\",\n message:\n \"no maintenance/testing day anchors and nothing ever sent — set both anchors to the announcement send date (the Sonder playbook) so recurrence starts from the announcement\",\n });\n }\n }\n\n return { site: site.name, findings };\n}\n\n/**\n * Fleet-level heuristics that need the whole selection. Airtable column renames\n * don't error — the mapper reads null forever (its own header comment admits this).\n * A load-bearing column empty on EVERY selected site is far more likely a rename\n * than N coincidences; say so. Likewise two different sites resolving to one\n * recipient is worth one eyeball (it can be legitimate — same owner, two sites).\n */\nexport function preflightFleet(sites: WebsiteRow[]): PreflightFinding[] {\n if (sites.length < 3) return []; // too few rows to distinguish rename from data\n const findings: PreflightFinding[] = [];\n const allEmpty = (get: (s: WebsiteRow) => unknown, column: string): void => {\n if (sites.every((s) => get(s) === null || get(s) === \"\")) {\n findings.push({\n level: \"warn\",\n check: \"column-possibly-renamed\",\n message: `'${column}' is empty on all ${sites.length} selected sites — if that column was renamed in Airtable the code reads null silently; verify the column name`,\n });\n }\n };\n allEmpty((s) => s.pointOfContact, \"point of contact\");\n allEmpty((s) => s.maintenanceFreqRaw, \"maintenence freq\");\n allEmpty((s) => s.headerImage, \"Header image\");\n\n const byContact = new Map<string, string[]>();\n for (const s of sites) {\n const to = parseAddresses(s.reportRecipientsTo) ?? parseAddresses(s.pointOfContact) ?? [];\n for (const addr of to) {\n const key = addr.toLowerCase();\n byContact.set(key, [...(byContact.get(key) ?? []), s.name]);\n }\n }\n for (const [addr, names] of byContact) {\n if (names.length > 1 && !OPERATOR_DOMAINS.includes(domainOf(addr))) {\n findings.push({\n level: \"info\",\n check: \"duplicate-contact\",\n message: `${names.join(\" and \")} resolve to the same recipient (${addr}) — fine if one person owns both; otherwise a copy-paste error on one row`,\n });\n }\n }\n return findings;\n}\n\nexport type PreflightDeps = {\n /** Airtable handle. Defaults to opening the live base from credentials. */\n base?: AirtableBase;\n /** Slug of a single site (matched via siteSlug). Mutually exclusive with `all`. */\n site?: string;\n /** Fleet mode: for Announcement, maintenance-status sites (announce's own filter);\n * for Maintenance/Testing, everything `report --due` schedules — ELIGIBLE_STATUSES\n * plus null-status rows (due.ts treats null as active for legacy rows). */\n all?: boolean;\n type?: ReportType;\n now?: Date;\n};\n\nexport type PreflightResult = {\n results: PreflightSiteResult[];\n fleet: PreflightFinding[];\n};\n\n/**\n * Read-only rollout preflight over the live Airtable base. One Websites fetch, one\n * Reports fetch (the --due path does the same for the same rate-limit reason), then\n * {@link preflightSite} per selected site plus {@link preflightFleet} across the\n * selection. NEVER writes and NEVER sends.\n */\nexport async function preflight(deps?: PreflightDeps): Promise<PreflightResult> {\n const base = deps?.base ?? openBase(readAirtableConfig());\n const type: ReportType = deps?.type ?? \"Announcement\";\n const now = deps?.now ?? new Date();\n\n const websites = await listWebsites(base);\n const selected = deps?.site\n ? websites.filter((w) => siteSlug(w.name) === siteSlug(deps.site!))\n : type === \"Announcement\"\n ? websites.filter((w) => w.status === \"maintenance\")\n : websites.filter((w) => w.status === null || ELIGIBLE_STATUSES.has(w.status));\n\n const allReports = selected.length > 0 ? await listAllReports(base) : [];\n const results = selected.map((site) =>\n preflightSite(\n site,\n allReports.filter((r) => r.siteId === site.id),\n type,\n now,\n ),\n );\n return { results, fleet: deps?.site ? [] : preflightFleet(selected) };\n}\n\n/**\n * The send-blocking subset for ONE report, at approve time: exactly the\n * conditions that make `sendOne` throw (no recipients, malformed To/CC, no\n * header image, no report-level Lighthouse scores) plus the wrong-inbox warn\n * (operator address resolved as a client's To). PURE — the approve gate, the\n * dashboard's pending-row chip, and the digest collector all call this one\n * function so \"approvable\" can't drift from \"sendable\".\n *\n * Deliberately narrower than {@link preflightSite}: schedule hygiene and\n * pending-draft races are fleet/site concerns — blocking THIS report's approval\n * on them would be over-gating.\n */\nexport function approveBlockers(site: WebsiteRow, report: ReportRow): PreflightFinding[] {\n const findings: PreflightFinding[] = [];\n\n const explicitTo = parseAddresses(site.reportRecipientsTo);\n const fallbackTo = parseAddresses(site.pointOfContact);\n const to = explicitTo ?? fallbackTo ?? [];\n if (to.length === 0) {\n findings.push({\n level: \"fail\",\n check: \"recipients-missing\",\n message:\n \"no recipients — Report recipients (To) and point of contact are both empty; the send will throw\",\n });\n }\n for (const addr of to) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `recipient '${addr}' is malformed — fix Report recipients (To) / point of contact in Airtable`,\n });\n }\n }\n for (const addr of parseAddresses(site.reportRecipientsCc) ?? []) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `CC '${addr}' is malformed — fix Report recipients (CC) in Airtable`,\n });\n }\n }\n if (!site.headerImage) {\n findings.push({\n level: \"fail\",\n check: \"header-image-missing\",\n message: \"no Header image on the Websites row — the send will throw\",\n });\n } else if (!site.headerImage.type.startsWith(\"image/\")) {\n findings.push({\n level: \"fail\",\n check: \"header-image-not-image\",\n message: `Header image attachment is '${site.headerImage.type}' (${site.headerImage.filename}) — not a decodable image; the send throws in prepareHeaderImage`,\n });\n }\n // sendOne throws on a null report.lighthouse (one blank/non-numeric cell nulls\n // all four) — the REPORT's snapshot, not the Websites-row scores.\n if (report.lighthouse === null) {\n findings.push({\n level: \"fail\",\n check: \"report-scores-missing\",\n message:\n \"the Reports row has no Lighthouse scores (one blank/non-numeric cell nulls all four) — the send will throw\",\n });\n }\n if (!isOperatorSite(site)) {\n const operatorAddrs = to.filter((a) => OPERATOR_DOMAINS.includes(domainOf(a)));\n if (operatorAddrs.length > 0 && operatorAddrs.length === to.length) {\n findings.push({\n level: \"warn\",\n check: \"recipient-operator-address\",\n message: `resolved To is ONLY operator address(es) ${operatorAddrs.join(\", \")} — the client will NOT receive this report`,\n });\n }\n }\n return findings;\n}\n\n/** Convenience: just the fail-level blockers, formatted for gate messages. */\nexport function formatBlockers(findings: PreflightFinding[]): string[] {\n return findings.filter((f) => f.level === \"fail\").map((f) => `${f.check}: ${f.message}`);\n}\n"],"mappings":"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","names":[]}
@@ -1,23 +1,21 @@
1
1
  import {
2
2
  queueDraft
3
- } from "./chunk-6LIWDIXU.js";
3
+ } from "./chunk-RINXPU3H.js";
4
4
  import {
5
+ checklistFor,
6
+ createDraft,
7
+ listReportsForSite,
5
8
  renderReportHtml,
6
9
  resolveCopy,
7
10
  uploadAttachment
8
- } from "./chunk-SYTPVYIK.js";
9
- import {
10
- checklistFor,
11
- createDraft,
12
- listReportsForSite
13
- } from "./chunk-OCS7SZUZ.js";
11
+ } from "./chunk-6JFOAPW4.js";
14
12
  import {
15
13
  isNetlifyAppUrl
16
14
  } from "./chunk-U5SZ4FOP.js";
17
15
  import {
18
16
  siteSlug,
19
17
  updateAnalyticsHealth
20
- } from "./chunk-4P6UQG5L.js";
18
+ } from "./chunk-C3PDS623.js";
21
19
  import {
22
20
  defaultCredentialsPath
23
21
  } from "./chunk-6VAL7XJT.js";
@@ -441,4 +439,4 @@ export {
441
439
  fetchGaUsers,
442
440
  fetchSearch
443
441
  };
444
- //# sourceMappingURL=chunk-G35B5U6F.js.map
442
+ //# sourceMappingURL=chunk-O6G4HHMF.js.map