@reddoorla/maintenance 0.85.1 → 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 (252) 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-QRDB4NLB.js → chunk-2ZPT7GLV.js} +43 -404
  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-N4MOEF32.js → chunk-C6B4RHXR.js} +36 -8
  32. package/dist/chunk-C6B4RHXR.js.map +1 -0
  33. package/dist/{chunk-6QRLRT3P.js → chunk-CQR4LIM7.js} +12 -265
  34. package/dist/chunk-CQR4LIM7.js.map +1 -0
  35. package/dist/{chunk-SEEA6RXF.js → chunk-DJZWYIPP.js} +2 -2
  36. package/dist/chunk-DJZWYIPP.js.map +1 -0
  37. package/dist/chunk-EVBCK2KC.js +68 -0
  38. package/dist/chunk-EVBCK2KC.js.map +1 -0
  39. package/dist/{chunk-TP5M2NQB.js → chunk-F5KM4LEQ.js} +3 -3
  40. package/dist/{chunk-62VF32IC.js → chunk-ILUNZHYV.js} +2 -2
  41. package/dist/chunk-ITIOFN7A.js +396 -0
  42. package/dist/chunk-ITIOFN7A.js.map +1 -0
  43. package/dist/{chunk-LS6QPANO.js → chunk-IYSEC543.js} +2 -2
  44. package/dist/{chunk-DC5H7KEQ.js → chunk-JHGHLKFS.js} +2 -2
  45. package/dist/{chunk-PBWDDDQH.js → chunk-KTH5ZODA.js} +2 -2
  46. package/dist/{chunk-4DHQCN5H.js → chunk-LBYOLBW7.js} +17 -2
  47. package/dist/chunk-LBYOLBW7.js.map +1 -0
  48. package/dist/{chunk-577HCGZO.js → chunk-LDWZSLSU.js} +38 -23
  49. package/dist/chunk-LDWZSLSU.js.map +1 -0
  50. package/dist/{chunk-LTB6KVJW.js → chunk-N45M3FDK.js} +13 -11
  51. package/dist/{chunk-LTB6KVJW.js.map → chunk-N45M3FDK.js.map} +1 -1
  52. package/dist/chunk-NYUYNYQL.js +48 -0
  53. package/dist/chunk-NYUYNYQL.js.map +1 -0
  54. package/dist/{chunk-7J53SUT6.js → chunk-P2FE5AI6.js} +4 -4
  55. package/dist/{chunk-3YXDSCM6.js → chunk-PPE5KZJK.js} +29 -11
  56. package/dist/chunk-PPE5KZJK.js.map +1 -0
  57. package/dist/chunk-PTDMVGKL.js +31 -0
  58. package/dist/chunk-PTDMVGKL.js.map +1 -0
  59. package/dist/{chunk-D4FVAIEJ.js → chunk-PZR4EBPY.js} +5 -2
  60. package/dist/chunk-PZR4EBPY.js.map +1 -0
  61. package/dist/{chunk-NWB4O5EY.js → chunk-SLUQ7R6I.js} +3 -3
  62. package/dist/chunk-SLUQ7R6I.js.map +1 -0
  63. package/dist/{chunk-GZNBCHAZ.js → chunk-TERE5SDF.js} +4 -4
  64. package/dist/{chunk-ZMIPG6TR.js → chunk-TVL5MVUF.js} +4 -4
  65. package/dist/chunk-UDRHAQT7.js +172 -0
  66. package/dist/chunk-UDRHAQT7.js.map +1 -0
  67. package/dist/chunk-VAD43MOR.js +72 -0
  68. package/dist/chunk-VAD43MOR.js.map +1 -0
  69. package/dist/chunk-VSBFM7DS.js +229 -0
  70. package/dist/chunk-VSBFM7DS.js.map +1 -0
  71. package/dist/chunk-W7Z3BGCI.js +274 -0
  72. package/dist/chunk-W7Z3BGCI.js.map +1 -0
  73. package/dist/chunk-X3B723J4.js +1209 -0
  74. package/dist/chunk-X3B723J4.js.map +1 -0
  75. package/dist/{chunk-VRWUL4TR.js → chunk-XHHMM5HX.js} +2 -2
  76. package/dist/{chunk-RJB3QUSP.js → chunk-XIDUYLSR.js} +3 -3
  77. package/dist/{chunk-QG6OSL4M.js → chunk-XUGSZP7Z.js} +2 -2
  78. package/dist/{chunk-R4AB6GCT.js → chunk-Y53WOQIJ.js} +7 -4
  79. package/dist/{chunk-R4AB6GCT.js.map → chunk-Y53WOQIJ.js.map} +1 -1
  80. package/dist/{chunk-ICXV25N7.js → chunk-Z3KYFUN6.js} +3 -3
  81. package/dist/{chunk-MPBXOJ3N.js → chunk-Z7AXYKQB.js} +33 -2
  82. package/dist/chunk-Z7AXYKQB.js.map +1 -0
  83. package/dist/{chunk-6V7565O2.js → chunk-ZKXSX4LO.js} +11 -11
  84. package/dist/cli/bin.js +46 -28
  85. package/dist/cli/bin.js.map +1 -1
  86. package/dist/cli/commands/audit.d.ts +30 -1
  87. package/dist/cli/commands/audit.js +63 -31
  88. package/dist/cli/commands/audit.js.map +1 -1
  89. package/dist/client-AUE3XUPO.js +11 -0
  90. package/dist/configs/playwright-a11y.js +1 -1
  91. package/dist/configs/svelte.d.ts +30 -1
  92. package/dist/configs/svelte.js +9 -1
  93. package/dist/configs/svelte.js.map +1 -1
  94. package/dist/{convert-to-pnpm-AT52NWZB.js → convert-to-pnpm-6L6P74WR.js} +9 -9
  95. package/dist/db-B35OGV3G.js +217 -0
  96. package/dist/db-B35OGV3G.js.map +1 -0
  97. package/dist/{digest-UVHYZUYE.js → digest-BL6UJYKH.js} +23 -20
  98. package/dist/{digest-UVHYZUYE.js.map → digest-BL6UJYKH.js.map} +1 -1
  99. package/dist/dump-P6EBSIOH.js +68 -0
  100. package/dist/dump-P6EBSIOH.js.map +1 -0
  101. package/dist/email-VTTTKKL5.js +195 -0
  102. package/dist/email-VTTTKKL5.js.map +1 -0
  103. package/dist/{ensure-site-S25MDDHW.js → ensure-site-DFTB7MN3.js} +8 -7
  104. package/dist/ensure-site-DFTB7MN3.js.map +1 -0
  105. package/dist/fleet-events-cF2Om88w.d.ts +24 -0
  106. package/dist/{forms-notify-target-TWN4EH6K.js → forms-notify-target-75U6OU7L.js} +38 -132
  107. package/dist/forms-notify-target-75U6OU7L.js.map +1 -0
  108. package/dist/{github-signals-G4ILZAJ6.js → github-signals-IJUPGCUX.js} +40 -19
  109. package/dist/github-signals-IJUPGCUX.js.map +1 -0
  110. package/dist/{header-image-T7XD37JL.js → header-image-AWLLQA4L.js} +34 -11
  111. package/dist/header-image-AWLLQA4L.js.map +1 -0
  112. package/dist/header-images-335L2AKB.js +68 -0
  113. package/dist/header-images-335L2AKB.js.map +1 -0
  114. package/dist/{health-endpoint-242WG2OJ.js → health-endpoint-WJC2UMYS.js} +9 -9
  115. package/dist/health-mirror-4B2L7T2Y.js +75 -0
  116. package/dist/health-mirror-4B2L7T2Y.js.map +1 -0
  117. package/dist/import-airtable-HE43FB4H.js +35 -0
  118. package/dist/index.d.ts +175 -21
  119. package/dist/index.js +116 -74
  120. package/dist/index.js.map +1 -1
  121. package/dist/{init-TFVAN5OY.js → init-NJEOEO7I.js} +21 -18
  122. package/dist/{init-TFVAN5OY.js.map → init-NJEOEO7I.js.map} +1 -1
  123. package/dist/{launch-DWY2MF54.js → launch-XNAX427U.js} +32 -26
  124. package/dist/{launch-DWY2MF54.js.map → launch-XNAX427U.js.map} +1 -1
  125. package/dist/lighthouse-VHI77PFB.js +11 -0
  126. package/dist/{lighthouse-airtable-BIPM26SZ.js → lighthouse-airtable-LBVIYYP5.js} +3 -3
  127. package/dist/mailchimp-XYCTEEJY.js +72 -0
  128. package/dist/mailchimp-XYCTEEJY.js.map +1 -0
  129. package/dist/migrate-GQH237A6.js +7 -0
  130. package/dist/notify-C645PGD5.js +22 -0
  131. package/dist/{onboard-53CITIM7.js → onboard-GK4PHPNI.js} +9 -9
  132. package/dist/orchestrate-77UO4BYJ.js +30 -0
  133. package/dist/orchestrate-77UO4BYJ.js.map +1 -0
  134. package/dist/parity-MWS24A3H.js +12 -0
  135. package/dist/parity-MWS24A3H.js.map +1 -0
  136. package/dist/pipeline-S6CVHDGB.js +12 -0
  137. package/dist/pipeline-S6CVHDGB.js.map +1 -0
  138. package/dist/{preflight-3XPDUHFJ.js → preflight-6WBNU2GZ.js} +16 -13
  139. package/dist/{preflight-3XPDUHFJ.js.map → preflight-6WBNU2GZ.js.map} +1 -1
  140. package/dist/{prismic-ci-UTQ6LRR3.js → prismic-ci-3RHOZUUT.js} +15 -15
  141. package/dist/{prismic-models-PVES6R5V.js → prismic-models-QBGUT2Q3.js} +10 -10
  142. package/dist/prospect-audit-IUNCJ2LV.js +174 -0
  143. package/dist/prospect-audit-IUNCJ2LV.js.map +1 -0
  144. package/dist/prospect-audits-3PMIO73D.js +19 -0
  145. package/dist/prospect-audits-3PMIO73D.js.map +1 -0
  146. package/dist/{protection-audit-64PLLBLI.js → protection-audit-SJO53MEQ.js} +3 -3
  147. package/dist/recipes/sync-configs.js +3 -3
  148. package/dist/render-AQAVQDPZ.js +10 -0
  149. package/dist/render-AQAVQDPZ.js.map +1 -0
  150. package/dist/{renovate-dispatch-ICJGSFCE.js → renovate-dispatch-QJY75ODR.js} +8 -8
  151. package/dist/replay-C2LXMJZP.js +66 -0
  152. package/dist/replay-C2LXMJZP.js.map +1 -0
  153. package/dist/{report-6CKSWJNG.js → report-4EVNDHOZ.js} +55 -29
  154. package/dist/report-4EVNDHOZ.js.map +1 -0
  155. package/dist/reports/header-image/assets/headline-announcement.png +0 -0
  156. package/dist/reports/header-image/assets/headline-launch.png +0 -0
  157. package/dist/reports/header-image/assets/headline-maintenance.png +0 -0
  158. package/dist/reports/header-image/assets/headline-testing.png +0 -0
  159. package/dist/reports/header-image/assets/plate-clean.png +0 -0
  160. package/dist/{self-updating-PPQVV6UA.js → self-updating-6DWI2Z56.js} +9 -9
  161. package/dist/{selftest-CQ6NOX52.js → selftest-RC36MTKP.js} +25 -20
  162. package/dist/selftest-RC36MTKP.js.map +1 -0
  163. package/dist/smoke-B2GQOSIZ.js +20 -0
  164. package/dist/smoke-B2GQOSIZ.js.map +1 -0
  165. package/dist/{smoke-suite-OBWNPALQ.js → smoke-suite-VYY2LXM2.js} +9 -9
  166. package/dist/{submissions-GJ6ITBRH.js → submissions-Q7DRFOCD.js} +3 -3
  167. package/dist/{svelte-codemods-ZOBDHIES.js → svelte-codemods-25J2FO22.js} +9 -9
  168. package/dist/sync-34HNZZ56.js +44 -0
  169. package/dist/sync-34HNZZ56.js.map +1 -0
  170. package/dist/{sync-configs-CJXUX6MD.js → sync-configs-3SE6PSCO.js} +9 -9
  171. package/dist/{upgrade-WLX27UPC.js → upgrade-WBRQ62FX.js} +9 -9
  172. package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
  173. package/dist/webhook-VOGOW3W6.js +38 -0
  174. package/dist/webhook-VOGOW3W6.js.map +1 -0
  175. package/dist/{websites-SADZR4XQ.js → websites-SXWTP72S.js} +6 -2
  176. package/dist/websites-SXWTP72S.js.map +1 -0
  177. package/dist/{write-audits-to-airtable-7PH75YWY.js → write-audits-to-airtable-MBEILLPU.js} +4 -4
  178. package/dist/write-audits-to-airtable-MBEILLPU.js.map +1 -0
  179. package/package.json +5 -2
  180. package/dist/airtable-HRKSA7V4.js +0 -9
  181. package/dist/announce-GIAYN67S.js.map +0 -1
  182. package/dist/chunk-37S6L6G4.js.map +0 -1
  183. package/dist/chunk-3YXDSCM6.js.map +0 -1
  184. package/dist/chunk-4DHQCN5H.js.map +0 -1
  185. package/dist/chunk-4SOCPK7A.js +0 -108
  186. package/dist/chunk-4SOCPK7A.js.map +0 -1
  187. package/dist/chunk-577HCGZO.js.map +0 -1
  188. package/dist/chunk-6QRLRT3P.js.map +0 -1
  189. package/dist/chunk-COZPL62X.js.map +0 -1
  190. package/dist/chunk-D4FVAIEJ.js.map +0 -1
  191. package/dist/chunk-MGR2H7F4.js.map +0 -1
  192. package/dist/chunk-MPBXOJ3N.js.map +0 -1
  193. package/dist/chunk-N4MOEF32.js.map +0 -1
  194. package/dist/chunk-NWB4O5EY.js.map +0 -1
  195. package/dist/chunk-QRDB4NLB.js.map +0 -1
  196. package/dist/chunk-SEEA6RXF.js.map +0 -1
  197. package/dist/chunk-U5SZ4FOP.js +0 -26
  198. package/dist/chunk-U5SZ4FOP.js.map +0 -1
  199. package/dist/client-X5WUJWDE.js +0 -11
  200. package/dist/db-JSIZYT23.js +0 -23
  201. package/dist/db-JSIZYT23.js.map +0 -1
  202. package/dist/ensure-site-S25MDDHW.js.map +0 -1
  203. package/dist/forms-notify-target-TWN4EH6K.js.map +0 -1
  204. package/dist/github-signals-G4ILZAJ6.js.map +0 -1
  205. package/dist/header-image-T7XD37JL.js.map +0 -1
  206. package/dist/migrate-LJUFGXQZ.js +0 -7
  207. package/dist/orchestrate-KDNXZQX7.js +0 -27
  208. package/dist/report-6CKSWJNG.js.map +0 -1
  209. package/dist/reports/header-image/assets/plate.png +0 -0
  210. package/dist/selftest-CQ6NOX52.js.map +0 -1
  211. /package/dist/{airtable-HRKSA7V4.js.map → airtable-G7WBQQMI.js.map} +0 -0
  212. /package/dist/{bump-deps-SWJB6H5V.js.map → bump-deps-EC74CPLS.js.map} +0 -0
  213. /package/dist/{chunk-SDUJIANJ.js.map → chunk-2NJQLSEU.js.map} +0 -0
  214. /package/dist/{chunk-ZTPPCHDB.js.map → chunk-3G25KIWW.js.map} +0 -0
  215. /package/dist/{chunk-Q276MZ7E.js.map → chunk-3VQWSE3F.js.map} +0 -0
  216. /package/dist/{chunk-THEFTZ7F.js.map → chunk-62CMMGEO.js.map} +0 -0
  217. /package/dist/{chunk-FDIFTIXC.js.map → chunk-6TWWY7ZU.js.map} +0 -0
  218. /package/dist/{chunk-UYCAF77E.js.map → chunk-7RJ2HMMJ.js.map} +0 -0
  219. /package/dist/{chunk-R6FPFMOS.js.map → chunk-BQRFQSGY.js.map} +0 -0
  220. /package/dist/{chunk-TP5M2NQB.js.map → chunk-F5KM4LEQ.js.map} +0 -0
  221. /package/dist/{chunk-62VF32IC.js.map → chunk-ILUNZHYV.js.map} +0 -0
  222. /package/dist/{chunk-LS6QPANO.js.map → chunk-IYSEC543.js.map} +0 -0
  223. /package/dist/{chunk-DC5H7KEQ.js.map → chunk-JHGHLKFS.js.map} +0 -0
  224. /package/dist/{chunk-PBWDDDQH.js.map → chunk-KTH5ZODA.js.map} +0 -0
  225. /package/dist/{chunk-7J53SUT6.js.map → chunk-P2FE5AI6.js.map} +0 -0
  226. /package/dist/{chunk-GZNBCHAZ.js.map → chunk-TERE5SDF.js.map} +0 -0
  227. /package/dist/{chunk-ZMIPG6TR.js.map → chunk-TVL5MVUF.js.map} +0 -0
  228. /package/dist/{chunk-VRWUL4TR.js.map → chunk-XHHMM5HX.js.map} +0 -0
  229. /package/dist/{chunk-RJB3QUSP.js.map → chunk-XIDUYLSR.js.map} +0 -0
  230. /package/dist/{chunk-QG6OSL4M.js.map → chunk-XUGSZP7Z.js.map} +0 -0
  231. /package/dist/{chunk-ICXV25N7.js.map → chunk-Z3KYFUN6.js.map} +0 -0
  232. /package/dist/{chunk-6V7565O2.js.map → chunk-ZKXSX4LO.js.map} +0 -0
  233. /package/dist/{client-X5WUJWDE.js.map → client-AUE3XUPO.js.map} +0 -0
  234. /package/dist/{convert-to-pnpm-AT52NWZB.js.map → convert-to-pnpm-6L6P74WR.js.map} +0 -0
  235. /package/dist/{health-endpoint-242WG2OJ.js.map → health-endpoint-WJC2UMYS.js.map} +0 -0
  236. /package/dist/{lighthouse-airtable-BIPM26SZ.js.map → import-airtable-HE43FB4H.js.map} +0 -0
  237. /package/dist/{migrate-LJUFGXQZ.js.map → lighthouse-VHI77PFB.js.map} +0 -0
  238. /package/dist/{orchestrate-KDNXZQX7.js.map → lighthouse-airtable-LBVIYYP5.js.map} +0 -0
  239. /package/dist/{websites-SADZR4XQ.js.map → migrate-GQH237A6.js.map} +0 -0
  240. /package/dist/{write-audits-to-airtable-7PH75YWY.js.map → notify-C645PGD5.js.map} +0 -0
  241. /package/dist/{onboard-53CITIM7.js.map → onboard-GK4PHPNI.js.map} +0 -0
  242. /package/dist/{prismic-ci-UTQ6LRR3.js.map → prismic-ci-3RHOZUUT.js.map} +0 -0
  243. /package/dist/{prismic-models-PVES6R5V.js.map → prismic-models-QBGUT2Q3.js.map} +0 -0
  244. /package/dist/{protection-audit-64PLLBLI.js.map → protection-audit-SJO53MEQ.js.map} +0 -0
  245. /package/dist/{renovate-dispatch-ICJGSFCE.js.map → renovate-dispatch-QJY75ODR.js.map} +0 -0
  246. /package/dist/{self-updating-PPQVV6UA.js.map → self-updating-6DWI2Z56.js.map} +0 -0
  247. /package/dist/{smoke-suite-OBWNPALQ.js.map → smoke-suite-VYY2LXM2.js.map} +0 -0
  248. /package/dist/{submissions-GJ6ITBRH.js.map → submissions-Q7DRFOCD.js.map} +0 -0
  249. /package/dist/{svelte-codemods-ZOBDHIES.js.map → svelte-codemods-25J2FO22.js.map} +0 -0
  250. /package/dist/{sync-configs-CJXUX6MD.js.map → sync-configs-3SE6PSCO.js.map} +0 -0
  251. /package/dist/{upgrade-WLX27UPC.js.map → upgrade-WBRQ62FX.js.map} +0 -0
  252. /package/dist/{webflow-624FEBFD.js.map → webflow-IRKOIT7S.js.map} +0 -0
@@ -3,30 +3,30 @@ import {
3
3
  } from "./chunk-PZQ3WED2.js";
4
4
  import {
5
5
  formatFleetWriteSummary
6
- } from "./chunk-3YXDSCM6.js";
7
- import "./chunk-PBWDDDQH.js";
6
+ } from "./chunk-PPE5KZJK.js";
7
+ import "./chunk-KTH5ZODA.js";
8
8
  import {
9
9
  detectSignalEvents,
10
10
  fleetSweptEvent
11
11
  } from "./chunk-5L3FPQIJ.js";
12
+ import {
13
+ openBase,
14
+ readAirtableConfig
15
+ } from "./chunk-HN6I2XWI.js";
12
16
  import {
13
17
  makeGitHub
14
- } from "./chunk-VRWUL4TR.js";
18
+ } from "./chunk-XHHMM5HX.js";
15
19
  import {
16
20
  recordFleetEventsBestEffort
17
- } from "./chunk-QG6OSL4M.js";
18
- import "./chunk-DC5H7KEQ.js";
19
- import "./chunk-4SOCPK7A.js";
20
- import "./chunk-4DHQCN5H.js";
21
+ } from "./chunk-XUGSZP7Z.js";
22
+ import "./chunk-JHGHLKFS.js";
23
+ import "./chunk-3SF7D4DY.js";
24
+ import "./chunk-LBYOLBW7.js";
21
25
  import {
22
26
  listWebsites,
23
27
  siteSlug,
24
28
  updateGitHubSignals
25
- } from "./chunk-577HCGZO.js";
26
- import {
27
- openBase,
28
- readAirtableConfig
29
- } from "./chunk-HN6I2XWI.js";
29
+ } from "./chunk-LDWZSLSU.js";
30
30
  import "./chunk-6VAL7XJT.js";
31
31
 
32
32
  // src/audits/github-signals.ts
@@ -59,7 +59,7 @@ async function collectGitHubSignals(sites, deps, onSkip = () => {
59
59
  function githubSignalsExitCode(written, failed) {
60
60
  return failed > written ? 1 : 0;
61
61
  }
62
- async function runGitHubSignalsCommand(opts) {
62
+ async function runGitHubSignalsCommand(opts, deps = {}) {
63
63
  if (!opts.fleet || !opts.writeAirtable) {
64
64
  return { output: "github-signals currently supports only --fleet --write-airtable", code: 2 };
65
65
  }
@@ -70,9 +70,9 @@ async function runGitHubSignalsCommand(opts) {
70
70
  code: 0
71
71
  };
72
72
  }
73
- const base = openBase(readAirtableConfig());
73
+ const base = deps.openBase ? deps.openBase() : openBase(readAirtableConfig());
74
74
  const websites = await listWebsites(base);
75
- const gh = makeGitHub({ token });
75
+ const gh = deps.makeGh ? deps.makeGh(token) : makeGitHub({ token });
76
76
  const sites = websites.map((w) => ({
77
77
  path: "",
78
78
  name: w.name,
@@ -89,7 +89,16 @@ async function runGitHubSignalsCommand(opts) {
89
89
  ({ repo }) => skipped.push(repo)
90
90
  );
91
91
  const sweptAt = (/* @__PURE__ */ new Date()).toISOString();
92
- const result = { written: [], failed: [] };
92
+ const makeMirror = deps.makeMirror ?? (async () => {
93
+ const { makeHealthMirrorBestEffort } = await import("./health-mirror-4B2L7T2Y.js");
94
+ return makeHealthMirrorBestEffort();
95
+ });
96
+ const mirror = await makeMirror();
97
+ const result = {
98
+ written: [],
99
+ failed: [],
100
+ ...mirror ? { mirrored: 0, mirrorFailed: 0, mirrorMissed: 0 } : {}
101
+ };
93
102
  const byRepo = new Map(websites.filter((w) => w.gitRepo).map((w) => [w.gitRepo, w]));
94
103
  const events = [];
95
104
  const sweptMs = Date.parse(sweptAt);
@@ -101,12 +110,24 @@ async function runGitHubSignalsCommand(opts) {
101
110
  continue;
102
111
  }
103
112
  try {
104
- await updateGitHubSignals(base, target.id, {
113
+ const ghFields = await updateGitHubSignals(base, target.id, {
105
114
  renovateFailingCis: row.renovateFailingCis,
106
115
  ciState: row.ciState,
107
116
  lastCommitAt: row.lastCommitAt,
108
117
  sweptAt
109
118
  });
119
+ if (mirror) {
120
+ try {
121
+ if (await mirror(target.id, ghFields)) {
122
+ result.mirrored = (result.mirrored ?? 0) + 1;
123
+ } else {
124
+ result.mirrorMissed = (result.mirrorMissed ?? 0) + 1;
125
+ }
126
+ } catch (e) {
127
+ result.mirrorFailed = (result.mirrorFailed ?? 0) + 1;
128
+ console.error(`[health-mirror] ${target.name}: ${e.message}`);
129
+ }
130
+ }
110
131
  result.written.push({
111
132
  siteName: target.name,
112
133
  writes: [{ audit: "github-signals", counts: row }]
@@ -124,7 +145,7 @@ async function runGitHubSignalsCommand(opts) {
124
145
  }
125
146
  for (const repo of skipped) result.failed.push({ slug: repo, error: "probe failed (skipped)" });
126
147
  events.push(fleetSweptEvent("github-signals", result.written.length, sweptAt));
127
- await recordFleetEventsBestEffort(events, /* @__PURE__ */ new Date());
148
+ await (deps.recordEvents ?? recordFleetEventsBestEffort)(events, /* @__PURE__ */ new Date());
128
149
  return {
129
150
  output: formatFleetWriteSummary(result),
130
151
  code: githubSignalsExitCode(result.written.length, result.failed.length)
@@ -134,4 +155,4 @@ export {
134
155
  githubSignalsExitCode,
135
156
  runGitHubSignalsCommand
136
157
  };
137
- //# sourceMappingURL=github-signals-G4ILZAJ6.js.map
158
+ //# sourceMappingURL=github-signals-IJUPGCUX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/audits/github-signals.ts","../src/cli/commands/github-signals.ts"],"sourcesContent":["import type { Site } from \"../types.js\";\nimport type { CiState, PullRequestSummary } from \"../github/gh.js\";\nimport { isFailingRenovatePR } from \"../alerts/renovate.js\";\n\n/** One swept row, ready for the Airtable writer (slug-keyed by `site`). */\nexport type GitHubSignalsRow = {\n site: string; // the site name/slug the writer matches on\n repo: string; // owner/repo\n renovateFailingCis: number;\n ciState: CiState;\n lastCommitAt: string | null;\n};\n\n/** Injected GitHub reads (so the sweep is pure + testable). */\nexport type GitHubSignalsDeps = {\n openPullRequests: (repo: string) => Promise<PullRequestSummary[]>;\n defaultBranchStatus: (repo: string) => Promise<{ ciState: CiState; lastCommitAt: string | null }>;\n};\n\n/** Per repo-backed site: count its failing Renovate PRs + read its default-branch\n * status. Sites without `gitRepo` are skipped (not errors). A repo whose probe\n * throws is reported via `onSkip` and produces no row — one GitHub hiccup never\n * sinks the sweep. PURE over `deps`. */\nexport async function collectGitHubSignals(\n sites: Site[],\n deps: GitHubSignalsDeps,\n onSkip: (s: { repo: string }) => void = () => {},\n): Promise<GitHubSignalsRow[]> {\n const rows: GitHubSignalsRow[] = [];\n for (const s of sites) {\n const repo = s.gitRepo;\n if (!repo) continue;\n const name = s.name;\n if (!name) continue;\n try {\n const prs = await deps.openPullRequests(repo);\n const status = await deps.defaultBranchStatus(repo);\n rows.push({\n site: name,\n repo,\n renovateFailingCis: prs.filter(isFailingRenovatePR).length,\n ciState: status.ciState,\n lastCommitAt: status.lastCommitAt,\n });\n } catch {\n onSkip({ repo });\n }\n }\n return rows;\n}\n","import { openBase, readAirtableConfig, type AirtableBase } from \"../../reports/airtable/client.js\";\nimport { listWebsites, siteSlug, updateGitHubSignals } from \"../../reports/airtable/websites.js\";\nimport type { Site } from \"../../types.js\";\nimport { collectGitHubSignals } from \"../../audits/github-signals.js\";\nimport { makeGitHub, type GitHub } from \"../../github/gh.js\";\nimport {\n formatFleetWriteSummary,\n type FleetWriteResult,\n} from \"../../audits/write-audits-to-airtable.js\";\nimport { detectSignalEvents, fleetSweptEvent } from \"../../audits/fleet-event-detectors.js\";\nimport { recordFleetEventsBestEffort } from \"../../audits/fleet-events-writer.js\";\nimport type { FleetEvent } from \"../../db/fleet-events.js\";\nimport type { HealthMirror } from \"../../audits/health-mirror.js\";\n\n/** The slice of the GitHub client this sweep actually probes. */\ntype GhProbes = Pick<\n GitHub,\n \"openPullRequests\" | \"defaultBranchStatus\" | \"mergedRenovatePullRequests\"\n>;\n\n/** Injectable wiring for {@link runGitHubSignalsCommand}. Every default is the\n * real fleet path; tests override to reach the per-row write+mirror loop —\n * the hand-rolled `makeHealthMirrorBestEffort()` call made that loop\n * untestable (no test could prove a mirror throw stays out of `failed`, that\n * the counters increment the right way round, or that the mirror sees the\n * same payload Airtable got). Same seam shape as\n * `writeFleetAuditsToAirtable`'s `mirror` argument. */\nexport type GitHubSignalsDeps = {\n /** Airtable base (default: real creds via readAirtableConfig). */\n openBase: () => AirtableBase;\n /** GitHub probe client for the fleet token (default: makeGitHub). */\n makeGh: (token: string) => GhProbes;\n /** Turso mirror factory (default: makeHealthMirrorBestEffort — null without\n * libSQL creds, leaving the Airtable sweep byte-for-byte unchanged). */\n makeMirror: () => Promise<HealthMirror | null>;\n /** Fleet-activity recorder (default: recordFleetEventsBestEffort). */\n recordEvents: (events: FleetEvent[], now: Date) => Promise<void>;\n};\n\n/** Exit code for a fleet github-signals run. Exit 1 when failures are the\n * MAJORITY of the fleet (`failed > written`), not only on a total wipeout —\n * a run where 11/12 repos failed but 1 wrote should still signal an outage.\n * All-success or a minority of flakes (e.g. 1/12 failed) stays exit 0. The\n * no-token clean-skip returns 0 separately (before any probe runs). */\nexport function githubSignalsExitCode(written: number, failed: number): number {\n return failed > written ? 1 : 0;\n}\n\n/** `github-signals --fleet --write-airtable`: sweep every repo-backed site for its\n * Renovate-failing count + default-branch CI state + last-commit date, write each\n * row serially (Airtable ~5 req/sec), and emit FLEET_WRITE_SUMMARY for CI. A\n * missing fleet token is a clean skip (local runs), not a failure. */\nexport async function runGitHubSignalsCommand(\n opts: {\n fleet?: boolean | undefined;\n writeAirtable?: boolean | undefined;\n },\n deps: Partial<GitHubSignalsDeps> = {},\n): Promise<{ output: string; code: number }> {\n if (!opts.fleet || !opts.writeAirtable) {\n return { output: \"github-signals currently supports only --fleet --write-airtable\", code: 2 };\n }\n const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();\n if (!token) {\n return {\n output: \"github-signals skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet read) configured.\",\n code: 0,\n };\n }\n const base = deps.openBase ? deps.openBase() : openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const gh: GhProbes = deps.makeGh ? deps.makeGh(token) : makeGitHub({ token });\n const sites: Site[] = websites.map((w) => ({\n path: \"\",\n name: w.name,\n meta: {},\n ...(w.gitRepo ? { gitRepo: w.gitRepo } : {}),\n }));\n\n const skipped: string[] = [];\n const rows = await collectGitHubSignals(\n sites,\n {\n openPullRequests: (r) => gh.openPullRequests(r),\n defaultBranchStatus: (r) => gh.defaultBranchStatus(r),\n },\n ({ repo }) => skipped.push(repo),\n );\n\n const sweptAt = new Date().toISOString();\n // Phase 3 dual-write (#539): mirror each row's written FieldSet into\n // site_health. Null when libSQL creds are absent — the Airtable sweep\n // proceeds exactly as before. (Dynamic import so the no-mirror path never\n // loads the db client.)\n const makeMirror =\n deps.makeMirror ??\n (async () => {\n const { makeHealthMirrorBestEffort } = await import(\"../../audits/health-mirror.js\");\n return makeHealthMirrorBestEffort();\n });\n const mirror = await makeMirror();\n const result: FleetWriteResult = {\n written: [],\n failed: [],\n ...(mirror ? { mirrored: 0, mirrorFailed: 0, mirrorMissed: 0 } : {}),\n };\n const byRepo = new Map(websites.filter((w) => w.gitRepo).map((w) => [w.gitRepo, w]));\n const events: FleetEvent[] = [];\n const sweptMs = Date.parse(sweptAt);\n const since24h = new Date(sweptMs - 24 * 60 * 60 * 1000).toISOString();\n // Serial: Airtable's ~5 req/sec limit (matches writeFleetAuditsToAirtable).\n for (const row of rows) {\n const target = byRepo.get(row.repo);\n if (!target) {\n result.failed.push({ slug: siteSlug(row.site), error: \"no Websites row matched\" });\n continue;\n }\n try {\n const ghFields = await updateGitHubSignals(base, target.id, {\n renovateFailingCis: row.renovateFailingCis,\n ciState: row.ciState,\n lastCommitAt: row.lastCommitAt,\n sweptAt,\n });\n if (mirror) {\n // Count, never throw: a Turso blip must not move an Airtable-written\n // row into `failed` (that would red the sweep via githubSignalsExitCode\n // on a fleet-wide mirror outage). A 0-row match (site not yet imported)\n // is a miss, not a mirror — see FleetWriteResult.mirrorMissed.\n try {\n if (await mirror(target.id, ghFields)) {\n result.mirrored = (result.mirrored ?? 0) + 1;\n } else {\n result.mirrorMissed = (result.mirrorMissed ?? 0) + 1;\n }\n } catch (e) {\n result.mirrorFailed = (result.mirrorFailed ?? 0) + 1;\n console.error(`[health-mirror] ${target.name}: ${(e as Error).message}`);\n }\n }\n result.written.push({\n siteName: target.name,\n writes: [{ audit: \"github-signals\", counts: row }],\n });\n // Fleet-activity events for this repo: merged Renovate PRs since the last sweep\n // (watermark = the row's prior GitHub Signals At, else a 24h fallback) + a\n // CI-recovered transition. A PR-fetch hiccup drops only this repo's PR events.\n const since = target.githubSignalsAt ?? since24h;\n let merged: Awaited<ReturnType<typeof gh.mergedRenovatePullRequests>> = [];\n try {\n merged = await gh.mergedRenovatePullRequests(row.repo, since);\n } catch {\n // PR list unavailable this run — skip pr_automerged for this repo, keep ci_recovered\n }\n events.push(...detectSignalEvents(target, row, merged, sweptAt));\n } catch (e) {\n result.failed.push({ slug: siteSlug(row.site), error: (e as Error).message });\n }\n }\n for (const repo of skipped) result.failed.push({ slug: repo, error: \"probe failed (skipped)\" });\n\n events.push(fleetSweptEvent(\"github-signals\", result.written.length, sweptAt));\n await (deps.recordEvents ?? recordFleetEventsBestEffort)(events, new Date());\n\n // Exit non-zero when failures are the MAJORITY of the fleet, not only on a\n // total wipeout. A run where 11/12 repos failed but 1 wrote used to return 0,\n // masking a large outage. The nightly cron step is `continue-on-error`, so a\n // non-zero here is an operator-visibility signal, not a red build.\n return {\n output: formatFleetWriteSummary(result),\n code: githubSignalsExitCode(result.written.length, result.failed.length),\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  generateHeaderImage
3
- } from "./chunk-37S6L6G4.js";
3
+ } from "./chunk-5UINJNAJ.js";
4
4
  import {
5
5
  uploadAttachment
6
- } from "./chunk-MPBXOJ3N.js";
7
- import {
8
- ACTIVE_STATUSES,
9
- listWebsites,
10
- siteSlug
11
- } from "./chunk-577HCGZO.js";
6
+ } from "./chunk-Z7AXYKQB.js";
12
7
  import {
13
8
  openBase,
14
9
  readAirtableConfig
15
10
  } from "./chunk-HN6I2XWI.js";
11
+ import {
12
+ ACTIVE_STATUSES,
13
+ listWebsites,
14
+ siteSlug
15
+ } from "./chunk-LDWZSLSU.js";
16
16
  import "./chunk-6VAL7XJT.js";
17
17
 
18
18
  // src/cli/commands/header-image.ts
@@ -57,9 +57,25 @@ async function generateForTargets(targets, opts, generate = generateHeaderImage)
57
57
  ...settleMs === void 0 ? {} : { settleMs }
58
58
  });
59
59
  if (opts.writeAirtable) {
60
- await uploadAttachment(row.id, "Header image", gen.bytes, gen.filename, gen.contentType);
60
+ await uploadAttachment(row.id, "Header image", gen.bytes, gen.filename, gen.contentType, {
61
+ replaceIn: "Websites"
62
+ });
63
+ let stored = "";
64
+ if (opts.storeDb) {
65
+ try {
66
+ await opts.storeDb(row.id, {
67
+ bytes: gen.bytes,
68
+ filename: gen.filename,
69
+ contentType: gen.contentType,
70
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
71
+ });
72
+ stored = " + turso";
73
+ } catch (err) {
74
+ stored = ` (\u26A0 turso store FAILED: ${err instanceof Error ? err.message : String(err)})`;
75
+ }
76
+ }
61
77
  lines.push(
62
- `\u2714 ${row.name} \u2014 uploaded ${gen.filename} (${(gen.bytes.byteLength / 1024 / 1024).toFixed(2)} MB)`
78
+ `\u2714 ${row.name} \u2014 uploaded ${gen.filename} (${(gen.bytes.byteLength / 1024 / 1024).toFixed(2)} MB)${stored}`
63
79
  );
64
80
  } else {
65
81
  const path = resolve(outDir, gen.filename);
@@ -84,7 +100,14 @@ async function runHeaderImageCommand(site, opts) {
84
100
  code: 1
85
101
  };
86
102
  }
87
- return generateForTargets(targets, opts);
103
+ let withStore = opts;
104
+ if (opts.writeAirtable && !opts.storeDb && process.env.TURSO_DATABASE_URL) {
105
+ const { openDb, readDbConfig } = await import("./client-AUE3XUPO.js");
106
+ const { storeHeaderImage } = await import("./header-images-335L2AKB.js");
107
+ const db = await openDb(readDbConfig());
108
+ withStore = { ...opts, storeDb: (id, img) => storeHeaderImage(db, id, img) };
109
+ }
110
+ return generateForTargets(targets, withStore);
88
111
  }
89
112
  export {
90
113
  generateForTargets,
@@ -92,4 +115,4 @@ export {
92
115
  resolveTargets,
93
116
  runHeaderImageCommand
94
117
  };
95
- //# sourceMappingURL=header-image-T7XD37JL.js.map
118
+ //# sourceMappingURL=header-image-AWLLQA4L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/commands/header-image.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { openBase, readAirtableConfig } from \"../../reports/airtable/client.js\";\nimport { listWebsites, siteSlug, ACTIVE_STATUSES } from \"../../reports/airtable/websites.js\";\nimport type { WebsiteRow } from \"../../reports/airtable/websites.js\";\nimport { uploadAttachment } from \"../../reports/airtable/attachments.js\";\nimport { generateHeaderImage } from \"../../reports/header-image/index.js\";\nimport type { StoredHeaderImage } from \"../../db/header-images.js\";\n\nexport type HeaderImageOptions = {\n all?: boolean;\n force?: boolean;\n writeAirtable?: boolean;\n outDir?: string;\n settleMs?: string;\n /** Injected Turso store for the dual-write (#539, design D5): while Airtable\n * is still written (until the Phase 5 freeze), every upload ALSO lands the\n * bytes in sites.header_image* so the Turso read layer serves a real image.\n * Absent → Airtable-only (a local run without Turso env still works). */\n storeDb?: (siteId: string, img: StoredHeaderImage) => Promise<void>;\n};\n\n/** Which rows this invocation should act on. Pure, so it is unit-tested. */\nexport function resolveTargets(\n rows: readonly WebsiteRow[],\n // `| undefined` spelled out because `exactOptionalPropertyTypes` otherwise\n // rejects the caller passing through possibly-undefined CLI flags.\n opts: { site?: string | undefined; all?: boolean | undefined; force?: boolean | undefined },\n): WebsiteRow[] {\n if (opts.site) {\n const want = siteSlug(opts.site);\n return rows.filter((r) => siteSlug(r.name) === want);\n }\n if (!opts.all) return [];\n return rows.filter((r) => {\n if (!r.url) return false;\n if (r.status === null || !ACTIVE_STATUSES.has(r.status)) return false;\n return opts.force ? true : !r.headerImage;\n });\n}\n\n/** Parse the `--settle-ms <ms>` flag. Unset → undefined (the generator's own\n * default settle). `Number(\"abc\")` is NaN, which would sail through as a NaN\n * timeout and silently skip the settle entirely, so anything non-finite or\n * negative is rejected up front rather than producing a subtly-wrong capture. */\nexport function parseSettleMs(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const n = Number(value);\n if (!Number.isFinite(n) || n < 0) {\n throw Object.assign(\n new Error(`--settle-ms must be a non-negative number of milliseconds, got \"${value}\"`),\n { exitCode: 2 },\n );\n }\n return n;\n}\n\n/** Injected in tests so the file-writing branch can be exercised without\n * driving a real browser (mirrors `GenerateInput.shooter`). */\ntype Generate = typeof generateHeaderImage;\n\n/**\n * Generate a header image per target row and either upload it to Airtable or\n * write it locally for review. Split out of {@link runHeaderImageCommand} so\n * the local-output path is testable without an Airtable base.\n */\nexport async function generateForTargets(\n targets: readonly WebsiteRow[],\n opts: HeaderImageOptions,\n generate: Generate = generateHeaderImage,\n): Promise<{ output: string; code: number }> {\n const outDir = resolve(opts.outDir ?? \"reports\");\n const settleMs = parseSettleMs(opts.settleMs);\n // `reports/` does not exist on a fresh checkout, so the writeFile() below\n // would throw ENOENT for the first operator to run the default (local) path.\n // Create it once, before the loop, rather than per site.\n if (!opts.writeAirtable) await mkdir(outDir, { recursive: true });\n\n const lines: string[] = [];\n let failed = 0;\n\n for (const row of targets) {\n try {\n const gen = await generate({\n url: row.url,\n slug: siteSlug(row.name),\n // `exactOptionalPropertyTypes` rejects an explicit `settleMs: undefined`\n // against `settleMs?: number`, so omit the key entirely when unset.\n ...(settleMs === undefined ? {} : { settleMs }),\n });\n if (opts.writeAirtable) {\n // replaceIn: the field must hold exactly the current header — see\n // uploadAttachment, where appending left readers on a stale [0].\n await uploadAttachment(row.id, \"Header image\", gen.bytes, gen.filename, gen.contentType, {\n replaceIn: \"Websites\",\n });\n let stored = \"\";\n if (opts.storeDb) {\n // Dual-write. A Turso failure must not void the Airtable upload —\n // but it must be VISIBLE, never a silent divergence.\n try {\n await opts.storeDb(row.id, {\n bytes: gen.bytes,\n filename: gen.filename,\n contentType: gen.contentType,\n generatedAt: new Date().toISOString(),\n });\n stored = \" + turso\";\n } catch (err) {\n stored = ` (⚠ turso store FAILED: ${err instanceof Error ? err.message : String(err)})`;\n }\n }\n lines.push(\n `✔ ${row.name} — uploaded ${gen.filename} (${(gen.bytes.byteLength / 1024 / 1024).toFixed(2)} MB)${stored}`,\n );\n } else {\n const path = resolve(outDir, gen.filename);\n await writeFile(path, gen.bytes);\n lines.push(`✔ ${row.name} — wrote ${path} (review, then re-run with --write-airtable)`);\n }\n } catch (err) {\n failed++;\n lines.push(`✖ ${row.name} — ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n lines.push(`${targets.length - failed}/${targets.length} generated.`);\n return { output: lines.join(\"\\n\"), code: failed > 0 ? 1 : 0 };\n}\n\n/**\n * `header-image [site]` — capture a site's live homepage and composite its\n * report header image. Defaults to writing the JPEG locally so the operator can\n * eyeball it; `--write-airtable` uploads it to the Websites row's Header image\n * field. `--all` backfills every live site that has no header image yet.\n */\nexport async function runHeaderImageCommand(\n site: string | undefined,\n opts: HeaderImageOptions,\n): Promise<{ output: string; code: number }> {\n const base = openBase(readAirtableConfig());\n const rows = await listWebsites(base);\n const targets = resolveTargets(rows, { site, all: opts.all, force: opts.force });\n if (targets.length === 0) {\n return {\n output: site ? `No site matched \"${site}\".` : \"No sites need a header image.\",\n code: 1,\n };\n }\n // Wire the Turso dual-write when the env is present; a local run without it\n // still works Airtable-only (the store is per-site error-isolated above).\n let withStore = opts;\n if (opts.writeAirtable && !opts.storeDb && process.env.TURSO_DATABASE_URL) {\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { storeHeaderImage } = await import(\"../../db/header-images.js\");\n const db = await openDb(readDbConfig());\n withStore = { ...opts, storeDb: (id, img) => storeHeaderImage(db, id, img) };\n }\n return generateForTargets(targets, withStore);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,iBAAiB;AACjC,SAAS,eAAe;AAsBjB,SAAS,eACd,MAGA,MACc;AACd,MAAI,KAAK,MAAM;AACb,UAAM,OAAO,SAAS,KAAK,IAAI;AAC/B,WAAO,KAAK,OAAO,CAAC,MAAM,SAAS,EAAE,IAAI,MAAM,IAAI;AAAA,EACrD;AACA,MAAI,CAAC,KAAK,IAAK,QAAO,CAAC;AACvB,SAAO,KAAK,OAAO,CAAC,MAAM;AACxB,QAAI,CAAC,EAAE,IAAK,QAAO;AACnB,QAAI,EAAE,WAAW,QAAQ,CAAC,gBAAgB,IAAI,EAAE,MAAM,EAAG,QAAO;AAChE,WAAO,KAAK,QAAQ,OAAO,CAAC,EAAE;AAAA,EAChC,CAAC;AACH;AAMO,SAAS,cAAc,OAA+C;AAC3E,MAAI,UAAU,OAAW,QAAO;AAChC,QAAM,IAAI,OAAO,KAAK;AACtB,MAAI,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,GAAG;AAChC,UAAM,OAAO;AAAA,MACX,IAAI,MAAM,mEAAmE,KAAK,GAAG;AAAA,MACrF,EAAE,UAAU,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AAWA,eAAsB,mBACpB,SACA,MACA,WAAqB,qBACsB;AAC3C,QAAM,SAAS,QAAQ,KAAK,UAAU,SAAS;AAC/C,QAAM,WAAW,cAAc,KAAK,QAAQ;AAI5C,MAAI,CAAC,KAAK,cAAe,OAAM,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AAEhE,QAAM,QAAkB,CAAC;AACzB,MAAI,SAAS;AAEb,aAAW,OAAO,SAAS;AACzB,QAAI;AACF,YAAM,MAAM,MAAM,SAAS;AAAA,QACzB,KAAK,IAAI;AAAA,QACT,MAAM,SAAS,IAAI,IAAI;AAAA;AAAA;AAAA,QAGvB,GAAI,aAAa,SAAY,CAAC,IAAI,EAAE,SAAS;AAAA,MAC/C,CAAC;AACD,UAAI,KAAK,eAAe;AAGtB,cAAM,iBAAiB,IAAI,IAAI,gBAAgB,IAAI,OAAO,IAAI,UAAU,IAAI,aAAa;AAAA,UACvF,WAAW;AAAA,QACb,CAAC;AACD,YAAI,SAAS;AACb,YAAI,KAAK,SAAS;AAGhB,cAAI;AACF,kBAAM,KAAK,QAAQ,IAAI,IAAI;AAAA,cACzB,OAAO,IAAI;AAAA,cACX,UAAU,IAAI;AAAA,cACd,aAAa,IAAI;AAAA,cACjB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,YACtC,CAAC;AACD,qBAAS;AAAA,UACX,SAAS,KAAK;AACZ,qBAAS,gCAA2B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,UACtF;AAAA,QACF;AACA,cAAM;AAAA,UACJ,UAAK,IAAI,IAAI,oBAAe,IAAI,QAAQ,MAAM,IAAI,MAAM,aAAa,OAAO,MAAM,QAAQ,CAAC,CAAC,OAAO,MAAM;AAAA,QAC3G;AAAA,MACF,OAAO;AACL,cAAM,OAAO,QAAQ,QAAQ,IAAI,QAAQ;AACzC,cAAM,UAAU,MAAM,IAAI,KAAK;AAC/B,cAAM,KAAK,UAAK,IAAI,IAAI,iBAAY,IAAI,8CAA8C;AAAA,MACxF;AAAA,IACF,SAAS,KAAK;AACZ;AACA,YAAM,KAAK,UAAK,IAAI,IAAI,WAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,IAClF;AAAA,EACF;AAEA,QAAM,KAAK,GAAG,QAAQ,SAAS,MAAM,IAAI,QAAQ,MAAM,aAAa;AACpE,SAAO,EAAE,QAAQ,MAAM,KAAK,IAAI,GAAG,MAAM,SAAS,IAAI,IAAI,EAAE;AAC9D;AAQA,eAAsB,sBACpB,MACA,MAC2C;AAC3C,QAAM,OAAO,SAAS,mBAAmB,CAAC;AAC1C,QAAM,OAAO,MAAM,aAAa,IAAI;AACpC,QAAM,UAAU,eAAe,MAAM,EAAE,MAAM,KAAK,KAAK,KAAK,OAAO,KAAK,MAAM,CAAC;AAC/E,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,MACL,QAAQ,OAAO,oBAAoB,IAAI,OAAO;AAAA,MAC9C,MAAM;AAAA,IACR;AAAA,EACF;AAGA,MAAI,YAAY;AAChB,MAAI,KAAK,iBAAiB,CAAC,KAAK,WAAW,QAAQ,IAAI,oBAAoB;AACzE,UAAM,EAAE,QAAQ,aAAa,IAAI,MAAM,OAAO,sBAAoB;AAClE,UAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,6BAA2B;AACrE,UAAM,KAAK,MAAM,OAAO,aAAa,CAAC;AACtC,gBAAY,EAAE,GAAG,MAAM,SAAS,CAAC,IAAI,QAAQ,iBAAiB,IAAI,IAAI,GAAG,EAAE;AAAA,EAC7E;AACA,SAAO,mBAAmB,SAAS,SAAS;AAC9C;","names":[]}
@@ -0,0 +1,68 @@
1
+ // src/db/header-images.ts
2
+ async function storeHeaderImage(db, siteId, image) {
3
+ await db.updateTable("sites").set({
4
+ header_image: image.bytes,
5
+ header_image_filename: image.filename,
6
+ header_image_type: image.contentType,
7
+ header_image_generated_at: image.generatedAt
8
+ }).where("id", "=", siteId).execute();
9
+ }
10
+ function headerImageAttachment(rec) {
11
+ const atts = rec.fields["Header image"];
12
+ const a = atts?.at(-1);
13
+ return a && typeof a.url === "string" && a.url.length > 0 ? {
14
+ url: a.url,
15
+ filename: typeof a.filename === "string" ? a.filename : "header-image",
16
+ type: typeof a.type === "string" ? a.type : "application/octet-stream"
17
+ } : null;
18
+ }
19
+ async function backfillHeaderImages(db, io) {
20
+ const records = await io.listWebsiteRecords();
21
+ const populated = new Set(
22
+ (await db.selectFrom("sites").select("id").where("header_image", "is not", null).execute()).map(
23
+ (r) => r.id
24
+ )
25
+ );
26
+ const result = { stored: 0, skipped: 0, absent: 0, failed: [] };
27
+ for (const rec of records) {
28
+ const att = headerImageAttachment(rec);
29
+ if (!att) {
30
+ result.absent++;
31
+ continue;
32
+ }
33
+ if (populated.has(rec.id)) {
34
+ result.skipped++;
35
+ continue;
36
+ }
37
+ const bytes = await io.fetchBytes(att.url);
38
+ if (bytes === null) {
39
+ result.failed.push(rec.id);
40
+ continue;
41
+ }
42
+ await storeHeaderImage(db, rec.id, {
43
+ bytes,
44
+ filename: att.filename,
45
+ contentType: att.type,
46
+ generatedAt: null
47
+ });
48
+ result.stored++;
49
+ }
50
+ return result;
51
+ }
52
+ function formatBackfillResult(r) {
53
+ const lines = [];
54
+ if (r.failed.length > 0) {
55
+ lines.push(`\u26A0 ${r.failed.length} attachment fetch(es) failed: ${r.failed.join(", ")}`);
56
+ }
57
+ lines.push(
58
+ `HEADER_IMAGE_BACKFILL stored=${r.stored} skipped=${r.skipped} absent=${r.absent} failed=${r.failed.length}`
59
+ );
60
+ return lines.join("\n");
61
+ }
62
+ export {
63
+ backfillHeaderImages,
64
+ formatBackfillResult,
65
+ headerImageAttachment,
66
+ storeHeaderImage
67
+ };
68
+ //# sourceMappingURL=header-images-335L2AKB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/header-images.ts"],"sourcesContent":["/** Header images move to Turso (#539, design D5 completed).\n *\n * Since Phase 1 the schema has reserved `sites.header_image*` as the image's\n * home — the importer deliberately never touches those columns — but nothing\n * wrote them, so the read layer's `headerImage` was null fleet-wide and both\n * cockpit preflight and approve-report stayed pinned to the Airtable reader.\n * Two writers close that:\n *\n * - `storeHeaderImage` — the shared write, used by the header-image CLI's\n * dual-write (Airtable upload + Turso store, until Phase 5 freezes Airtable)\n * and by the backfill.\n * - `backfillHeaderImages` — one-shot: copy every site's CURRENT Airtable\n * attachment into Turso. Idempotent — a site whose BLOB is already\n * populated is skipped, so re-runs never clobber a freshly generated image\n * with a stale Airtable copy. Emits its machine line on EVERY run.\n */\nimport type { Db } from \"./client.js\";\nimport type { RawRecord } from \"./import-airtable.js\";\n\nexport type StoredHeaderImage = {\n bytes: Uint8Array;\n filename: string;\n contentType: string;\n /** The generator's stamp; null for a backfilled copy (a backfill is not a\n * generation — the column keeps meaning \"when the generator made it\"). */\n generatedAt: string | null;\n};\n\nexport async function storeHeaderImage(\n db: Db,\n siteId: string,\n image: StoredHeaderImage,\n): Promise<void> {\n await db\n .updateTable(\"sites\")\n .set({\n header_image: image.bytes,\n header_image_filename: image.filename,\n header_image_type: image.contentType,\n header_image_generated_at: image.generatedAt,\n })\n .where(\"id\", \"=\", siteId)\n .execute();\n}\n\ntype Attachment = { url: string; filename: string; type: string };\n\n/** NEWEST \"Header image\" attachment of a raw Websites record, or null.\n *\n * The tail, not the head: Airtable's uploadAttachment APPENDS, so a field that ever\n * held more than one file served its OLDEST image from `[0]` forever — see the note\n * in `reports/airtable/websites.ts` and #574/#577. Kept in step with that reader so\n * the Turso mirror and the send path can never disagree about which file is current. */\nexport function headerImageAttachment(rec: RawRecord): Attachment | null {\n const atts = rec.fields[\"Header image\"] as Array<Partial<Attachment>> | undefined;\n const a = atts?.at(-1);\n return a && typeof a.url === \"string\" && a.url.length > 0\n ? {\n url: a.url,\n filename: typeof a.filename === \"string\" ? a.filename : \"header-image\",\n type: typeof a.type === \"string\" ? a.type : \"application/octet-stream\",\n }\n : null;\n}\n\nexport type BackfillIo = {\n listWebsiteRecords: () => Promise<RawRecord[]>;\n /** Fetch attachment bytes; null on failure (named in the summary, not fatal). */\n fetchBytes: (url: string) => Promise<Uint8Array | null>;\n};\n\nexport type BackfillResult = {\n stored: number;\n /** Sites whose BLOB was already populated — never overwritten. */\n skipped: number;\n /** Sites with no Airtable attachment at all. */\n absent: number;\n /** Record ids whose attachment fetch failed — named, never silent. */\n failed: string[];\n};\n\nexport async function backfillHeaderImages(db: Db, io: BackfillIo): Promise<BackfillResult> {\n const records = await io.listWebsiteRecords();\n const populated = new Set(\n (await db.selectFrom(\"sites\").select(\"id\").where(\"header_image\", \"is not\", null).execute()).map(\n (r) => r.id,\n ),\n );\n const result: BackfillResult = { stored: 0, skipped: 0, absent: 0, failed: [] };\n for (const rec of records) {\n const att = headerImageAttachment(rec);\n if (!att) {\n result.absent++;\n continue;\n }\n if (populated.has(rec.id)) {\n result.skipped++;\n continue;\n }\n const bytes = await io.fetchBytes(att.url);\n if (bytes === null) {\n result.failed.push(rec.id);\n continue;\n }\n await storeHeaderImage(db, rec.id, {\n bytes,\n filename: att.filename,\n contentType: att.type,\n generatedAt: null,\n });\n result.stored++;\n }\n return result;\n}\n\n/** Human lines + the machine line, emitted on every run (count=0 included). */\nexport function formatBackfillResult(r: BackfillResult): string {\n const lines: string[] = [];\n if (r.failed.length > 0) {\n lines.push(`⚠ ${r.failed.length} attachment fetch(es) failed: ${r.failed.join(\", \")}`);\n }\n lines.push(\n `HEADER_IMAGE_BACKFILL stored=${r.stored} skipped=${r.skipped} absent=${r.absent} failed=${r.failed.length}`,\n );\n return lines.join(\"\\n\");\n}\n"],"mappings":";AA4BA,eAAsB,iBACpB,IACA,QACA,OACe;AACf,QAAM,GACH,YAAY,OAAO,EACnB,IAAI;AAAA,IACH,cAAc,MAAM;AAAA,IACpB,uBAAuB,MAAM;AAAA,IAC7B,mBAAmB,MAAM;AAAA,IACzB,2BAA2B,MAAM;AAAA,EACnC,CAAC,EACA,MAAM,MAAM,KAAK,MAAM,EACvB,QAAQ;AACb;AAUO,SAAS,sBAAsB,KAAmC;AACvE,QAAM,OAAO,IAAI,OAAO,cAAc;AACtC,QAAM,IAAI,MAAM,GAAG,EAAE;AACrB,SAAO,KAAK,OAAO,EAAE,QAAQ,YAAY,EAAE,IAAI,SAAS,IACpD;AAAA,IACE,KAAK,EAAE;AAAA,IACP,UAAU,OAAO,EAAE,aAAa,WAAW,EAAE,WAAW;AAAA,IACxD,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;AAAA,EAC9C,IACA;AACN;AAkBA,eAAsB,qBAAqB,IAAQ,IAAyC;AAC1F,QAAM,UAAU,MAAM,GAAG,mBAAmB;AAC5C,QAAM,YAAY,IAAI;AAAA,KACnB,MAAM,GAAG,WAAW,OAAO,EAAE,OAAO,IAAI,EAAE,MAAM,gBAAgB,UAAU,IAAI,EAAE,QAAQ,GAAG;AAAA,MAC1F,CAAC,MAAM,EAAE;AAAA,IACX;AAAA,EACF;AACA,QAAM,SAAyB,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAAE;AAC9E,aAAW,OAAO,SAAS;AACzB,UAAM,MAAM,sBAAsB,GAAG;AACrC,QAAI,CAAC,KAAK;AACR,aAAO;AACP;AAAA,IACF;AACA,QAAI,UAAU,IAAI,IAAI,EAAE,GAAG;AACzB,aAAO;AACP;AAAA,IACF;AACA,UAAM,QAAQ,MAAM,GAAG,WAAW,IAAI,GAAG;AACzC,QAAI,UAAU,MAAM;AAClB,aAAO,OAAO,KAAK,IAAI,EAAE;AACzB;AAAA,IACF;AACA,UAAM,iBAAiB,IAAI,IAAI,IAAI;AAAA,MACjC;AAAA,MACA,UAAU,IAAI;AAAA,MACd,aAAa,IAAI;AAAA,MACjB,aAAa;AAAA,IACf,CAAC;AACD,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGO,SAAS,qBAAqB,GAA2B;AAC9D,QAAM,QAAkB,CAAC;AACzB,MAAI,EAAE,OAAO,SAAS,GAAG;AACvB,UAAM,KAAK,UAAK,EAAE,OAAO,MAAM,iCAAiC,EAAE,OAAO,KAAK,IAAI,CAAC,EAAE;AAAA,EACvF;AACA,QAAM;AAAA,IACJ,gCAAgC,EAAE,MAAM,YAAY,EAAE,OAAO,WAAW,EAAE,MAAM,WAAW,EAAE,OAAO,MAAM;AAAA,EAC5G;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}
@@ -5,20 +5,20 @@ import {
5
5
  appendSkipNotice,
6
6
  fleetWorkdir,
7
7
  prepareFleetSites
8
- } from "./chunk-ZMIPG6TR.js";
8
+ } from "./chunk-TVL5MVUF.js";
9
9
  import {
10
10
  resolveSites
11
- } from "./chunk-7J53SUT6.js";
11
+ } from "./chunk-P2FE5AI6.js";
12
12
  import {
13
13
  healthEndpoint
14
- } from "./chunk-R6FPFMOS.js";
14
+ } from "./chunk-BQRFQSGY.js";
15
15
  import "./chunk-R53VL3RI.js";
16
- import "./chunk-ZTPPCHDB.js";
17
- import "./chunk-FDIFTIXC.js";
18
- import "./chunk-4DHQCN5H.js";
19
- import "./chunk-XXTZBPUY.js";
16
+ import "./chunk-3G25KIWW.js";
17
+ import "./chunk-6TWWY7ZU.js";
18
+ import "./chunk-EVBCK2KC.js";
20
19
  import "./chunk-XTK5VIQB.js";
21
- import "./chunk-U5SZ4FOP.js";
20
+ import "./chunk-LBYOLBW7.js";
21
+ import "./chunk-XXTZBPUY.js";
22
22
 
23
23
  // src/cli/commands/health-endpoint.ts
24
24
  import { resolve } from "path";
@@ -51,4 +51,4 @@ async function runHealthEndpointCommand(site, opts) {
51
51
  export {
52
52
  runHealthEndpointCommand
53
53
  };
54
- //# sourceMappingURL=health-endpoint-242WG2OJ.js.map
54
+ //# sourceMappingURL=health-endpoint-WJC2UMYS.js.map
@@ -0,0 +1,75 @@
1
+ import {
2
+ healthColumnFor,
3
+ scheduleColumnFor
4
+ } from "./chunk-ITIOFN7A.js";
5
+ import "./chunk-ACBRLLFT.js";
6
+ import {
7
+ MAINTENANCE_CHECKLIST,
8
+ TESTING_CHECKLIST
9
+ } from "./chunk-VAD43MOR.js";
10
+ import {
11
+ openDb,
12
+ readDbConfig
13
+ } from "./chunk-JHGHLKFS.js";
14
+ import "./chunk-3SF7D4DY.js";
15
+ import "./chunk-LDWZSLSU.js";
16
+ import "./chunk-6VAL7XJT.js";
17
+
18
+ // src/db/fleet-state.ts
19
+ async function mirrorHealthFields(db, siteId, fields) {
20
+ const patch = {};
21
+ for (const [field, value] of Object.entries(fields)) {
22
+ const m = healthColumnFor(field);
23
+ if (!m)
24
+ throw new Error(`mirrorHealthFields: importer claims no site_health column for '${field}'`);
25
+ patch[m.col] = m.coerce(value);
26
+ }
27
+ if (Object.keys(patch).length === 0) return true;
28
+ const res = await db.updateTable("site_health").set(patch).where("site_id", "=", siteId).executeTakeFirst();
29
+ return res.numUpdatedRows > 0n;
30
+ }
31
+ async function mirrorScheduleFields(db, siteId, fields, computedAt) {
32
+ const patch = {};
33
+ for (const [field, value] of Object.entries(fields)) {
34
+ const m = scheduleColumnFor(field);
35
+ if (!m)
36
+ throw new Error(
37
+ `mirrorScheduleFields: importer claims no site_schedule column for '${field}'`
38
+ );
39
+ patch[m.col] = m.coerce(value);
40
+ }
41
+ if (Object.keys(patch).length === 0) return true;
42
+ patch.computed_at = computedAt;
43
+ const res = await db.updateTable("site_schedule").set(patch).where("site_id", "=", siteId).executeTakeFirst();
44
+ return res.numUpdatedRows > 0n;
45
+ }
46
+ var CHECKLIST_FIELD_BY_KEY = new Map(
47
+ [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map((i) => [i.key, i.field])
48
+ );
49
+
50
+ // src/audits/health-mirror.ts
51
+ async function makeHealthMirrorBestEffort(open = () => openDb(readDbConfig())) {
52
+ let db;
53
+ try {
54
+ db = await open();
55
+ } catch (e) {
56
+ console.error(`[health-mirror] mirroring disabled: no libSQL (${String(e)})`);
57
+ return null;
58
+ }
59
+ return (siteId, fields) => mirrorHealthFields(db, siteId, fields);
60
+ }
61
+ async function makeScheduleMirrorBestEffort(open = () => openDb(readDbConfig())) {
62
+ let db;
63
+ try {
64
+ db = await open();
65
+ } catch (e) {
66
+ console.error(`[schedule-mirror] mirroring disabled: no libSQL (${String(e)})`);
67
+ return null;
68
+ }
69
+ return (siteId, fields, computedAt) => mirrorScheduleFields(db, siteId, fields, computedAt);
70
+ }
71
+ export {
72
+ makeHealthMirrorBestEffort,
73
+ makeScheduleMirrorBestEffort
74
+ };
75
+ //# sourceMappingURL=health-mirror-4B2L7T2Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/fleet-state.ts","../src/audits/health-mirror.ts"],"sourcesContent":["/** Phase 2 of the Airtable → Turso migration (#539): the fleet-state READ layer.\n *\n * Returns the exact `WebsiteRow` shape the Airtable module returns, so every\n * repoint is an import-only swap at a composition root — the same trick\n * listSubmissionsForSite used for the hybrid-db cutover. The raw values live\n * in `sites`/`site_health`/`site_schedule` (written by the importer / hourly\n * sync); coercion to `WebsiteRow` happens HERE, reusing the Airtable module's\n * own exported coercers (`toVerdict`, `toFrequency`, `parseNotifyRouting`,\n * `parseSecurityAdvisories`, `trimToNull`) so there is one truth for each.\n *\n * The equivalence instrument (tests/db/fleet-state.test.ts) pins this module\n * to `mapRow` field-by-field: for a fixture record, `mapRow(rec)` must deep-\n * equal the row read back through here after an import. A new WebsiteRow\n * field fails that test until this module carries it.\n *\n * `headerImage` is the ONE deliberate exception (design D5): Airtable stopped\n * being its source — the bytes belong in `sites.header_image*`. Nothing\n * writes those columns yet (verified empty across all 44 prod rows,\n * 2026-08-24), so this reads null fleet-wide today, and approve-report — the\n * only request-path consumer — stays on the Airtable reader until the\n * header-image writer moves in Phase 3. The `url` is \"\" because the bytes\n * live in the row itself, not behind a signed URL.\n */\nimport type { Selectable, Updateable } from \"kysely\";\nimport type { Db } from \"./client.js\";\nimport type { ReportsTable, SiteHealthTable, SiteScheduleTable } from \"./schema.js\";\nimport { SITE_FIELDS, healthColumnFor, scheduleColumnFor } from \"./import-airtable.js\";\nimport {\n toReportType,\n parseAutoEvidence,\n type ReportRow,\n type DeliveryStatus,\n} from \"../reports/airtable/reports.js\";\nimport { MAINTENANCE_CHECKLIST, TESTING_CHECKLIST } from \"../reports/checklist.js\";\nimport type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { canonicalizeStatus } from \"../reports/airtable/site-status.js\";\nimport {\n parseNotifyRouting,\n parseSecurityAdvisories,\n toFrequency,\n toPrismicModelsVerdict,\n toVerdict,\n trimToNull,\n} from \"../reports/airtable/websites.js\";\n\ntype JoinedRow = Record<string, unknown>;\n\nconst str = (v: unknown): string | null => (typeof v === \"string\" ? v : null);\nconst num = (v: unknown): number | null => (typeof v === \"number\" ? v : null);\n/** Stored 1/0/null (importer's b01n) → boolean | null. */\nconst bool = (v: unknown): boolean | null => (typeof v === \"number\" ? v !== 0 : null);\n\n/** Stored JSON array (importer-normalized) → the trimmed string[] mapRow yields. */\nfunction parseAwc(raw: unknown): string[] {\n if (typeof raw !== \"string\" || raw.trim() === \"\") return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed\n .filter((x): x is string => typeof x === \"string\")\n .map((s) => s.trim())\n .filter(Boolean);\n } catch {\n return [];\n }\n}\n\nfunction rowFromJoined(r: JoinedRow): WebsiteRow {\n const name = String(r.name ?? \"\");\n const headerFilename = str(r.header_image_filename);\n const headerType = str(r.header_image_type);\n return {\n id: String(r.id),\n name,\n url: str(r.url) ?? \"\",\n // `sites.status` holds the RAW Airtable cell (the importer stores it verbatim\n // so the hourly parity check compares raw-to-raw). Canonicalizing HERE — the\n // twin of mapRow's seam — is what keeps the #558 equivalence instrument green.\n status: canonicalizeStatus(r.status),\n statusRaw: str(r.status),\n pointOfContact: str(r.point_of_contact),\n maintenanceFreq: toFrequency(r.maintenance_freq, `${name} maintenance`),\n testingFreq: toFrequency(r.testing_freq, `${name} testing`),\n maintenanceFreqRaw: str(r.maintenance_freq),\n testingFreqRaw: str(r.testing_freq),\n maintenanceDay: str(r.maintenance_day),\n testingDay: str(r.testing_day),\n ga4PropertyId: str(r.ga4_property_id),\n searchQuery: str(r.search_query),\n searchConsoleProperty: str(r.search_console_property),\n analyticsSoftFailAt: str(r.analytics_soft_fail_at),\n gitRepo: str(r.git_repo),\n reportRecipientsTo: str(r.report_recipients_to),\n reportRecipientsCc: str(r.report_recipients_cc),\n acceptedWatchConditions: parseAwc(r.accepted_watch_conditions),\n headerImage:\n headerFilename !== null && headerType !== null\n ? { url: \"\", filename: headerFilename, type: headerType }\n : null,\n pScore: num(r.p_score),\n rScore: num(r.r_score),\n bpScore: num(r.bp_score),\n seoScore: num(r.seo_score),\n lastLighthouseAuditAt: str(r.lighthouse_at),\n a11yViolations: num(r.a11y_violations),\n depsDrifted: num(r.deps_drifted),\n depsMajorBehind: num(r.deps_major_behind),\n depsOutdated: num(r.deps_outdated),\n depsMajorOutdated: num(r.deps_major_outdated),\n securityVulnsCritical: num(r.vulns_critical),\n securityVulnsHigh: num(r.vulns_high),\n securityVulnsModerate: num(r.vulns_moderate),\n securityVulnsLow: num(r.vulns_low),\n securityAutoFixAttempts: num(r.auto_fix_attempts),\n lastSecurityAuditAt: str(r.security_audit_at),\n securityAdvisories: parseSecurityAdvisories(r.security_advisories),\n certDaysRemaining: num(r.cert_days_remaining),\n domainCheckedAt: str(r.domain_checked_at),\n netlifyId: trimToNull(r.netlify_id),\n deployStatus: str(r.deploy_status),\n lastDeployAt: str(r.last_deploy_at),\n deployLogUrl: str(r.deploy_log_url),\n deployCheckedAt: str(r.deploy_checked_at),\n functionHealth: toVerdict(r.function_health),\n cmsReachable: toVerdict(r.cms_reachable),\n turnstileWidget: toVerdict(r.turnstile_widget),\n functionHealthCheckedAt: str(r.function_health_checked_at),\n crossbrowserOk: bool(r.crossbrowser_ok),\n mobileOk: bool(r.mobile_ok),\n linksOk: bool(r.links_ok),\n brokenLinks: num(r.broken_links),\n browserCheckedAt: str(r.browser_checked_at),\n reachableOk: toVerdict(r.uptime_reachable),\n titleMetaOk: toVerdict(r.titles_meta_ok),\n copyIntro: trimToNull(r.copy_intro),\n copyContact: trimToNull(r.copy_contact),\n copyFooter: trimToNull(r.copy_footer),\n launchedAt: str(r.launched_at),\n newsletterWebhook: trimToNull(r.newsletter_webhook),\n notifyRouting: parseNotifyRouting(r.notify_routing),\n notifyRoutingRaw: trimToNull(r.notify_routing),\n mailchimpApiKey: trimToNull(r.mailchimp_api_key),\n mailchimpAudienceId: trimToNull(r.mailchimp_audience_id),\n requireTurnstile: typeof r.require_turnstile === \"number\" ? r.require_turnstile !== 0 : false,\n renovateFailingCis: num(r.renovate_failing_cis),\n defaultBranchCi: str(r.default_branch_ci),\n lastCommitAt: str(r.last_commit_at),\n githubSignalsAt: str(r.github_signals_at),\n smokeOk: toVerdict(r.smoke_ok),\n lastSmokeAt: str(r.last_smoke_at),\n formE2eOk: toVerdict(r.form_e2e_ok),\n formE2eCheckedAt: str(r.form_e2e_checked_at),\n prismicModels: toPrismicModelsVerdict(r.prismic_models),\n prismicModelsCheckedAt: str(r.prismic_models_checked_at),\n prismicModelsDrift: str(r.prismic_models_drift),\n prismicAckUntil: str(r.prismic_ack_until),\n nextMaintenanceAt: str(r.next_maintenance_at),\n nextTestingAt: str(r.next_testing_at),\n };\n}\n\n/** Every sites column EXCEPT the header-image BLOB. Since the 2026-08-24\n * backfill, header_image holds 0.7–3.5 MB JPEGs — a selectAll would haul\n * megabytes into every site lookup (per LEAD on the ingest path, ×44 on the\n * fleet list) and Turso bills the bytes. WebsiteRow only needs the metadata\n * columns; the bytes have their own dedicated readers when a consumer wants\n * them. Kept in lockstep with SitesTable by the blob-exclusion test. */\nconst SITE_COLUMNS = [\n \"sites.id\",\n \"sites.slug\",\n \"sites.name\",\n \"sites.url\",\n \"sites.status\",\n \"sites.point_of_contact\",\n \"sites.maintenance_freq\",\n \"sites.testing_freq\",\n \"sites.maintenance_day\",\n \"sites.testing_day\",\n \"sites.ga4_property_id\",\n \"sites.search_query\",\n \"sites.search_console_property\",\n \"sites.git_repo\",\n \"sites.netlify_id\",\n \"sites.report_recipients_to\",\n \"sites.report_recipients_cc\",\n \"sites.copy_intro\",\n \"sites.copy_contact\",\n \"sites.copy_footer\",\n \"sites.newsletter_webhook\",\n \"sites.mailchimp_api_key\",\n \"sites.mailchimp_audience_id\",\n \"sites.notify_routing\",\n \"sites.require_turnstile\",\n \"sites.accepted_watch_conditions\",\n \"sites.prismic_ack_until\",\n \"sites.launched_at\",\n \"sites.header_image_filename\",\n \"sites.header_image_type\",\n \"sites.header_image_generated_at\",\n \"sites.legacy\",\n] as const;\n\n/** The three-table join every read composes from. site_health/site_schedule\n * rows are upserted alongside sites by the importer, but LEFT JOIN anyway —\n * a site row must never vanish from a surface because a health row is absent. */\nfunction joined(db: Db) {\n return db\n .selectFrom(\"sites\")\n .leftJoin(\"site_health\", \"site_health.site_id\", \"sites.id\")\n .leftJoin(\"site_schedule\", \"site_schedule.site_id\", \"sites.id\")\n .select(SITE_COLUMNS)\n .selectAll([\"site_health\", \"site_schedule\"]);\n}\n\n/** Write-through for the site-detail editor (Phase 2): after the Airtable\n * write (still the Phase 2 source of truth), mirror the same cell into\n * `sites` so a Turso-reading page renders the edit immediately instead of\n * waiting for the next hourly sync. The column mapping is the IMPORTER's own\n * map — one truth — and the value gets the importer's empty-clears-to-null\n * semantics. Throws on a column the importer doesn't claim (the lockstep\n * test makes that unreachable for the editor's allowlist); the caller treats\n * a mirror failure as non-fatal — the hourly sync converges it.\n *\n * Deliberately NOT applicable to `Require Turnstile` or other non-text\n * columns: every editor field is plain text today, and the lockstep test\n * pins that assumption. */\nexport async function mirrorSiteField(\n db: Db,\n siteId: string,\n airtableColumn: string,\n value: string,\n): Promise<void> {\n const col = SITE_FIELDS[airtableColumn];\n if (!col)\n throw new Error(`mirrorSiteField: importer claims no sites column for '${airtableColumn}'`);\n const stored = value.trim() === \"\" ? null : value;\n await db\n .updateTable(\"sites\")\n .set({ [col]: stored })\n .where(\"id\", \"=\", siteId)\n .execute();\n}\n\n/** Write-through mirror for the nightly health writers (#539 Phase 3). Takes\n * the EXACT FieldSet just written to Airtable (updateAuditFields /\n * updateGitHubSignals return it), so the mirror can never carry a different\n * payload than the Airtable write it shadows. Resolution + coercion come from\n * the importer's healthColumnFor — one truth; an Airtable column no\n * site_health column claims throws (same contract as mirrorSiteField).\n * Partial by design: absent fields stay untouched, matching\n * updateGitHubSignals' deliberate omission of a null lastCommitAt. Returns\n * whether a site_health row matched: a site the hourly sync hasn't imported\n * yet updates 0 rows → false, so the caller can count it honestly\n * (mirror_missed) instead of claiming it mirrored — it still converges on the\n * next sync, like every mirror. An empty FieldSet runs no SQL and returns\n * true: nothing to mirror is not a miss. */\nexport async function mirrorHealthFields(\n db: Db,\n siteId: string,\n fields: Record<string, unknown>,\n): Promise<boolean> {\n // Per-column value types (number vs text) are guaranteed by healthColumnFor's\n // coercion — the numeric/text split lives there, once — so the patch builds\n // untyped and casts at the .set() boundary (the importer's own idiom).\n const patch: Record<string, string | number | null> = {};\n for (const [field, value] of Object.entries(fields)) {\n const m = healthColumnFor(field);\n if (!m)\n throw new Error(`mirrorHealthFields: importer claims no site_health column for '${field}'`);\n patch[m.col] = m.coerce(value);\n }\n if (Object.keys(patch).length === 0) return true;\n const res = await db\n .updateTable(\"site_health\")\n .set(patch as Updateable<SiteHealthTable>)\n .where(\"site_id\", \"=\", siteId)\n .executeTakeFirst();\n // kysely/libSQL reports numUpdatedRows as a BigInt — compare in BigInt.\n return res.numUpdatedRows > 0n;\n}\n\n/** The site_schedule twin of {@link mirrorHealthFields}, for the nightly\n * next-due write-back. `computedAt` stamps when THIS computation ran — the\n * hourly sync overwrites it with its own import stamp, same as every mirrored\n * value. Empty fields → full no-op (no lone computed_at stamp for a write\n * that carried nothing). Same return contract as mirrorHealthFields: false\n * when the UPDATE matched no site_schedule row (site not yet imported), true\n * otherwise — including the empty-fields no-op. */\nexport async function mirrorScheduleFields(\n db: Db,\n siteId: string,\n fields: Record<string, unknown>,\n computedAt: string,\n): Promise<boolean> {\n const patch: Record<string, string | null> = {};\n for (const [field, value] of Object.entries(fields)) {\n const m = scheduleColumnFor(field);\n if (!m)\n throw new Error(\n `mirrorScheduleFields: importer claims no site_schedule column for '${field}'`,\n );\n patch[m.col] = m.coerce(value);\n }\n if (Object.keys(patch).length === 0) return true;\n patch.computed_at = computedAt;\n const res = await db\n .updateTable(\"site_schedule\")\n .set(patch as Updateable<SiteScheduleTable>)\n .where(\"site_id\", \"=\", siteId)\n .executeTakeFirst();\n return res.numUpdatedRows > 0n;\n}\n\n/** Same contract as the Airtable getWebsiteBySlug: slug is siteSlug(Name),\n * precomputed into the UNIQUE sites.slug column at import time. */\nexport async function getSiteBySlug(db: Db, slug: string): Promise<WebsiteRow | null> {\n const r = await joined(db).where(\"sites.slug\", \"=\", slug).executeTakeFirst();\n return r ? rowFromJoined(r as JoinedRow) : null;\n}\n\n/** By Airtable rec id (the PK) — approve-report's lookup shape. */\nexport async function getSiteById(db: Db, id: string): Promise<WebsiteRow | null> {\n const r = await joined(db).where(\"sites.id\", \"=\", id).executeTakeFirst();\n return r ? rowFromJoined(r as JoinedRow) : null;\n}\n\n/** Same contract as the Airtable listWebsites: every site, one row each.\n * Name-ordered for determinism (Airtable returned table order; no consumer\n * is order-sensitive — the cockpit groups and sorts itself). */\nexport async function listSites(db: Db): Promise<WebsiteRow[]> {\n const rows = await joined(db).orderBy(\"sites.name\").execute();\n return rows.map((r) => rowFromJoined(r as JoinedRow));\n}\n\n// ————————————————————————— reports —————————————————————————\n\n/** stable checklist key → Airtable column name. The importer stores stable keys\n * (mapReportRecord); `ReportRow.checklist` exposes Airtable column names — one\n * derived map, built from the checklist definitions themselves. */\nconst CHECKLIST_FIELD_BY_KEY: ReadonlyMap<string, string> = new Map(\n [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map((i) => [i.key, i.field]),\n);\n\nfunction checklistFromStored(raw: string | null): Record<string, boolean> {\n let stored: Record<string, unknown> = {};\n if (typeof raw === \"string\" && raw.trim() !== \"\") {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed))\n stored = parsed as Record<string, unknown>;\n } catch {\n // fall through to all-false — a bad blob must not throw the page\n }\n }\n const out: Record<string, boolean> = {};\n for (const [key, field] of CHECKLIST_FIELD_BY_KEY) out[field] = Boolean(stored[key]);\n return out;\n}\n\nfunction reportRowFromDb(r: Selectable<ReportsTable>): ReportRow {\n const p = r.lighthouse_performance;\n const a = r.lighthouse_accessibility;\n const b = r.lighthouse_best_practices;\n const seo = r.lighthouse_seo;\n const lighthouse =\n typeof p === \"number\" &&\n typeof a === \"number\" &&\n typeof b === \"number\" &&\n typeof seo === \"number\"\n ? { performance: p, accessibility: a, bestPractices: b, seo }\n : null;\n return {\n id: r.id,\n reportId: r.report_id ?? \"\",\n siteId: r.site_id ?? \"\",\n reportType: toReportType(r.report_type ?? undefined),\n period: r.period,\n periodStart: r.period_start,\n periodEnd: r.period_end,\n completedOn: r.completed_on,\n lighthouse,\n gaUsersCurrent: r.ga_users_current,\n gaUsersPrevious: r.ga_users_previous,\n searchFoundPage1: bool(r.search_found_page1),\n searchPosition: r.search_position,\n lastTestedDate: r.last_tested_date,\n commentary: r.commentary,\n subjectOverride: r.subject_override,\n draftReady: r.draft_ready !== 0,\n approvedToSend: r.approved_to_send !== 0,\n sentAt: r.sent_at,\n approvedAt: r.approved_at,\n approvedBy: r.approved_by,\n deliveryStatus: (r.delivery_status ?? \"pending\") as DeliveryStatus,\n // The body lives IN the row (rendered_html) — the link points at the\n // dashboard's own preview route instead of an EXPIRING Airtable signed URL.\n // Strictly better for the operator: the old link 404'd once the URL aged out.\n renderedHtmlAttachment:\n r.rendered_html !== null\n ? { url: `/api/reports/${r.id}/preview`, filename: `${r.report_id ?? r.id}.html` }\n : null,\n resendMessageId: r.resend_message_id,\n checklist: checklistFromStored(r.checklist),\n autoEvidence: parseAutoEvidence(r.checklist_auto_evidence),\n sendOverride: r.send_override !== 0,\n overrideReason: r.override_reason,\n overrideBy: r.override_by,\n overrideAt: r.override_at,\n };\n}\n\n/** Same contract as the Airtable listAllReports. Newest-period-first (Airtable\n * returned manual table order; every consumer filters/sorts itself). */\nexport async function listAllReports(db: Db): Promise<ReportRow[]> {\n const rows = await db\n .selectFrom(\"reports\")\n .selectAll()\n .orderBy(\"period_start\", \"desc\")\n .orderBy(\"id\")\n .execute();\n return rows.map(reportRowFromDb);\n}\n\n/** Same contract as the Airtable listReportsForSite — served by idx_reports_site. */\nexport async function listReportsForSite(db: Db, siteId: string): Promise<ReportRow[]> {\n const rows = await db\n .selectFrom(\"reports\")\n .selectAll()\n .where(\"site_id\", \"=\", siteId)\n .orderBy(\"period_start\", \"desc\")\n .orderBy(\"id\")\n .execute();\n return rows.map(reportRowFromDb);\n}\n\n/** Columns the REQUEST-PATH report writers mirror after their Airtable write\n * (same pattern as mirrorSiteField): the approve/override flow and the\n * resend-webhook's delivery status. Send-path columns (sent_at,\n * resend_message_id, rendered_html) are deliberately absent — the send is a\n * CLI batch whose writes reach Turso via the hourly sync until Phase 3. */\nexport type ReportMirrorPatch = Partial<\n Pick<\n ReportsTable,\n | \"approved_to_send\"\n | \"approved_at\"\n | \"approved_by\"\n | \"send_override\"\n | \"override_reason\"\n | \"override_by\"\n | \"override_at\"\n | \"delivery_status\"\n >\n>;\n\n/** Mirror an Airtable report write into Turso so the page re-render after an\n * approve/override/bounce shows the new state immediately instead of after\n * the next hourly sync. Callers treat a failure as non-fatal (the sync\n * converges it); an empty patch is a no-op, never invalid SQL. */\nexport async function mirrorReportPatch(\n db: Db,\n reportId: string,\n patch: ReportMirrorPatch,\n): Promise<void> {\n if (Object.keys(patch).length === 0) return;\n await db.updateTable(\"reports\").set(patch).where(\"id\", \"=\", reportId).execute();\n}\n\n/** By rec id (the PK) — approve-report's read. */\nexport async function getReportById(db: Db, id: string): Promise<ReportRow | null> {\n const r = await db.selectFrom(\"reports\").selectAll().where(\"id\", \"=\", id).executeTakeFirst();\n return r ? reportRowFromDb(r) : null;\n}\n\n/** The preview route's read: the stored rendered body, or null when the report\n * has none (imported while its signed URL was expired, or predates rendering). */\nexport async function getReportHtml(\n db: Db,\n id: string,\n): Promise<{ html: string; reportId: string | null } | null> {\n const r = await db\n .selectFrom(\"reports\")\n .select([\"rendered_html\", \"report_id\"])\n .where(\"id\", \"=\", id)\n .executeTakeFirst();\n if (!r || r.rendered_html === null) return null;\n return { html: r.rendered_html, reportId: r.report_id };\n}\n","import { openDb, readDbConfig, type Db } from \"../db/client.js\";\nimport { mirrorHealthFields, mirrorScheduleFields } from \"../db/fleet-state.js\";\n\n/** One site's just-written Airtable FieldSet, mirrored into site_health.\n * Resolves true when a site_health row matched; false when the UPDATE touched\n * 0 rows (site created in Airtable after the last hourly import) — callers\n * count that as mirror_missed, never as mirrored. */\nexport type HealthMirror = (siteId: string, fields: Record<string, unknown>) => Promise<boolean>;\n\n/** The site_schedule twin, for the nightly next-due write-back. Same\n * matched-row contract as {@link HealthMirror}: false = 0-row UPDATE →\n * mirror_missed. */\nexport type ScheduleMirror = (\n siteId: string,\n fields: Record<string, unknown>,\n computedAt: string,\n) => Promise<boolean>;\n\n/** Build the Turso write-through for the nightly writers (#539 Phase 3\n * dual-write), or null when libSQL creds are absent — mirroring NOT attempted\n * is a different outcome than mirroring failed, and the caller's summary line\n * reports mirror counts only when a mirror existed. Same contract as\n * recordFleetEventsBestEffort: the mirror must never fail the sweep that\n * produced the data (per-site failures are the caller's to count; Airtable\n * stays authoritative and the hourly sync converges whatever the mirror\n * missed). `open` is injectable for tests. */\nexport async function makeHealthMirrorBestEffort(\n open: () => Promise<Db> = () => openDb(readDbConfig()),\n): Promise<HealthMirror | null> {\n let db: Db;\n try {\n db = await open();\n } catch (e) {\n console.error(`[health-mirror] mirroring disabled: no libSQL (${String(e)})`);\n return null;\n }\n return (siteId, fields) => mirrorHealthFields(db, siteId, fields);\n}\n\n/** {@link makeHealthMirrorBestEffort}'s schedule twin — same null-without-creds\n * contract, for `writeNextDueDates`' site_schedule write-through. */\nexport async function makeScheduleMirrorBestEffort(\n open: () => Promise<Db> = () => openDb(readDbConfig()),\n): Promise<ScheduleMirror | null> {\n let db: Db;\n try {\n db = await open();\n } catch (e) {\n console.error(`[schedule-mirror] mirroring disabled: no libSQL (${String(e)})`);\n return null;\n }\n return (siteId, fields, computedAt) => mirrorScheduleFields(db, siteId, fields, computedAt);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgQA,eAAsB,mBACpB,IACA,QACA,QACkB;AAIlB,QAAM,QAAgD,CAAC;AACvD,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACnD,UAAM,IAAI,gBAAgB,KAAK;AAC/B,QAAI,CAAC;AACH,YAAM,IAAI,MAAM,kEAAkE,KAAK,GAAG;AAC5F,UAAM,EAAE,GAAG,IAAI,EAAE,OAAO,KAAK;AAAA,EAC/B;AACA,MAAI,OAAO,KAAK,KAAK,EAAE,WAAW,EAAG,QAAO;AAC5C,QAAM,MAAM,MAAM,GACf,YAAY,aAAa,EACzB,IAAI,KAAoC,EACxC,MAAM,WAAW,KAAK,MAAM,EAC5B,iBAAiB;AAEpB,SAAO,IAAI,iBAAiB;AAC9B;AASA,eAAsB,qBACpB,IACA,QACA,QACA,YACkB;AAClB,QAAM,QAAuC,CAAC;AAC9C,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACnD,UAAM,IAAI,kBAAkB,KAAK;AACjC,QAAI,CAAC;AACH,YAAM,IAAI;AAAA,QACR,sEAAsE,KAAK;AAAA,MAC7E;AACF,UAAM,EAAE,GAAG,IAAI,EAAE,OAAO,KAAK;AAAA,EAC/B;AACA,MAAI,OAAO,KAAK,KAAK,EAAE,WAAW,EAAG,QAAO;AAC5C,QAAM,cAAc;AACpB,QAAM,MAAM,MAAM,GACf,YAAY,eAAe,EAC3B,IAAI,KAAsC,EAC1C,MAAM,WAAW,KAAK,MAAM,EAC5B,iBAAiB;AACpB,SAAO,IAAI,iBAAiB;AAC9B;AA4BA,IAAM,yBAAsD,IAAI;AAAA,EAC9D,CAAC,GAAG,uBAAuB,GAAG,iBAAiB,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC;AAC9E;;;AC3TA,eAAsB,2BACpB,OAA0B,MAAM,OAAO,aAAa,CAAC,GACvB;AAC9B,MAAI;AACJ,MAAI;AACF,SAAK,MAAM,KAAK;AAAA,EAClB,SAAS,GAAG;AACV,YAAQ,MAAM,kDAAkD,OAAO,CAAC,CAAC,GAAG;AAC5E,WAAO;AAAA,EACT;AACA,SAAO,CAAC,QAAQ,WAAW,mBAAmB,IAAI,QAAQ,MAAM;AAClE;AAIA,eAAsB,6BACpB,OAA0B,MAAM,OAAO,aAAa,CAAC,GACrB;AAChC,MAAI;AACJ,MAAI;AACF,SAAK,MAAM,KAAK;AAAA,EAClB,SAAS,GAAG;AACV,YAAQ,MAAM,oDAAoD,OAAO,CAAC,CAAC,GAAG;AAC9E,WAAO;AAAA,EACT;AACA,SAAO,CAAC,QAAQ,QAAQ,eAAe,qBAAqB,IAAI,QAAQ,QAAQ,UAAU;AAC5F;","names":[]}
@@ -0,0 +1,35 @@
1
+ import {
2
+ EXCLUDED_WEBSITE_FIELDS,
3
+ HEALTH_BOOLEAN,
4
+ HEALTH_FIELDS,
5
+ SCHEDULE_FIELDS,
6
+ SITE_FIELDS,
7
+ formatReapSummary,
8
+ healthColumnFor,
9
+ importFleetState,
10
+ mapReportRecord,
11
+ mapWebsiteRecord,
12
+ reapAllowance,
13
+ reapRefusal,
14
+ renderedHtmlUrl,
15
+ scheduleColumnFor
16
+ } from "./chunk-ITIOFN7A.js";
17
+ import "./chunk-VAD43MOR.js";
18
+ import "./chunk-LDWZSLSU.js";
19
+ export {
20
+ EXCLUDED_WEBSITE_FIELDS,
21
+ HEALTH_BOOLEAN,
22
+ HEALTH_FIELDS,
23
+ SCHEDULE_FIELDS,
24
+ SITE_FIELDS,
25
+ formatReapSummary,
26
+ healthColumnFor,
27
+ importFleetState,
28
+ mapReportRecord,
29
+ mapWebsiteRecord,
30
+ reapAllowance,
31
+ reapRefusal,
32
+ renderedHtmlUrl,
33
+ scheduleColumnFor
34
+ };
35
+ //# sourceMappingURL=import-airtable-HE43FB4H.js.map