@reddoorla/maintenance 0.85.2 → 0.86.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 (245) hide show
  1. package/README.md +8 -0
  2. package/dist/airtable-G7WBQQMI.js +9 -0
  3. package/dist/{announce-GIAYN67S.js → announce-MXTUQ6C7.js} +26 -19
  4. package/dist/announce-MXTUQ6C7.js.map +1 -0
  5. package/dist/{bump-deps-SWJB6H5V.js → bump-deps-EC74CPLS.js} +9 -9
  6. package/dist/chunk-2EVEFOZY.js +129 -0
  7. package/dist/chunk-2EVEFOZY.js.map +1 -0
  8. package/dist/{chunk-COZPL62X.js → chunk-2NDRHXMR.js} +25 -17
  9. package/dist/chunk-2NDRHXMR.js.map +1 -0
  10. package/dist/{chunk-SDUJIANJ.js → chunk-2NJQLSEU.js} +3 -3
  11. package/dist/chunk-2NQUCS7O.js +243 -0
  12. package/dist/chunk-2NQUCS7O.js.map +1 -0
  13. package/dist/{chunk-DTTIFYAM.js → chunk-2ZPT7GLV.js} +42 -403
  14. package/dist/chunk-2ZPT7GLV.js.map +1 -0
  15. package/dist/{chunk-ZTPPCHDB.js → chunk-3G25KIWW.js} +4 -4
  16. package/dist/chunk-3SF7D4DY.js +359 -0
  17. package/dist/chunk-3SF7D4DY.js.map +1 -0
  18. package/dist/{chunk-Q276MZ7E.js → chunk-3VQWSE3F.js} +4 -4
  19. package/dist/{chunk-37S6L6G4.js → chunk-5UINJNAJ.js} +72 -2
  20. package/dist/chunk-5UINJNAJ.js.map +1 -0
  21. package/dist/{chunk-THEFTZ7F.js → chunk-62CMMGEO.js} +5 -5
  22. package/dist/{chunk-FDIFTIXC.js → chunk-6TWWY7ZU.js} +2 -2
  23. package/dist/{chunk-UYCAF77E.js → chunk-7RJ2HMMJ.js} +3 -3
  24. package/dist/chunk-ACBRLLFT.js +198 -0
  25. package/dist/chunk-ACBRLLFT.js.map +1 -0
  26. package/dist/chunk-BNM5POSJ.js +284 -0
  27. package/dist/chunk-BNM5POSJ.js.map +1 -0
  28. package/dist/{chunk-R6FPFMOS.js → chunk-BQRFQSGY.js} +3 -3
  29. package/dist/{chunk-MGR2H7F4.js → chunk-BYK66EWO.js} +19 -12
  30. package/dist/chunk-BYK66EWO.js.map +1 -0
  31. package/dist/{chunk-6QRLRT3P.js → chunk-CQR4LIM7.js} +12 -265
  32. package/dist/chunk-CQR4LIM7.js.map +1 -0
  33. package/dist/{chunk-SEEA6RXF.js → chunk-DJZWYIPP.js} +2 -2
  34. package/dist/chunk-DJZWYIPP.js.map +1 -0
  35. package/dist/chunk-EVBCK2KC.js +68 -0
  36. package/dist/chunk-EVBCK2KC.js.map +1 -0
  37. package/dist/{chunk-TP5M2NQB.js → chunk-F5KM4LEQ.js} +3 -3
  38. package/dist/{chunk-62VF32IC.js → chunk-ILUNZHYV.js} +2 -2
  39. package/dist/chunk-ITIOFN7A.js +396 -0
  40. package/dist/chunk-ITIOFN7A.js.map +1 -0
  41. package/dist/{chunk-LS6QPANO.js → chunk-IYSEC543.js} +2 -2
  42. package/dist/{chunk-DC5H7KEQ.js → chunk-JHGHLKFS.js} +2 -2
  43. package/dist/{chunk-PBWDDDQH.js → chunk-KTH5ZODA.js} +2 -2
  44. package/dist/{chunk-4DHQCN5H.js → chunk-LBYOLBW7.js} +17 -2
  45. package/dist/chunk-LBYOLBW7.js.map +1 -0
  46. package/dist/{chunk-577HCGZO.js → chunk-LDWZSLSU.js} +38 -23
  47. package/dist/chunk-LDWZSLSU.js.map +1 -0
  48. package/dist/{chunk-LTB6KVJW.js → chunk-N45M3FDK.js} +13 -11
  49. package/dist/{chunk-LTB6KVJW.js.map → chunk-N45M3FDK.js.map} +1 -1
  50. package/dist/chunk-NYUYNYQL.js +48 -0
  51. package/dist/chunk-NYUYNYQL.js.map +1 -0
  52. package/dist/{chunk-7J53SUT6.js → chunk-P2FE5AI6.js} +4 -4
  53. package/dist/{chunk-3YXDSCM6.js → chunk-PPE5KZJK.js} +29 -11
  54. package/dist/chunk-PPE5KZJK.js.map +1 -0
  55. package/dist/chunk-PTDMVGKL.js +31 -0
  56. package/dist/chunk-PTDMVGKL.js.map +1 -0
  57. package/dist/{chunk-D4FVAIEJ.js → chunk-PZR4EBPY.js} +5 -2
  58. package/dist/chunk-PZR4EBPY.js.map +1 -0
  59. package/dist/{chunk-NWB4O5EY.js → chunk-SLUQ7R6I.js} +3 -3
  60. package/dist/chunk-SLUQ7R6I.js.map +1 -0
  61. package/dist/{chunk-GZNBCHAZ.js → chunk-TERE5SDF.js} +4 -4
  62. package/dist/{chunk-ZMIPG6TR.js → chunk-TVL5MVUF.js} +4 -4
  63. package/dist/chunk-UDRHAQT7.js +172 -0
  64. package/dist/chunk-UDRHAQT7.js.map +1 -0
  65. package/dist/chunk-VAD43MOR.js +72 -0
  66. package/dist/chunk-VAD43MOR.js.map +1 -0
  67. package/dist/chunk-VSBFM7DS.js +229 -0
  68. package/dist/chunk-VSBFM7DS.js.map +1 -0
  69. package/dist/chunk-W7Z3BGCI.js +274 -0
  70. package/dist/chunk-W7Z3BGCI.js.map +1 -0
  71. package/dist/chunk-X3B723J4.js +1209 -0
  72. package/dist/chunk-X3B723J4.js.map +1 -0
  73. package/dist/{chunk-VRWUL4TR.js → chunk-XHHMM5HX.js} +2 -2
  74. package/dist/{chunk-RJB3QUSP.js → chunk-XIDUYLSR.js} +3 -3
  75. package/dist/{chunk-QG6OSL4M.js → chunk-XUGSZP7Z.js} +2 -2
  76. package/dist/{chunk-S56JQJ4F.js → chunk-Y53WOQIJ.js} +3 -3
  77. package/dist/{chunk-ICXV25N7.js → chunk-Z3KYFUN6.js} +3 -3
  78. package/dist/{chunk-MPBXOJ3N.js → chunk-Z7AXYKQB.js} +33 -2
  79. package/dist/chunk-Z7AXYKQB.js.map +1 -0
  80. package/dist/{chunk-WYYQK4D3.js → chunk-ZKXSX4LO.js} +11 -11
  81. package/dist/cli/bin.js +46 -28
  82. package/dist/cli/bin.js.map +1 -1
  83. package/dist/cli/commands/audit.d.ts +30 -1
  84. package/dist/cli/commands/audit.js +62 -30
  85. package/dist/cli/commands/audit.js.map +1 -1
  86. package/dist/client-AUE3XUPO.js +11 -0
  87. package/dist/{convert-to-pnpm-AT52NWZB.js → convert-to-pnpm-6L6P74WR.js} +9 -9
  88. package/dist/db-B35OGV3G.js +217 -0
  89. package/dist/db-B35OGV3G.js.map +1 -0
  90. package/dist/{digest-UVHYZUYE.js → digest-BL6UJYKH.js} +23 -20
  91. package/dist/{digest-UVHYZUYE.js.map → digest-BL6UJYKH.js.map} +1 -1
  92. package/dist/dump-P6EBSIOH.js +68 -0
  93. package/dist/dump-P6EBSIOH.js.map +1 -0
  94. package/dist/email-VTTTKKL5.js +195 -0
  95. package/dist/email-VTTTKKL5.js.map +1 -0
  96. package/dist/{ensure-site-S25MDDHW.js → ensure-site-DFTB7MN3.js} +8 -7
  97. package/dist/ensure-site-DFTB7MN3.js.map +1 -0
  98. package/dist/fleet-events-cF2Om88w.d.ts +24 -0
  99. package/dist/{forms-notify-target-TWN4EH6K.js → forms-notify-target-75U6OU7L.js} +38 -132
  100. package/dist/forms-notify-target-75U6OU7L.js.map +1 -0
  101. package/dist/{github-signals-G4ILZAJ6.js → github-signals-IJUPGCUX.js} +40 -19
  102. package/dist/github-signals-IJUPGCUX.js.map +1 -0
  103. package/dist/{header-image-T7XD37JL.js → header-image-AWLLQA4L.js} +34 -11
  104. package/dist/header-image-AWLLQA4L.js.map +1 -0
  105. package/dist/header-images-335L2AKB.js +68 -0
  106. package/dist/header-images-335L2AKB.js.map +1 -0
  107. package/dist/{health-endpoint-242WG2OJ.js → health-endpoint-WJC2UMYS.js} +9 -9
  108. package/dist/health-mirror-4B2L7T2Y.js +75 -0
  109. package/dist/health-mirror-4B2L7T2Y.js.map +1 -0
  110. package/dist/import-airtable-HE43FB4H.js +35 -0
  111. package/dist/index.d.ts +175 -21
  112. package/dist/index.js +115 -73
  113. package/dist/index.js.map +1 -1
  114. package/dist/{init-JN26NTWF.js → init-NJEOEO7I.js} +20 -17
  115. package/dist/{init-JN26NTWF.js.map → init-NJEOEO7I.js.map} +1 -1
  116. package/dist/{launch-JA7Z4CIM.js → launch-XNAX427U.js} +31 -25
  117. package/dist/{launch-JA7Z4CIM.js.map → launch-XNAX427U.js.map} +1 -1
  118. package/dist/lighthouse-VHI77PFB.js +11 -0
  119. package/dist/{lighthouse-airtable-BIPM26SZ.js → lighthouse-airtable-LBVIYYP5.js} +3 -3
  120. package/dist/mailchimp-XYCTEEJY.js +72 -0
  121. package/dist/mailchimp-XYCTEEJY.js.map +1 -0
  122. package/dist/migrate-GQH237A6.js +7 -0
  123. package/dist/notify-C645PGD5.js +22 -0
  124. package/dist/{onboard-53CITIM7.js → onboard-GK4PHPNI.js} +9 -9
  125. package/dist/orchestrate-77UO4BYJ.js +30 -0
  126. package/dist/orchestrate-77UO4BYJ.js.map +1 -0
  127. package/dist/parity-MWS24A3H.js +12 -0
  128. package/dist/parity-MWS24A3H.js.map +1 -0
  129. package/dist/pipeline-S6CVHDGB.js +12 -0
  130. package/dist/pipeline-S6CVHDGB.js.map +1 -0
  131. package/dist/{preflight-3XPDUHFJ.js → preflight-6WBNU2GZ.js} +16 -13
  132. package/dist/{preflight-3XPDUHFJ.js.map → preflight-6WBNU2GZ.js.map} +1 -1
  133. package/dist/{prismic-ci-UTQ6LRR3.js → prismic-ci-3RHOZUUT.js} +15 -15
  134. package/dist/{prismic-models-PVES6R5V.js → prismic-models-QBGUT2Q3.js} +10 -10
  135. package/dist/prospect-audit-IUNCJ2LV.js +174 -0
  136. package/dist/prospect-audit-IUNCJ2LV.js.map +1 -0
  137. package/dist/prospect-audits-3PMIO73D.js +19 -0
  138. package/dist/prospect-audits-3PMIO73D.js.map +1 -0
  139. package/dist/{protection-audit-64PLLBLI.js → protection-audit-SJO53MEQ.js} +3 -3
  140. package/dist/recipes/sync-configs.js +3 -3
  141. package/dist/render-AQAVQDPZ.js +10 -0
  142. package/dist/render-AQAVQDPZ.js.map +1 -0
  143. package/dist/{renovate-dispatch-ICJGSFCE.js → renovate-dispatch-QJY75ODR.js} +8 -8
  144. package/dist/replay-C2LXMJZP.js +66 -0
  145. package/dist/replay-C2LXMJZP.js.map +1 -0
  146. package/dist/{report-6CKSWJNG.js → report-4EVNDHOZ.js} +55 -29
  147. package/dist/report-4EVNDHOZ.js.map +1 -0
  148. package/dist/reports/header-image/assets/headline-announcement.png +0 -0
  149. package/dist/reports/header-image/assets/headline-launch.png +0 -0
  150. package/dist/reports/header-image/assets/headline-maintenance.png +0 -0
  151. package/dist/reports/header-image/assets/headline-testing.png +0 -0
  152. package/dist/reports/header-image/assets/plate-clean.png +0 -0
  153. package/dist/{self-updating-PPQVV6UA.js → self-updating-6DWI2Z56.js} +9 -9
  154. package/dist/{selftest-CQ6NOX52.js → selftest-RC36MTKP.js} +25 -20
  155. package/dist/selftest-RC36MTKP.js.map +1 -0
  156. package/dist/smoke-B2GQOSIZ.js +20 -0
  157. package/dist/smoke-B2GQOSIZ.js.map +1 -0
  158. package/dist/{smoke-suite-TMCSOYRK.js → smoke-suite-VYY2LXM2.js} +9 -9
  159. package/dist/{submissions-GJ6ITBRH.js → submissions-Q7DRFOCD.js} +3 -3
  160. package/dist/{svelte-codemods-ZOBDHIES.js → svelte-codemods-25J2FO22.js} +9 -9
  161. package/dist/sync-34HNZZ56.js +44 -0
  162. package/dist/sync-34HNZZ56.js.map +1 -0
  163. package/dist/{sync-configs-CJXUX6MD.js → sync-configs-3SE6PSCO.js} +9 -9
  164. package/dist/{upgrade-WLX27UPC.js → upgrade-WBRQ62FX.js} +9 -9
  165. package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
  166. package/dist/webhook-VOGOW3W6.js +38 -0
  167. package/dist/webhook-VOGOW3W6.js.map +1 -0
  168. package/dist/{websites-SADZR4XQ.js → websites-SXWTP72S.js} +6 -2
  169. package/dist/websites-SXWTP72S.js.map +1 -0
  170. package/dist/{write-audits-to-airtable-7PH75YWY.js → write-audits-to-airtable-MBEILLPU.js} +4 -4
  171. package/dist/write-audits-to-airtable-MBEILLPU.js.map +1 -0
  172. package/package.json +5 -2
  173. package/dist/airtable-HRKSA7V4.js +0 -9
  174. package/dist/announce-GIAYN67S.js.map +0 -1
  175. package/dist/chunk-37S6L6G4.js.map +0 -1
  176. package/dist/chunk-3YXDSCM6.js.map +0 -1
  177. package/dist/chunk-4DHQCN5H.js.map +0 -1
  178. package/dist/chunk-4SOCPK7A.js +0 -108
  179. package/dist/chunk-4SOCPK7A.js.map +0 -1
  180. package/dist/chunk-577HCGZO.js.map +0 -1
  181. package/dist/chunk-6QRLRT3P.js.map +0 -1
  182. package/dist/chunk-COZPL62X.js.map +0 -1
  183. package/dist/chunk-D4FVAIEJ.js.map +0 -1
  184. package/dist/chunk-DTTIFYAM.js.map +0 -1
  185. package/dist/chunk-MGR2H7F4.js.map +0 -1
  186. package/dist/chunk-MPBXOJ3N.js.map +0 -1
  187. package/dist/chunk-NWB4O5EY.js.map +0 -1
  188. package/dist/chunk-SEEA6RXF.js.map +0 -1
  189. package/dist/chunk-U5SZ4FOP.js +0 -26
  190. package/dist/chunk-U5SZ4FOP.js.map +0 -1
  191. package/dist/client-X5WUJWDE.js +0 -11
  192. package/dist/db-JSIZYT23.js +0 -23
  193. package/dist/db-JSIZYT23.js.map +0 -1
  194. package/dist/ensure-site-S25MDDHW.js.map +0 -1
  195. package/dist/forms-notify-target-TWN4EH6K.js.map +0 -1
  196. package/dist/github-signals-G4ILZAJ6.js.map +0 -1
  197. package/dist/header-image-T7XD37JL.js.map +0 -1
  198. package/dist/migrate-LJUFGXQZ.js +0 -7
  199. package/dist/orchestrate-KDNXZQX7.js +0 -27
  200. package/dist/report-6CKSWJNG.js.map +0 -1
  201. package/dist/reports/header-image/assets/plate.png +0 -0
  202. package/dist/selftest-CQ6NOX52.js.map +0 -1
  203. /package/dist/{airtable-HRKSA7V4.js.map → airtable-G7WBQQMI.js.map} +0 -0
  204. /package/dist/{bump-deps-SWJB6H5V.js.map → bump-deps-EC74CPLS.js.map} +0 -0
  205. /package/dist/{chunk-SDUJIANJ.js.map → chunk-2NJQLSEU.js.map} +0 -0
  206. /package/dist/{chunk-ZTPPCHDB.js.map → chunk-3G25KIWW.js.map} +0 -0
  207. /package/dist/{chunk-Q276MZ7E.js.map → chunk-3VQWSE3F.js.map} +0 -0
  208. /package/dist/{chunk-THEFTZ7F.js.map → chunk-62CMMGEO.js.map} +0 -0
  209. /package/dist/{chunk-FDIFTIXC.js.map → chunk-6TWWY7ZU.js.map} +0 -0
  210. /package/dist/{chunk-UYCAF77E.js.map → chunk-7RJ2HMMJ.js.map} +0 -0
  211. /package/dist/{chunk-R6FPFMOS.js.map → chunk-BQRFQSGY.js.map} +0 -0
  212. /package/dist/{chunk-TP5M2NQB.js.map → chunk-F5KM4LEQ.js.map} +0 -0
  213. /package/dist/{chunk-62VF32IC.js.map → chunk-ILUNZHYV.js.map} +0 -0
  214. /package/dist/{chunk-LS6QPANO.js.map → chunk-IYSEC543.js.map} +0 -0
  215. /package/dist/{chunk-DC5H7KEQ.js.map → chunk-JHGHLKFS.js.map} +0 -0
  216. /package/dist/{chunk-PBWDDDQH.js.map → chunk-KTH5ZODA.js.map} +0 -0
  217. /package/dist/{chunk-7J53SUT6.js.map → chunk-P2FE5AI6.js.map} +0 -0
  218. /package/dist/{chunk-GZNBCHAZ.js.map → chunk-TERE5SDF.js.map} +0 -0
  219. /package/dist/{chunk-ZMIPG6TR.js.map → chunk-TVL5MVUF.js.map} +0 -0
  220. /package/dist/{chunk-VRWUL4TR.js.map → chunk-XHHMM5HX.js.map} +0 -0
  221. /package/dist/{chunk-RJB3QUSP.js.map → chunk-XIDUYLSR.js.map} +0 -0
  222. /package/dist/{chunk-QG6OSL4M.js.map → chunk-XUGSZP7Z.js.map} +0 -0
  223. /package/dist/{chunk-S56JQJ4F.js.map → chunk-Y53WOQIJ.js.map} +0 -0
  224. /package/dist/{chunk-ICXV25N7.js.map → chunk-Z3KYFUN6.js.map} +0 -0
  225. /package/dist/{chunk-WYYQK4D3.js.map → chunk-ZKXSX4LO.js.map} +0 -0
  226. /package/dist/{client-X5WUJWDE.js.map → client-AUE3XUPO.js.map} +0 -0
  227. /package/dist/{convert-to-pnpm-AT52NWZB.js.map → convert-to-pnpm-6L6P74WR.js.map} +0 -0
  228. /package/dist/{health-endpoint-242WG2OJ.js.map → health-endpoint-WJC2UMYS.js.map} +0 -0
  229. /package/dist/{lighthouse-airtable-BIPM26SZ.js.map → import-airtable-HE43FB4H.js.map} +0 -0
  230. /package/dist/{migrate-LJUFGXQZ.js.map → lighthouse-VHI77PFB.js.map} +0 -0
  231. /package/dist/{orchestrate-KDNXZQX7.js.map → lighthouse-airtable-LBVIYYP5.js.map} +0 -0
  232. /package/dist/{websites-SADZR4XQ.js.map → migrate-GQH237A6.js.map} +0 -0
  233. /package/dist/{write-audits-to-airtable-7PH75YWY.js.map → notify-C645PGD5.js.map} +0 -0
  234. /package/dist/{onboard-53CITIM7.js.map → onboard-GK4PHPNI.js.map} +0 -0
  235. /package/dist/{prismic-ci-UTQ6LRR3.js.map → prismic-ci-3RHOZUUT.js.map} +0 -0
  236. /package/dist/{prismic-models-PVES6R5V.js.map → prismic-models-QBGUT2Q3.js.map} +0 -0
  237. /package/dist/{protection-audit-64PLLBLI.js.map → protection-audit-SJO53MEQ.js.map} +0 -0
  238. /package/dist/{renovate-dispatch-ICJGSFCE.js.map → renovate-dispatch-QJY75ODR.js.map} +0 -0
  239. /package/dist/{self-updating-PPQVV6UA.js.map → self-updating-6DWI2Z56.js.map} +0 -0
  240. /package/dist/{smoke-suite-TMCSOYRK.js.map → smoke-suite-VYY2LXM2.js.map} +0 -0
  241. /package/dist/{submissions-GJ6ITBRH.js.map → submissions-Q7DRFOCD.js.map} +0 -0
  242. /package/dist/{svelte-codemods-ZOBDHIES.js.map → svelte-codemods-25J2FO22.js.map} +0 -0
  243. /package/dist/{sync-configs-CJXUX6MD.js.map → sync-configs-3SE6PSCO.js.map} +0 -0
  244. /package/dist/{upgrade-WLX27UPC.js.map → upgrade-WBRQ62FX.js.map} +0 -0
  245. /package/dist/{webflow-624FEBFD.js.map → webflow-IRKOIT7S.js.map} +0 -0
@@ -0,0 +1,72 @@
1
+ // src/reports/checklist.ts
2
+ var MAINTENANCE_CHECKLIST = [
3
+ { key: "deploy", label: "Deploy & Function Health", field: "Maint: Deploy & Function Health" },
4
+ { key: "cms", label: "CMS Checked", field: "Maint: CMS Checked" },
5
+ { key: "domain", label: "Domain, DNS & SSL", field: "Maint: Domain, DNS & SSL" },
6
+ { key: "google", label: "Google Indexed", field: "Maint: Google Indexed" },
7
+ { key: "security", label: "Security Updates", field: "Maint: Security Updates" },
8
+ { key: "uptime", label: "Uptime Checked", field: "Maint: Uptime Checked" }
9
+ ];
10
+ var TESTING_CHECKLIST = [
11
+ { key: "desktop", label: "Desktop Browsers", field: "Test: Desktop Browsers" },
12
+ { key: "mobile", label: "Mobile Browsers", field: "Test: Mobile Browsers" },
13
+ { key: "titles", label: "Page Titles & Meta", field: "Test: Page Titles & Meta" },
14
+ { key: "links", label: "Links & Navigation", field: "Test: Links & Navigation" },
15
+ { key: "forms", label: "Form Functionality", field: "Test: Form Functionality" },
16
+ {
17
+ key: "interactions",
18
+ label: "Interactions & Animations",
19
+ field: "Test: Interactions & Animations"
20
+ },
21
+ // `field` keeps its original Airtable column name ("…Verified After Updates") even though
22
+ // the client-facing label is now "Tested After Updates" — the column holds operator data,
23
+ // so renaming the label is display-only and avoids a live-base column migration.
24
+ { key: "updates", label: "Tested After Updates", field: "Test: Verified After Updates" }
25
+ ];
26
+ var ALL_CHECKLIST_FIELDS = [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map(
27
+ (i) => i.field
28
+ );
29
+ function checklistFor(type) {
30
+ if (type === "Maintenance") return MAINTENANCE_CHECKLIST;
31
+ if (type === "Testing") return [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST];
32
+ return [];
33
+ }
34
+ var MAINTENANCE_GATING_FIELDS = [
35
+ "Maint: Deploy & Function Health",
36
+ "Maint: CMS Checked",
37
+ "Maint: Domain, DNS & SSL",
38
+ "Maint: Security Updates",
39
+ "Maint: Uptime Checked"
40
+ ];
41
+ function gatingFields(type) {
42
+ if (type === "Maintenance") return MAINTENANCE_GATING_FIELDS;
43
+ if (type === "Testing") return checklistFor("Testing").map((i) => i.field);
44
+ return [];
45
+ }
46
+ function isHealthGateClear(report) {
47
+ return gatingFields(report.reportType).every((field) => {
48
+ const r = report.autoEvidence[field]?.result;
49
+ return r === "pass" || r === "n/a";
50
+ });
51
+ }
52
+ function gatingHealth(report) {
53
+ return gatingFields(report.reportType).map((field) => ({
54
+ field,
55
+ status: report.autoEvidence[field]?.result ?? "unknown"
56
+ }));
57
+ }
58
+ function isSendOverridden(report) {
59
+ return report.sendOverride && (report.overrideReason ?? "").trim() !== "";
60
+ }
61
+
62
+ export {
63
+ MAINTENANCE_CHECKLIST,
64
+ TESTING_CHECKLIST,
65
+ ALL_CHECKLIST_FIELDS,
66
+ checklistFor,
67
+ gatingFields,
68
+ isHealthGateClear,
69
+ gatingHealth,
70
+ isSendOverridden
71
+ };
72
+ //# sourceMappingURL=chunk-VAD43MOR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/checklist.ts"],"sourcesContent":["import type { ReportType } from \"./types.js\";\nimport type { EvidenceResult, EvidenceRecord } from \"./auto-tick.js\";\n\n/**\n * One operator checklist item: a stable `key`, its display `label` (which mirrors\n * the client email's checklist line — kept in sync by a test against DEFAULT_COPY),\n * and the exact Airtable checkbox column `field` it reads/writes.\n */\nexport type ChecklistItem = { key: string; label: string; field: string };\n\n/**\n * The 6 Maintenance-report items. `label`s mirror DEFAULT_COPY.maintenanceChecks\n * (same order — \"Google Indexed\" MUST stay at index 3; the email template special-cases\n * that row to inject the live search position).\n */\nexport const MAINTENANCE_CHECKLIST: ChecklistItem[] = [\n { key: \"deploy\", label: \"Deploy & Function Health\", field: \"Maint: Deploy & Function Health\" },\n { key: \"cms\", label: \"CMS Checked\", field: \"Maint: CMS Checked\" },\n { key: \"domain\", label: \"Domain, DNS & SSL\", field: \"Maint: Domain, DNS & SSL\" },\n { key: \"google\", label: \"Google Indexed\", field: \"Maint: Google Indexed\" },\n { key: \"security\", label: \"Security Updates\", field: \"Maint: Security Updates\" },\n { key: \"uptime\", label: \"Uptime Checked\", field: \"Maint: Uptime Checked\" },\n];\n\n/** The 7 Testing-report items. `label`s mirror DEFAULT_COPY.testingChecklist. */\nexport const TESTING_CHECKLIST: ChecklistItem[] = [\n { key: \"desktop\", label: \"Desktop Browsers\", field: \"Test: Desktop Browsers\" },\n { key: \"mobile\", label: \"Mobile Browsers\", field: \"Test: Mobile Browsers\" },\n { key: \"titles\", label: \"Page Titles & Meta\", field: \"Test: Page Titles & Meta\" },\n { key: \"links\", label: \"Links & Navigation\", field: \"Test: Links & Navigation\" },\n { key: \"forms\", label: \"Form Functionality\", field: \"Test: Form Functionality\" },\n {\n key: \"interactions\",\n label: \"Interactions & Animations\",\n field: \"Test: Interactions & Animations\",\n },\n // `field` keeps its original Airtable column name (\"…Verified After Updates\") even though\n // the client-facing label is now \"Tested After Updates\" — the column holds operator data,\n // so renaming the label is display-only and avoids a live-base column migration.\n { key: \"updates\", label: \"Tested After Updates\", field: \"Test: Verified After Updates\" },\n];\n\n/** All 13 Airtable checkbox column names. mapRow reads exactly these into the row's checklist. */\nexport const ALL_CHECKLIST_FIELDS: string[] = [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map(\n (i) => i.field,\n);\n\n/**\n * The checklist that gates a report of the given type, in the order the email renders it.\n * Maintenance gates on its 6 items. A Testing pass also performs the maintenance checks —\n * and the Testing email shows BOTH lists — so a Testing report gates on all 13\n * (maintenance first, then testing). Launch/Announcement have no checklist (the gate is\n * vacuously satisfied). PURE.\n */\nexport function checklistFor(type: ReportType): ChecklistItem[] {\n if (type === \"Maintenance\") return MAINTENANCE_CHECKLIST;\n if (type === \"Testing\") return [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST];\n return [];\n}\n\n/** The maintenance items whose HEALTH gates a Maintenance send. Google Indexed is advisory\n * (reported, never blocks) so it is excluded. */\nconst MAINTENANCE_GATING_FIELDS: string[] = [\n \"Maint: Deploy & Function Health\",\n \"Maint: CMS Checked\",\n \"Maint: Domain, DNS & SSL\",\n \"Maint: Security Updates\",\n \"Maint: Uptime Checked\",\n];\n\n/**\n * The checklist fields whose health gates a send of this report type. Maintenance gates\n * availability/integrity only (Google Indexed is advisory); Testing holds the full bar (all 13,\n * including Google Indexed); Launch/Announcement are ungated. PURE.\n */\nexport function gatingFields(type: ReportType): string[] {\n if (type === \"Maintenance\") return MAINTENANCE_GATING_FIELDS;\n if (type === \"Testing\") return checklistFor(\"Testing\").map((i) => i.field);\n return [];\n}\n\n/**\n * The health gate: clear iff EVERY gating field's evidence result is `pass` or `n/a`. A `fail`,\n * an `unknown`, or an ABSENT record all block — the semantic inversion (no fresh signal → cannot\n * confirm health → don't send). Launch/Announcement have no gating fields → vacuously clear. PURE.\n */\nexport function isHealthGateClear(report: {\n reportType: ReportType;\n autoEvidence: Record<string, EvidenceRecord>;\n}): boolean {\n return gatingFields(report.reportType).every((field) => {\n const r = report.autoEvidence[field]?.result;\n return r === \"pass\" || r === \"n/a\";\n });\n}\n\n/** Per-gating-field status for by-name blocker messaging (send log, dashboard). An absent record\n * surfaces as `unknown`. PURE. */\nexport function gatingHealth(report: {\n reportType: ReportType;\n autoEvidence: Record<string, EvidenceRecord>;\n}): { field: string; status: EvidenceResult }[] {\n return gatingFields(report.reportType).map((field) => ({\n field,\n status: report.autoEvidence[field]?.result ?? \"unknown\",\n }));\n}\n\n/**\n * True when a logged send-anyway override is active AND carries a non-empty reason. The effective\n * send gate is `isHealthGateClear(report) || isSendOverridden(report)`. PURE — takes the minimal\n * structural shape so it can be evaluated over a ReportRow or a synthetic \"about to override\" copy.\n */\nexport function isSendOverridden(report: {\n sendOverride: boolean;\n overrideReason: string | null;\n}): boolean {\n return report.sendOverride && (report.overrideReason ?? \"\").trim() !== \"\";\n}\n"],"mappings":";AAeO,IAAM,wBAAyC;AAAA,EACpD,EAAE,KAAK,UAAU,OAAO,4BAA4B,OAAO,kCAAkC;AAAA,EAC7F,EAAE,KAAK,OAAO,OAAO,eAAe,OAAO,qBAAqB;AAAA,EAChE,EAAE,KAAK,UAAU,OAAO,qBAAqB,OAAO,2BAA2B;AAAA,EAC/E,EAAE,KAAK,UAAU,OAAO,kBAAkB,OAAO,wBAAwB;AAAA,EACzE,EAAE,KAAK,YAAY,OAAO,oBAAoB,OAAO,0BAA0B;AAAA,EAC/E,EAAE,KAAK,UAAU,OAAO,kBAAkB,OAAO,wBAAwB;AAC3E;AAGO,IAAM,oBAAqC;AAAA,EAChD,EAAE,KAAK,WAAW,OAAO,oBAAoB,OAAO,yBAAyB;AAAA,EAC7E,EAAE,KAAK,UAAU,OAAO,mBAAmB,OAAO,wBAAwB;AAAA,EAC1E,EAAE,KAAK,UAAU,OAAO,sBAAsB,OAAO,2BAA2B;AAAA,EAChF,EAAE,KAAK,SAAS,OAAO,sBAAsB,OAAO,2BAA2B;AAAA,EAC/E,EAAE,KAAK,SAAS,OAAO,sBAAsB,OAAO,2BAA2B;AAAA,EAC/E;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,OAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAIA,EAAE,KAAK,WAAW,OAAO,wBAAwB,OAAO,+BAA+B;AACzF;AAGO,IAAM,uBAAiC,CAAC,GAAG,uBAAuB,GAAG,iBAAiB,EAAE;AAAA,EAC7F,CAAC,MAAM,EAAE;AACX;AASO,SAAS,aAAa,MAAmC;AAC9D,MAAI,SAAS,cAAe,QAAO;AACnC,MAAI,SAAS,UAAW,QAAO,CAAC,GAAG,uBAAuB,GAAG,iBAAiB;AAC9E,SAAO,CAAC;AACV;AAIA,IAAM,4BAAsC;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,SAAS,aAAa,MAA4B;AACvD,MAAI,SAAS,cAAe,QAAO;AACnC,MAAI,SAAS,UAAW,QAAO,aAAa,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK;AACzE,SAAO,CAAC;AACV;AAOO,SAAS,kBAAkB,QAGtB;AACV,SAAO,aAAa,OAAO,UAAU,EAAE,MAAM,CAAC,UAAU;AACtD,UAAM,IAAI,OAAO,aAAa,KAAK,GAAG;AACtC,WAAO,MAAM,UAAU,MAAM;AAAA,EAC/B,CAAC;AACH;AAIO,SAAS,aAAa,QAGmB;AAC9C,SAAO,aAAa,OAAO,UAAU,EAAE,IAAI,CAAC,WAAW;AAAA,IACrD;AAAA,IACA,QAAQ,OAAO,aAAa,KAAK,GAAG,UAAU;AAAA,EAChD,EAAE;AACJ;AAOO,SAAS,iBAAiB,QAGrB;AACV,SAAO,OAAO,iBAAiB,OAAO,kBAAkB,IAAI,KAAK,MAAM;AACzE;","names":[]}
@@ -0,0 +1,229 @@
1
+ import {
2
+ hostsMatch
3
+ } from "./chunk-BNM5POSJ.js";
4
+ import {
5
+ operatorEmail
6
+ } from "./chunk-5ML7FMBD.js";
7
+ import {
8
+ escapeHtml
9
+ } from "./chunk-DJZWYIPP.js";
10
+
11
+ // src/forms/notify.ts
12
+ var FORMS_FROM = "forms@reddoorla.com";
13
+ var FALLBACK_REPLY_TO = "info@reddoorla.com";
14
+ function displayName(raw) {
15
+ return raw.replace(/["\r\n]/g, "").trim() || "Reddoor";
16
+ }
17
+ function pocAddress(site) {
18
+ return site.pointOfContact ?? site.reportRecipientsTo ?? null;
19
+ }
20
+ function normalizeRecipients(v) {
21
+ const arr = Array.isArray(v) ? v : v ? [v] : [];
22
+ const seen = /* @__PURE__ */ new Set();
23
+ const out = [];
24
+ for (const raw of arr) {
25
+ if (typeof raw !== "string") continue;
26
+ const t = raw.trim();
27
+ if (t && !seen.has(t)) {
28
+ seen.add(t);
29
+ out.push(t);
30
+ }
31
+ }
32
+ return out;
33
+ }
34
+ function resolveRecipients(site, submission) {
35
+ if (site.status !== "maintained") {
36
+ return { to: [operatorEmail()], cc: [] };
37
+ }
38
+ const routing = site.notifyRouting;
39
+ if (routing) {
40
+ const value = parseExtraFields(submission.extraFields)[routing.field];
41
+ const match = typeof value === "string" ? routing.routes[value] : void 0;
42
+ const to = normalizeRecipients(match ?? routing.default);
43
+ if (to.length > 0) return { to, cc: normalizeRecipients(routing.cc) };
44
+ }
45
+ const poc = pocAddress(site);
46
+ return poc ? { to: [poc], cc: [] } : null;
47
+ }
48
+ function probeSubmission(extraFields) {
49
+ return {
50
+ id: "probe",
51
+ submissionId: null,
52
+ siteId: "probe",
53
+ formType: "contact",
54
+ name: "probe",
55
+ email: "probe@example.com",
56
+ phone: null,
57
+ message: null,
58
+ extraFields,
59
+ sourceUrl: null,
60
+ utm: null,
61
+ submittedAt: null,
62
+ status: "new",
63
+ notifyStatus: "pending",
64
+ resendMessageId: null
65
+ };
66
+ }
67
+ function describeNotifyTarget(site) {
68
+ if (site.status !== "maintained") {
69
+ const guarded = resolveRecipients(site, probeSubmission(null));
70
+ return {
71
+ audience: "operator",
72
+ to: guarded?.to ?? [],
73
+ cc: [],
74
+ reason: `Status is ${site.status === null ? "blank" : `"${site.status}"`}, not "maintained" \u2014 the pre-launch guard routes every notification to the operator.`
75
+ };
76
+ }
77
+ const routing = site.notifyRouting;
78
+ if (!routing) {
79
+ const direct = resolveRecipients(site, probeSubmission(null));
80
+ if (!direct || direct.to.length === 0) {
81
+ return {
82
+ audience: "nobody",
83
+ to: [],
84
+ cc: [],
85
+ reason: `Status is "maintained" but the site has no Point of Contact and no Notify Routing \u2014 the lead is stored and no notification is sent.`
86
+ };
87
+ }
88
+ return {
89
+ audience: "client",
90
+ to: direct.to,
91
+ cc: direct.cc,
92
+ reason: `Status is "maintained" \u2014 notifications go to the site's Point of Contact.`
93
+ };
94
+ }
95
+ const to = /* @__PURE__ */ new Set();
96
+ const cc = /* @__PURE__ */ new Set();
97
+ const values = [...Object.keys(routing.routes), "\0no-such-route"];
98
+ for (const value of values) {
99
+ const r = resolveRecipients(site, probeSubmission(JSON.stringify({ [routing.field]: value })));
100
+ r?.to.forEach((a) => to.add(a));
101
+ r?.cc.forEach((a) => cc.add(a));
102
+ }
103
+ if (to.size === 0) {
104
+ return {
105
+ audience: "nobody",
106
+ to: [],
107
+ cc: [],
108
+ reason: `Status is "maintained" with Notify Routing on "${routing.field}", but no route, default or Point of Contact resolves to an address.`
109
+ };
110
+ }
111
+ return {
112
+ audience: "client",
113
+ to: [...to],
114
+ cc: [...cc],
115
+ reason: `Status is "maintained" with Notify Routing on "${routing.field}" \u2014 the recipient depends on that field's value; every address it could reach is listed.`
116
+ };
117
+ }
118
+ function humanizeKey(k) {
119
+ const spaced = k.replace(/[_-]+/g, " ").trim();
120
+ return spaced ? spaced.charAt(0).toUpperCase() + spaced.slice(1) : k;
121
+ }
122
+ function formatValue(v) {
123
+ if (v === null || v === void 0) return "\u2014";
124
+ if (typeof v === "string") return v;
125
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
126
+ return JSON.stringify(v);
127
+ }
128
+ function parseExtraFields(raw) {
129
+ if (!raw) return {};
130
+ try {
131
+ const parsed = JSON.parse(raw);
132
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
133
+ return parsed;
134
+ }
135
+ } catch {
136
+ }
137
+ return {};
138
+ }
139
+ function extraFieldRows(raw) {
140
+ return Object.entries(parseExtraFields(raw)).filter(([, v]) => !(typeof v === "string" && v.trim() === "")).map(([k, v]) => [humanizeKey(k), formatValue(v)]);
141
+ }
142
+ function fieldsTable(submission) {
143
+ const rows = [
144
+ ["Form", submission.formType],
145
+ ["Name", submission.name || "\u2014"],
146
+ ["Email", submission.email || "\u2014"]
147
+ ];
148
+ if (submission.phone) rows.push(["Phone", submission.phone]);
149
+ rows.push(...extraFieldRows(submission.extraFields));
150
+ if (submission.sourceUrl) rows.push(["Page", submission.sourceUrl]);
151
+ if (submission.utm) rows.push(["UTM", submission.utm]);
152
+ const body = rows.map(([k, v]) => `<tr><td><strong>${escapeHtml(k)}</strong></td><td>${escapeHtml(v)}</td></tr>`).join("");
153
+ const message = submission.message ? `<p style="white-space:pre-wrap">${escapeHtml(submission.message)}</p>` : "";
154
+ return `<table>${body}</table>${message}`;
155
+ }
156
+ function buildPocNotification(site, submission) {
157
+ if (submission.status === "spam_auto" || submission.status === "spam") return null;
158
+ const recipients = resolveRecipients(site, submission);
159
+ if (!recipients || recipients.to.length === 0) return null;
160
+ const input = {
161
+ from: `${displayName(site.name)} Forms <${FORMS_FROM}>`,
162
+ to: recipients.to,
163
+ subject: `New ${submission.formType} from ${site.name}`,
164
+ html: `<h2>New ${escapeHtml(submission.formType)} submission \u2014 ${escapeHtml(
165
+ site.name
166
+ )}</h2>${fieldsTable(submission)}`
167
+ };
168
+ if (recipients.cc.length > 0) input.cc = recipients.cc;
169
+ if (submission.email) input.replyTo = submission.email;
170
+ return input;
171
+ }
172
+ function buildAutoresponder(site, submission) {
173
+ if (submission.status === "spam_auto" || submission.status === "spam") return null;
174
+ if (!submission.email) return null;
175
+ const emailDomain = submission.email.split("@").pop() ?? "";
176
+ let siteHost = "";
177
+ try {
178
+ siteHost = new URL(site.url).hostname;
179
+ } catch {
180
+ }
181
+ if (siteHost && hostsMatch(emailDomain, siteHost)) return null;
182
+ const intro = site.copyIntro ?? `Thanks for reaching out to ${site.name}.`;
183
+ const contact = site.copyContact ?? "We've received your message and will be in touch soon.";
184
+ const footer = site.copyFooter ?? site.name;
185
+ return {
186
+ from: `${displayName(site.name)} <${FORMS_FROM}>`,
187
+ to: [submission.email],
188
+ replyTo: resolveRecipients(site, submission)?.to[0] ?? FALLBACK_REPLY_TO,
189
+ subject: "We got your message",
190
+ html: `<p>${escapeHtml(intro)}</p><p>${escapeHtml(contact)}</p><p>${escapeHtml(footer)}</p>`
191
+ };
192
+ }
193
+ async function notifySubmission(deps, site, submission) {
194
+ const poc = buildPocNotification(site, submission);
195
+ let outcome;
196
+ if (!poc) {
197
+ outcome = { status: "skipped", messageId: null };
198
+ } else {
199
+ try {
200
+ const { messageId } = await deps.send(poc);
201
+ outcome = { status: "sent", messageId };
202
+ } catch (err) {
203
+ console.error(`[submissions] POC notification failed: ${String(err)}`);
204
+ outcome = { status: "failed", messageId: null };
205
+ }
206
+ }
207
+ const auto = buildAutoresponder(site, submission);
208
+ if (auto) {
209
+ try {
210
+ await deps.send(auto);
211
+ } catch (err) {
212
+ console.error(`[submissions] autoresponder failed: ${String(err)}`);
213
+ }
214
+ }
215
+ return outcome;
216
+ }
217
+ function makeNotify(send) {
218
+ return (site, submission) => send ? notifySubmission({ send }, site, submission) : Promise.resolve({ status: "failed", messageId: null });
219
+ }
220
+
221
+ export {
222
+ resolveRecipients,
223
+ describeNotifyTarget,
224
+ buildPocNotification,
225
+ buildAutoresponder,
226
+ notifySubmission,
227
+ makeNotify
228
+ };
229
+ //# sourceMappingURL=chunk-VSBFM7DS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/forms/notify.ts"],"sourcesContent":["import type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport type { SubmissionRow, NotifyStatus } from \"../reports/submission-row.js\";\nimport type { ResendSendInput } from \"../reports/send/resend.js\";\nimport { hostsMatch } from \"./ingest.js\";\nimport { escapeHtml } from \"../util/html.js\";\nimport { operatorEmail } from \"../util/operator.js\";\n\nconst FORMS_FROM = \"forms@reddoorla.com\";\n// Reply-To only: a client replying to a lead notification should reach the\n// shared inbox. The RECIPIENT fallback is the operator's own — see util/operator.\nconst FALLBACK_REPLY_TO = \"info@reddoorla.com\";\n\n/** Strip characters that would break an RFC 5322 display name. */\nfunction displayName(raw: string): string {\n return raw.replace(/[\"\\r\\n]/g, \"\").trim() || \"Reddoor\";\n}\n\nfunction pocAddress(site: WebsiteRow): string | null {\n return site.pointOfContact ?? site.reportRecipientsTo ?? null;\n}\n\n/** Coerce a string|string[] recipient config into a clean, de-duped address list. */\nfunction normalizeRecipients(v: string | string[] | undefined): string[] {\n const arr = Array.isArray(v) ? v : v ? [v] : [];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const raw of arr) {\n if (typeof raw !== \"string\") continue;\n const t = raw.trim();\n if (t && !seen.has(t)) {\n seen.add(t);\n out.push(t);\n }\n }\n return out;\n}\n\nexport type Recipients = { to: string[]; cc: string[] };\n\n/**\n * Where a submission notification goes.\n * - Pre-launch (status !== \"maintained\"): the operator only — no routing, no CC.\n * Preserves the verify guard (flip a site to \"launching\" to route tests to\n * yourself).\n * - Maintained + a `Notify Routing` config: address by the routing field's value\n * (`extraFields[field]`) → its matched route, else the config `default`; CC from\n * the config. If nothing resolves, fall through to the single POC.\n * - Maintained, no routing: the single site POC (pointOfContact ?? reportRecipientsTo).\n * Returns null only when nothing resolves — the lead is still persisted; notify skips.\n */\nexport function resolveRecipients(site: WebsiteRow, submission: SubmissionRow): Recipients | null {\n if (site.status !== \"maintained\") {\n return { to: [operatorEmail()], cc: [] };\n }\n const routing = site.notifyRouting;\n if (routing) {\n const value = parseExtraFields(submission.extraFields)[routing.field];\n const match = typeof value === \"string\" ? routing.routes[value] : undefined;\n const to = normalizeRecipients(match ?? routing.default);\n if (to.length > 0) return { to, cc: normalizeRecipients(routing.cc) };\n // routing matched nothing → fall through to the POC below\n }\n const poc = pocAddress(site);\n return poc ? { to: [poc], cc: [] } : null;\n}\n\n/** Who a submission to this site would notify, answered WITHOUT sending one. */\nexport type NotifyTarget = {\n /** `client` means a real point of contact receives it. `operator` means the\n * pre-launch guard is holding and only you do. `nobody` means nothing\n * resolves — the lead is still stored, but no notification goes out. */\n audience: \"client\" | \"operator\" | \"nobody\";\n /** Every address a submission could reach, across ALL routing branches. */\n to: string[];\n cc: string[];\n /** Plain-language reason, naming the field that decided it. */\n reason: string;\n};\n\n/** A submission that exists only to ask resolveRecipients a question. */\nfunction probeSubmission(extraFields: string | null): SubmissionRow {\n return {\n id: \"probe\",\n submissionId: null,\n siteId: \"probe\",\n formType: \"contact\" as SubmissionRow[\"formType\"],\n name: \"probe\",\n email: \"probe@example.com\",\n phone: null,\n message: null,\n extraFields,\n sourceUrl: null,\n utm: null,\n submittedAt: null,\n status: \"new\" as SubmissionRow[\"status\"],\n notifyStatus: \"pending\" as NotifyStatus,\n resendMessageId: null,\n };\n}\n\n/**\n * Resolve who a form submission would notify, for a site, right now.\n *\n * This exists because the pre-launch guard had no feedback. Its whole state is\n * one Airtable `Status` cell nobody is looking at while testing, the site\n * reports nothing about it, and the only way to learn the answer was to submit\n * a form and read the `resend_message_id` afterwards. On 2026-08-03 an intended\n * flip never landed and a real client received a test lead — an email cannot be\n * recalled. A guard whose state you cannot see is a guard you will eventually\n * assume is on.\n *\n * Every address here comes back out of `resolveRecipients` itself rather than\n * from a second reading of the same rules — for a routed site each branch is\n * probed with a synthetic submission and the results unioned. A copy of the\n * routing logic that drifted would be worse than no answer at all, because it\n * would be a confident wrong one.\n */\nexport function describeNotifyTarget(site: WebsiteRow): NotifyTarget {\n if (site.status !== \"maintained\") {\n const guarded = resolveRecipients(site, probeSubmission(null));\n return {\n audience: \"operator\",\n to: guarded?.to ?? [],\n cc: [],\n reason: `Status is ${site.status === null ? \"blank\" : `\"${site.status}\"`}, not \"maintained\" — the pre-launch guard routes every notification to the operator.`,\n };\n }\n\n const routing = site.notifyRouting;\n if (!routing) {\n const direct = resolveRecipients(site, probeSubmission(null));\n if (!direct || direct.to.length === 0) {\n return {\n audience: \"nobody\",\n to: [],\n cc: [],\n reason: `Status is \"maintained\" but the site has no Point of Contact and no Notify Routing — the lead is stored and no notification is sent.`,\n };\n }\n return {\n audience: \"client\",\n to: direct.to,\n cc: direct.cc,\n reason: `Status is \"maintained\" — notifications go to the site's Point of Contact.`,\n };\n }\n\n // Probe every declared route value, plus one value that matches none so the\n // `default` (and the POC fall-through behind it) is covered too.\n const to = new Set<string>();\n const cc = new Set<string>();\n const values = [...Object.keys(routing.routes), \"\u0000no-such-route\"];\n for (const value of values) {\n const r = resolveRecipients(site, probeSubmission(JSON.stringify({ [routing.field]: value })));\n r?.to.forEach((a) => to.add(a));\n r?.cc.forEach((a) => cc.add(a));\n }\n if (to.size === 0) {\n return {\n audience: \"nobody\",\n to: [],\n cc: [],\n reason: `Status is \"maintained\" with Notify Routing on \"${routing.field}\", but no route, default or Point of Contact resolves to an address.`,\n };\n }\n return {\n audience: \"client\",\n to: [...to],\n cc: [...cc],\n reason: `Status is \"maintained\" with Notify Routing on \"${routing.field}\" — the recipient depends on that field's value; every address it could reach is listed.`,\n };\n}\n\n/** Humanize an extraFields key for display: \"appointment_date\" → \"Appointment date\". */\nfunction humanizeKey(k: string): string {\n const spaced = k.replace(/[_-]+/g, \" \").trim();\n return spaced ? spaced.charAt(0).toUpperCase() + spaced.slice(1) : k;\n}\n\nfunction formatValue(v: unknown): string {\n if (v === null || v === undefined) return \"—\";\n if (typeof v === \"string\") return v;\n if (typeof v === \"number\" || typeof v === \"boolean\") return String(v);\n return JSON.stringify(v);\n}\n\n/** Parse the stored `extraFields` JSON into a plain object — bad JSON or a\n * non-object yields {} (never throws). Shared by the email renderer and routing. */\nfunction parseExtraFields(raw: string | null): Record<string, unknown> {\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n } catch {\n /* malformed JSON → no fields */\n }\n return {};\n}\n\nfunction extraFieldRows(raw: string | null): Array<[string, string]> {\n return Object.entries(parseExtraFields(raw))\n .filter(([, v]) => !(typeof v === \"string\" && v.trim() === \"\"))\n .map(([k, v]) => [humanizeKey(k), formatValue(v)] as [string, string]);\n}\n\nfunction fieldsTable(submission: SubmissionRow): string {\n const rows: Array<[string, string]> = [\n [\"Form\", submission.formType],\n [\"Name\", submission.name || \"—\"],\n [\"Email\", submission.email || \"—\"],\n ];\n if (submission.phone) rows.push([\"Phone\", submission.phone]);\n // Site-specific context (the artwork an inquiry is about, the event an rsvp is\n // for, etc.) lives in extraFields — surface it so the recipient sees what the\n // submitter was looking at, not just their name and email.\n rows.push(...extraFieldRows(submission.extraFields));\n if (submission.sourceUrl) rows.push([\"Page\", submission.sourceUrl]);\n if (submission.utm) rows.push([\"UTM\", submission.utm]);\n const body = rows\n .map(([k, v]) => `<tr><td><strong>${escapeHtml(k)}</strong></td><td>${escapeHtml(v)}</td></tr>`)\n .join(\"\");\n const message = submission.message\n ? `<p style=\"white-space:pre-wrap\">${escapeHtml(submission.message)}</p>`\n : \"\";\n return `<table>${body}</table>${message}`;\n}\n\n/** POC notification — the primary email; null when the site has no contact address. */\nexport function buildPocNotification(\n site: WebsiteRow,\n submission: SubmissionRow,\n): ResendSendInput | null {\n if (submission.status === \"spam_auto\" || submission.status === \"spam\") return null;\n const recipients = resolveRecipients(site, submission);\n if (!recipients || recipients.to.length === 0) return null;\n const input: ResendSendInput = {\n from: `${displayName(site.name)} Forms <${FORMS_FROM}>`,\n to: recipients.to,\n subject: `New ${submission.formType} from ${site.name}`,\n html: `<h2>New ${escapeHtml(submission.formType)} submission — ${escapeHtml(\n site.name,\n )}</h2>${fieldsTable(submission)}`,\n };\n if (recipients.cc.length > 0) input.cc = recipients.cc;\n // Reply straight to the lead.\n if (submission.email) input.replyTo = submission.email;\n return input;\n}\n\n/** Autoresponder to the submitter — null when there's no submitter email. */\nexport function buildAutoresponder(\n site: WebsiteRow,\n submission: SubmissionRow,\n): ResendSendInput | null {\n if (submission.status === \"spam_auto\" || submission.status === \"spam\") return null;\n if (!submission.email) return null;\n // A submitter email on the site's OWN domain is never a real lead wanting a\n // confirmation — it is the spoofed-sender backscatter case (a bot writes the\n // site's info@ as its email and the \"We got your message\" lands in the\n // client's inbox). The POC notification still sends; only this email is\n // suppressed. Unparseable/blank site url → fail open and send, same\n // philosophy as turnstileHostnameAcceptable.\n const emailDomain = submission.email.split(\"@\").pop() ?? \"\";\n let siteHost = \"\";\n try {\n siteHost = new URL(site.url).hostname;\n } catch {\n /* fail open */\n }\n if (siteHost && hostsMatch(emailDomain, siteHost)) return null;\n const intro = site.copyIntro ?? `Thanks for reaching out to ${site.name}.`;\n const contact = site.copyContact ?? \"We've received your message and will be in touch soon.\";\n const footer = site.copyFooter ?? site.name;\n return {\n from: `${displayName(site.name)} <${FORMS_FROM}>`,\n to: [submission.email],\n replyTo: resolveRecipients(site, submission)?.to[0] ?? FALLBACK_REPLY_TO,\n subject: \"We got your message\",\n html: `<p>${escapeHtml(intro)}</p><p>${escapeHtml(contact)}</p><p>${escapeHtml(footer)}</p>`,\n };\n}\n\nexport type NotifyDeps = {\n send: (input: ResendSendInput) => Promise<{ messageId: string }>;\n};\n\nexport type NotifyOutcome = { status: NotifyStatus; messageId: string | null };\n\n/**\n * Send the POC notification (primary — drives notifyStatus) then the submitter\n * autoresponder (best-effort — logged, never changes the outcome). The submission\n * is already persisted before this runs, so a Resend outage degrades to\n * notifyStatus=\"failed\", never a lost lead.\n */\nexport async function notifySubmission(\n deps: NotifyDeps,\n site: WebsiteRow,\n submission: SubmissionRow,\n): Promise<NotifyOutcome> {\n const poc = buildPocNotification(site, submission);\n let outcome: NotifyOutcome;\n if (!poc) {\n outcome = { status: \"skipped\", messageId: null };\n } else {\n try {\n const { messageId } = await deps.send(poc);\n outcome = { status: \"sent\", messageId };\n } catch (err) {\n console.error(`[submissions] POC notification failed: ${String(err)}`);\n outcome = { status: \"failed\", messageId: null };\n }\n }\n const auto = buildAutoresponder(site, submission);\n if (auto) {\n try {\n await deps.send(auto);\n } catch (err) {\n console.error(`[submissions] autoresponder failed: ${String(err)}`);\n }\n }\n return outcome;\n}\n\n/**\n * Build the ingest `notify` dependency from a Resend send fn — or `null` when the\n * Resend client couldn't even be constructed (e.g. `RESEND_API_KEY` unset). A null\n * send marks the notification `failed` WITHOUT attempting it, so a Resend\n * misconfiguration degrades to a captured-but-unemailed lead rather than aborting\n * ingest and losing it. Mirrors the in-flight failure isolation in notifySubmission.\n */\nexport function makeNotify(\n send: NotifyDeps[\"send\"] | null,\n): (site: WebsiteRow, submission: SubmissionRow) => Promise<NotifyOutcome> {\n return (site, submission) =>\n send\n ? notifySubmission({ send }, site, submission)\n : Promise.resolve({ status: \"failed\", messageId: null });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,274 @@
1
+ import {
2
+ ANALYZE_SKIPPED,
3
+ PROBES_SKIPPED
4
+ } from "./chunk-X3B723J4.js";
5
+ import {
6
+ escapeHtml,
7
+ safeUrl
8
+ } from "./chunk-DJZWYIPP.js";
9
+ import {
10
+ hostnameOf
11
+ } from "./chunk-EVBCK2KC.js";
12
+
13
+ // src/prospect/render.ts
14
+ var RED = "#d71920";
15
+ var IMPACT_ORDER = { high: 0, medium: 1, low: 2 };
16
+ var KIND_LABEL = {
17
+ branded: "Branded \u2014 asked about the business by name",
18
+ category: "Category \u2014 the questions a buyer would actually type",
19
+ competitor: "Competitor \u2014 head-to-head comparisons"
20
+ };
21
+ var KIND_ORDER = ["category", "branded", "competitor"];
22
+ var AI_AGENT_LABELS = {
23
+ GPTBot: "ChatGPT",
24
+ "OAI-SearchBot": "ChatGPT's search results",
25
+ ClaudeBot: "Claude",
26
+ PerplexityBot: "Perplexity",
27
+ "Google-Extended": "Gemini and Google's AI Overviews",
28
+ CCBot: "Common Crawl, used to train many AI models"
29
+ };
30
+ function describeAgent(agent) {
31
+ const label = AI_AGENT_LABELS[agent];
32
+ return label ? `${agent} (feeds ${label})` : agent;
33
+ }
34
+ var STAGE_FAILED_MESSAGE = "we could not complete this part of the analysis";
35
+ var STYLES = `
36
+ :root { color-scheme: light; }
37
+ * { box-sizing: border-box; }
38
+ body { margin: 0; background: #faf8f5; color: #1a1a1a;
39
+ font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
40
+ .wrap { max-width: 860px; margin: 0 auto; padding: 48px 24px 80px; }
41
+ h1, h2, h3 { font-family: Besley, Georgia, "Times New Roman", serif; font-weight: 600; line-height: 1.2; }
42
+ h1 { font-size: 40px; margin: 0 0 8px; }
43
+ h2 { font-size: 26px; margin: 48px 0 12px; border-top: 2px solid #e6e1da; padding-top: 24px; }
44
+ h3 { font-size: 18px; margin: 24px 0 8px; }
45
+ .lede { color: #57544f; margin: 0 0 8px; }
46
+ .scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin: 32px 0; }
47
+ .score { background: #fff; border: 1px solid #e6e1da; border-radius: 8px; padding: 16px; }
48
+ .score .n { font-family: Besley, Georgia, serif; font-size: 38px; line-height: 1; color: ${RED}; }
49
+ .score .n.na { font-size: 18px; color: #8a857e; }
50
+ .score .l { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: #57544f; margin-top: 8px; }
51
+ .score .hint { font-size: 12px; color: #8a857e; margin-top: 4px; }
52
+ .card { background: #fff; border: 1px solid #e6e1da; border-radius: 8px; padding: 16px; margin: 12px 0; }
53
+ .q { font-weight: 600; }
54
+ .tag { display: inline-block; font-size: 12px; letter-spacing: .06em; text-transform: uppercase;
55
+ border-radius: 999px; padding: 2px 10px; border: 1px solid currentColor; }
56
+ .yes { color: #14663c; } .partial { color: #8a6d00; } .no { color: ${RED}; }
57
+ ul { padding-left: 20px; } li { margin: 6px 0; }
58
+ .muted { color: #8a857e; }
59
+ .cta { margin-top: 56px; background: ${RED}; color: #fff; border-radius: 8px; padding: 24px 28px; }
60
+ .cta h2 { border: 0; padding: 0; margin: 0 0 8px; color: #fff; }
61
+ .cta a { color: #fff; }
62
+ table { width: 100%; border-collapse: collapse; margin-top: 8px; }
63
+ td, th { text-align: left; padding: 6px 8px; border-bottom: 1px solid #eee; vertical-align: top; }
64
+ @media print {
65
+ body { background: #fff; }
66
+ .card, .score, .cta { break-inside: avoid; }
67
+ /* Browsers default "print background graphics" OFF, which drops the red
68
+ .cta background but keeps its white text \u2014 the only ask in the report
69
+ would otherwise print white-on-white and vanish. Fall back to dark
70
+ text on no background with a visible border instead of relying on the
71
+ background for contrast. */
72
+ .cta { background: none; color: #1a1a1a; border: 2px solid ${RED}; }
73
+ .cta h2, .cta a { color: #1a1a1a; }
74
+ }
75
+ `;
76
+ function scoreCard(label, value, hint) {
77
+ const n = value === null ? `<div class="n na">Not measured</div>` : `<div class="n">${value}</div>`;
78
+ const hintHtml = hint ? `<div class="hint">${escapeHtml(hint)}</div>` : "";
79
+ return `<div class="score">${n}<div class="l">${escapeHtml(label)}</div>${hintHtml}</div>`;
80
+ }
81
+ function stageBody(stage, body) {
82
+ return stage.ok ? body(stage.data) : notMeasuredNote();
83
+ }
84
+ function notMeasuredNote() {
85
+ return `<p class="muted">Not measured \u2014 ${STAGE_FAILED_MESSAGE}.</p>`;
86
+ }
87
+ function skippableStageNote(error, skipConstant, skipMessage) {
88
+ if (error === skipConstant) {
89
+ return `<p class="muted">${escapeHtml(skipMessage)}</p>`;
90
+ }
91
+ return notMeasuredNote();
92
+ }
93
+ function formatIsoDate(iso) {
94
+ const d = new Date(iso);
95
+ if (Number.isNaN(d.getTime())) return iso;
96
+ return d.toLocaleDateString("en-US", {
97
+ year: "numeric",
98
+ month: "long",
99
+ day: "numeric",
100
+ timeZone: "UTC"
101
+ });
102
+ }
103
+ function buildProbesSection(p, businessNameUsed) {
104
+ const byKind = /* @__PURE__ */ new Map();
105
+ for (const a of p.answers) {
106
+ const bucket = byKind.get(a.kind) ?? [];
107
+ bucket.push(a);
108
+ byKind.set(a.kind, bucket);
109
+ }
110
+ const recognition = p.brandedRecognized ? "<p>When the engines were asked about the business by name, they recognized it.</p>" : businessNameUsed ? "<p>When the engines were asked about the business by name, they did not recognize it \u2014 a real citation of the site never showed up, even with the name handed to them.</p>" : "<p>The engines did not recognize the business \u2014 there was no business name available to ask them with, so this reflects a harder starting point than a named query would.</p>";
111
+ const categoryCaveat = byKind.has("category") ? "" : `<p class="muted"><strong>No buyer-question (category) query was tested here</strong> \u2014 only name-recognition. An engine echoing back a name it was just given says nothing about whether a buyer who had never heard of the business would be shown it; treat the line above as a floor, not a visibility signal.</p>`;
112
+ const groups = KIND_ORDER.filter((k) => byKind.has(k)).map((kind) => {
113
+ const answers = byKind.get(kind) ?? [];
114
+ const cards = answers.map(
115
+ (a) => `<div class="card">
116
+ <div class="q">${escapeHtml(a.engine)} \xB7 \u201C${escapeHtml(a.query)}\u201D</div>
117
+ <p class="muted">Asked ${escapeHtml(formatIsoDate(a.askedAt))}</p>
118
+ <p>${escapeHtml(a.snippet)}${a.truncated ? "\u2026" : ""}</p>
119
+ <p class="muted">${a.domainCited || a.brandMentioned ? "You were named in this answer." : "You were not named in this answer."}${a.citedDomains.length ? ` Cited: ${escapeHtml(a.citedDomains.join(", "))}` : ""}</p>
120
+ </div>`
121
+ ).join("");
122
+ return `<h3>${escapeHtml(KIND_LABEL[kind])}</h3>${cards}`;
123
+ }).join("");
124
+ const competitors = p.competitorsSeen.length ? `<h3>Who the engines cited instead</h3><ul>${p.competitorsSeen.map((c) => `<li>${escapeHtml(c.domain)} \u2014 ${c.count} time${c.count === 1 ? "" : "s"}</li>`).join("")}</ul>` : "";
125
+ return recognition + categoryCaveat + groups + competitors;
126
+ }
127
+ function buildFindabilitySection(c) {
128
+ const accessBlock = !c.crawlerAccessMeasured ? `<p class="muted">Crawler access: not measured \u2014 ${STAGE_FAILED_MESSAGE}.</p>` : c.crawlerAccess.blockedAi.length ? `<p><strong>Blocked AI crawlers:</strong> ${escapeHtml(
129
+ c.crawlerAccess.blockedAi.map(describeAgent).join(", ")
130
+ )}</p>` : `<p>Every AI crawler we checked can reach the site.</p>`;
131
+ const classical = c.crawlerAccessMeasured && c.crawlerAccess.blockedClassical.length ? `<p><strong>Blocked search crawlers:</strong> ${escapeHtml(
132
+ c.crawlerAccess.blockedClassical.join(", ")
133
+ )}</p>` : "";
134
+ const sitemapLine = c.sitemapMeasured ? `<li>sitemap.xml: ${c.sitemapPresent ? "present" : "missing"}</li>` : `<li>sitemap.xml: not measured \u2014 ${STAGE_FAILED_MESSAGE}.</li>`;
135
+ const llmsLine = c.llmsTxtMeasured ? `<li>llms.txt: ${c.llmsTxtPresent ? "present" : "missing"}</li>` : `<li>llms.txt: not measured \u2014 ${STAGE_FAILED_MESSAGE}.</li>`;
136
+ return `${accessBlock}${classical}
137
+ <ul>
138
+ ${sitemapLine}
139
+ ${llmsLine}
140
+ <li>Pages missing a meta description: ${c.meta.missingDescription} of ${c.meta.pageCount}</li>
141
+ <li>Pages missing a canonical URL: ${c.meta.missingCanonical} of ${c.meta.pageCount}</li>
142
+ <li>Pages missing share images/titles: ${c.meta.missingSocial} of ${c.meta.pageCount}</li>
143
+ <li>Security headers missing: ${c.securityHeaders.missing.length ? escapeHtml(c.securityHeaders.missing.join(", ")) : "none"}</li>
144
+ ${c.meta.pagesWithoutExtract > 0 ? `<li><strong>${c.meta.pagesWithoutExtract}</strong> additional page${c.meta.pagesWithoutExtract === 1 ? "" : "s"} produced no readable content at all and ${c.meta.pagesWithoutExtract === 1 ? "is" : "are"} not counted in the ${c.meta.pageCount} pages above.</li>` : ""}
145
+ </ul>`;
146
+ }
147
+ function buildLighthouseBlock(lh) {
148
+ return lh.ok ? `<p class="muted">Lighthouse \u2014 performance ${lh.data.performance ?? "not measured"},
149
+ SEO ${lh.data.seo ?? "not measured"}, accessibility ${lh.data.accessibility ?? "not measured"}.</p>` : `<p class="muted">Lighthouse not measured \u2014 ${STAGE_FAILED_MESSAGE}.</p>`;
150
+ }
151
+ function buildReadabilitySection(c) {
152
+ const jsLine = c.jsDependence.avgMissing === null ? `<p class="muted">JavaScript-dependence: not measured \u2014 no page produced a comparable raw/rendered pair.</p>` : `<p><strong>${Math.round(c.jsDependence.avgMissing * 100)}%</strong> of the words a visitor reads only appear after JavaScript runs.
153
+ Most AI crawlers do not run JavaScript, so that share of your site is invisible to them.</p>`;
154
+ return `${jsLine}
155
+ <ul>
156
+ <li>Structured data found: ${c.schema.typesFound.length ? escapeHtml(c.schema.typesFound.join(", ")) : "none"}</li>
157
+ <li>Expected structured data missing: ${c.schema.missingExpected.length ? escapeHtml(c.schema.missingExpected.join(", ")) : "none"}</li>
158
+ <li>Pages without a top-level heading: ${c.headings.pagesWithoutH1} of ${c.meta.pageCount}</li>
159
+ ${c.meta.pagesWithoutExtract > 0 ? `<li><strong>${c.meta.pagesWithoutExtract}</strong> page${c.meta.pagesWithoutExtract === 1 ? "" : "s"} produced no extract at all \u2014 excluded from every figure above, not counted as readable or unreadable.</li>` : ""}
160
+ </ul>`;
161
+ }
162
+ function buildAnswersSection(a) {
163
+ const rows = a.buyerQuestions.map((q) => {
164
+ const answered = escapeHtml(q.answered);
165
+ const evidenceText = q.evidence ? escapeHtml(q.evidence) : null;
166
+ const evidenceHref = q.evidence && q.page ? safeUrl(q.page) : null;
167
+ const evidenceCell = evidenceText === null ? '<span class="muted">no passage on the site</span>' : evidenceHref && evidenceHref !== "#" ? `<a href="${escapeHtml(evidenceHref)}" target="_blank" rel="noopener noreferrer">${evidenceText}</a>` : evidenceText;
168
+ return `<tr>
169
+ <td>${escapeHtml(q.question)}</td>
170
+ <td><span class="tag ${answered}">${answered}</span></td>
171
+ <td>${evidenceCell}</td>
172
+ </tr>`;
173
+ }).join("");
174
+ return `<p>${escapeHtml(a.narrative.answers)}</p>
175
+ <table><tr><th>What buyers ask</th><th>Answered</th><th>Evidence</th></tr>${rows}</table>`;
176
+ }
177
+ function renderProspectReport(result) {
178
+ const host = hostnameOf(result.url);
179
+ const name = result.businessName && result.businessName.trim() ? result.businessName : host;
180
+ const date = formatIsoDate(result.generatedAt);
181
+ const findabilitySection = stageBody(result.checks, buildFindabilitySection);
182
+ const lighthouseBlock = buildLighthouseBlock(result.lighthouse);
183
+ const readabilitySection = stageBody(result.checks, buildReadabilitySection);
184
+ const businessNameUsed = Boolean(result.businessName && result.businessName.trim());
185
+ const fixes = result.analyze.ok ? (() => {
186
+ const sorted = [...result.analyze.data.fixes].sort(
187
+ (x, y) => IMPACT_ORDER[x.impact] - IMPACT_ORDER[y.impact]
188
+ );
189
+ if (sorted.length === 0) {
190
+ return `<p>Nothing here reads as urgent \u2014 the essentials are already in place.
191
+ Reddoor can still help push this further: tightening structured data, monitoring
192
+ how the answer engines describe you, and keeping pace as they change.</p>`;
193
+ }
194
+ return `<ol>${sorted.map((f) => {
195
+ const impact = escapeHtml(f.impact);
196
+ const effort = escapeHtml(f.effort);
197
+ return `<li><strong>${escapeHtml(f.title)}</strong> \u2014 ${escapeHtml(f.why)}
198
+ <span class="muted">(${impact} impact, ${effort} effort)</span></li>`;
199
+ }).join("")}</ol>`;
200
+ })() : skippableStageNote(
201
+ result.analyze.error,
202
+ ANALYZE_SKIPPED,
203
+ "Skipped \u2014 the checks stage failed, so there was nothing to build fixes from."
204
+ );
205
+ const probesSectionFinal = result.probes.ok ? buildProbesSection(result.probes.data, businessNameUsed) : skippableStageNote(
206
+ result.probes.error,
207
+ PROBES_SKIPPED,
208
+ "This audit did not run the AI-visibility probes."
209
+ );
210
+ const answersSectionFinal = result.analyze.ok ? buildAnswersSection(result.analyze.data) : skippableStageNote(
211
+ result.analyze.error,
212
+ ANALYZE_SKIPPED,
213
+ "Skipped \u2014 the checks stage failed, so there were no buyer questions to check."
214
+ );
215
+ const description = result.analyze.ok ? result.analyze.data.business : null;
216
+ const narrative = result.analyze.ok ? `<p class="lede">${escapeHtml(description ?? "")}</p>
217
+ <p class="lede">${escapeHtml(result.analyze.data.narrative.findability)}
218
+ ${escapeHtml(result.analyze.data.narrative.readability)}</p>` : "";
219
+ return `<!doctype html>
220
+ <html lang="en">
221
+ <head>
222
+ <meta charset="utf-8" />
223
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
224
+ <meta name="robots" content="noindex" />
225
+ <title>Can AI find ${escapeHtml(name)}? \u2014 Reddoor audit</title>
226
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
227
+ <link href="https://fonts.googleapis.com/css2?family=Besley:wght@400;600&display=swap" rel="stylesheet" />
228
+ <style>${STYLES}</style>
229
+ </head>
230
+ <body>
231
+ <div class="wrap">
232
+ <h1>Can AI and Google actually find ${escapeHtml(name)}?</h1>
233
+ <p class="lede"><a href="${escapeHtml(safeUrl(result.url))}" target="_blank" rel="noopener noreferrer">${escapeHtml(result.url)}</a> \xB7 audited ${escapeHtml(date)} by Reddoor Creative</p>
234
+ ${narrative}
235
+
236
+ <p class="muted">Each score below runs 0\u2013100 \u2014 read it as a score, not a percentage.</p>
237
+ <div class="scores">
238
+ ${scoreCard("Findability", result.scores.findability, "How easily AI and search crawlers can find and reach your site \u2014 crawl rules, sitemap, key metadata")}
239
+ ${scoreCard("Readability", result.scores.readability, "How much of your site's content those crawlers can actually read once they're in \u2014 most don't run JavaScript")}
240
+ ${scoreCard("Answers", result.scores.answers, "How many buyer questions your site's own content answers \u2014 separate from what the AI engines say back")}
241
+ ${scoreCard("AI Visibility", result.scores.aiVisibility, "Based on buyer questions \u2014 not questions that name the business directly")}
242
+ </div>
243
+
244
+ <h2>What the AI engines said about you</h2>
245
+ ${probesSectionFinal}
246
+
247
+ <h2>What the crawlers can reach</h2>
248
+ ${findabilitySection}
249
+ ${lighthouseBlock}
250
+
251
+ <h2>What the crawlers can read</h2>
252
+ ${readabilitySection}
253
+
254
+ <h2>The questions your buyers ask</h2>
255
+ ${answersSectionFinal}
256
+
257
+ <h2>What to fix first</h2>
258
+ ${fixes}
259
+
260
+ <div class="cta">
261
+ <h2>Want this fixed?</h2>
262
+ <p>Reddoor Creative rebuilds sites so answer engines can read, quote and recommend them.
263
+ Reply to the email this link came from, or start at
264
+ <a href="https://reddoorla.com/">reddoorla.com</a>.</p>
265
+ </div>
266
+ </div>
267
+ </body>
268
+ </html>`;
269
+ }
270
+
271
+ export {
272
+ renderProspectReport
273
+ };
274
+ //# sourceMappingURL=chunk-W7Z3BGCI.js.map