@reddoorla/maintenance 0.87.0 → 0.88.3

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 (189) hide show
  1. package/dist/airtable-QG5ICI3Y.js +9 -0
  2. package/dist/{announce-I2Z4JN3I.js → announce-GKD5YJNB.js} +9 -9
  3. package/dist/{blux-ZGDSHI3U.js → blux-EATHOHKK.js} +5 -5
  4. package/dist/{bump-deps-CGVELRLK.js → bump-deps-KGH5UYSF.js} +4 -4
  5. package/dist/{chunk-SDDPUAWH.js → chunk-6I6L3CMX.js} +2 -2
  6. package/dist/{chunk-NXNPDCF3.js → chunk-6UCVWYDA.js} +11 -1
  7. package/dist/chunk-6UCVWYDA.js.map +1 -0
  8. package/dist/{chunk-ZBDZTJUO.js → chunk-7FFUBF4I.js} +5 -2
  9. package/dist/chunk-7FFUBF4I.js.map +1 -0
  10. package/dist/{chunk-3JQ2FO3D.js → chunk-B23FFWU4.js} +2 -2
  11. package/dist/{chunk-DJZWYIPP.js → chunk-BGTYPVLT.js} +1 -1
  12. package/dist/chunk-BGTYPVLT.js.map +1 -0
  13. package/dist/chunk-BRYXFH77.js +70 -0
  14. package/dist/chunk-BRYXFH77.js.map +1 -0
  15. package/dist/{chunk-HSI6Q7MI.js → chunk-CEW5PEHZ.js} +168 -31
  16. package/dist/chunk-CEW5PEHZ.js.map +1 -0
  17. package/dist/{chunk-NDWYJLRF.js → chunk-DGZWFI2G.js} +3 -3
  18. package/dist/{chunk-HHMMUR4P.js → chunk-GQDDWYU2.js} +45 -8
  19. package/dist/chunk-GQDDWYU2.js.map +1 -0
  20. package/dist/{chunk-BBUSGEES.js → chunk-J3WEZGIN.js} +6 -6
  21. package/dist/{chunk-BNM5POSJ.js → chunk-JRHIFA5R.js} +2 -2
  22. package/dist/{chunk-MT2JINSS.js → chunk-KJGKZLCT.js} +3 -3
  23. package/dist/{chunk-3SF7D4DY.js → chunk-MFHS7IQ2.js} +36 -1
  24. package/dist/{chunk-3SF7D4DY.js.map → chunk-MFHS7IQ2.js.map} +1 -1
  25. package/dist/{chunk-SNSTJLQW.js → chunk-NO74DXDP.js} +48 -2
  26. package/dist/chunk-NO74DXDP.js.map +1 -0
  27. package/dist/{chunk-C6B4RHXR.js → chunk-O3GT46R2.js} +8 -2
  28. package/dist/chunk-O3GT46R2.js.map +1 -0
  29. package/dist/{chunk-C6CLRKPV.js → chunk-PR343K7F.js} +2 -2
  30. package/dist/{chunk-CROKUHJB.js → chunk-Q47DU255.js} +4 -4
  31. package/dist/{chunk-EVBCK2KC.js → chunk-RARREDQE.js} +2 -1
  32. package/dist/{chunk-EVBCK2KC.js.map → chunk-RARREDQE.js.map} +1 -1
  33. package/dist/chunk-RUPPWAD6.js +7 -0
  34. package/dist/chunk-RUPPWAD6.js.map +1 -0
  35. package/dist/{chunk-VSBFM7DS.js → chunk-SJOXWJ5Y.js} +3 -3
  36. package/dist/{chunk-HEWSRXYV.js → chunk-ULHPALI5.js} +3 -3
  37. package/dist/{chunk-XUGSZP7Z.js → chunk-W45FZEGE.js} +2 -2
  38. package/dist/{chunk-NCMZMLY7.js → chunk-WPL5GERE.js} +5 -69
  39. package/dist/chunk-WPL5GERE.js.map +1 -0
  40. package/dist/{chunk-JHGHLKFS.js → chunk-WS6NU475.js} +2 -2
  41. package/dist/{chunk-DEWJMMG3.js → chunk-XKDCR4UE.js} +19 -8
  42. package/dist/chunk-XKDCR4UE.js.map +1 -0
  43. package/dist/{chunk-ZGBEIWSC.js → chunk-XP5WM2LV.js} +5 -5
  44. package/dist/{chunk-TD2NNXLZ.js → chunk-XSOINOPY.js} +8 -8
  45. package/dist/{chunk-6TWWY7ZU.js → chunk-Z3JXLULD.js} +2 -2
  46. package/dist/cli/bin.js +31 -28
  47. package/dist/cli/bin.js.map +1 -1
  48. package/dist/cli/commands/audit.d.ts +4 -0
  49. package/dist/cli/commands/audit.js +16 -13
  50. package/dist/cli/commands/audit.js.map +1 -1
  51. package/dist/client-JYKPVCJX.js +11 -0
  52. package/dist/configs/playwright-a11y.js +1 -1
  53. package/dist/{convert-to-pnpm-QFJOEP5V.js → convert-to-pnpm-NWB5FCQ2.js} +4 -4
  54. package/dist/{db-UXNIZ2RG.js → db-DXUUAMXT.js} +121 -26
  55. package/dist/db-DXUUAMXT.js.map +1 -0
  56. package/dist/{digest-FW6QMZZJ.js → digest-NBLECFM7.js} +110 -29
  57. package/dist/digest-NBLECFM7.js.map +1 -0
  58. package/dist/digest-collectors-NM7WUWQP.js +50 -0
  59. package/dist/digest-state-3KHZF7GD.js +50 -0
  60. package/dist/digest-state-3KHZF7GD.js.map +1 -0
  61. package/dist/digest-state-F37XUCS5.js +13 -0
  62. package/dist/{dump-P6EBSIOH.js → dump-EPL7AZFR.js} +38 -13
  63. package/dist/dump-EPL7AZFR.js.map +1 -0
  64. package/dist/{email-KJXASVH5.js → email-FMMEUPX6.js} +16 -6
  65. package/dist/{email-KJXASVH5.js.map → email-FMMEUPX6.js.map} +1 -1
  66. package/dist/{ensure-site-4DCEBAPM.js → ensure-site-FU6527OU.js} +2 -2
  67. package/dist/{fleet-state-7TJ2GYT2.js → fleet-state-R5LU7IC3.js} +4 -2
  68. package/dist/{forms-notify-target-2EAWCWBW.js → forms-notify-target-SGDGXXK5.js} +6 -6
  69. package/dist/{github-signals-E2WPYTBK.js → github-signals-DB25UWY4.js} +7 -6
  70. package/dist/{github-signals-E2WPYTBK.js.map → github-signals-DB25UWY4.js.map} +1 -1
  71. package/dist/{header-image-6IC53W27.js → header-image-7A7XLVEU.js} +2 -2
  72. package/dist/{health-endpoint-RFXL2F4A.js → health-endpoint-UVX6R5N5.js} +4 -4
  73. package/dist/{health-mirror-L5JBEJVA.js → health-mirror-GCDN7BJL.js} +11 -6
  74. package/dist/health-mirror-GCDN7BJL.js.map +1 -0
  75. package/dist/index.d.ts +3 -0
  76. package/dist/index.js +136 -95
  77. package/dist/index.js.map +1 -1
  78. package/dist/{init-XRAQZYID.js → init-32AWPS7A.js} +9 -9
  79. package/dist/{launch-IX4Y5JJC.js → launch-52WUE4TZ.js} +12 -11
  80. package/dist/{launch-IX4Y5JJC.js.map → launch-52WUE4TZ.js.map} +1 -1
  81. package/dist/migrate-YITCXBLS.js +7 -0
  82. package/dist/{notify-C645PGD5.js → notify-4HP4GZ2H.js} +5 -5
  83. package/dist/{onboard-O4IAHJMO.js → onboard-BKCYBAVW.js} +4 -4
  84. package/dist/{orchestrate-YLWQ6KK2.js → orchestrate-JNKCWU5K.js} +9 -9
  85. package/dist/{parity-56KA2DGR.js → parity-NBYZUBFU.js} +4 -2
  86. package/dist/pdf-N5QLQ5ZQ.js +38 -0
  87. package/dist/pdf-N5QLQ5ZQ.js.map +1 -0
  88. package/dist/{pipeline-3AANZFCR.js → pipeline-YIBIQ7XK.js} +3 -3
  89. package/dist/{preflight-TNORKGCX.js → preflight-IMTYDDD4.js} +10 -10
  90. package/dist/{prismic-ci-G2AK53VL.js → prismic-ci-MY2U6NN7.js} +4 -4
  91. package/dist/{prismic-models-O6Z3OK3A.js → prismic-models-IJWZW2EO.js} +6 -6
  92. package/dist/prospect/types.d.ts +33 -0
  93. package/dist/{prospect-audit-KSXUNQ5J.js → prospect-audit-WJQI7V5V.js} +32 -13
  94. package/dist/prospect-audit-WJQI7V5V.js.map +1 -0
  95. package/dist/render-CVKE7IWC.js +10 -0
  96. package/dist/{render-from-row-BWSOIYDS.js → render-from-row-DOYJDO2L.js} +5 -5
  97. package/dist/{replay-C2LXMJZP.js → replay-4S6ENYCH.js} +3 -3
  98. package/dist/{report-R3E3Y7HR.js → report-2LM2SX67.js} +20 -20
  99. package/dist/{report-mirror-4MNORYQ4.js → report-mirror-AAK3MXHY.js} +12 -6
  100. package/dist/report-mirror-AAK3MXHY.js.map +1 -0
  101. package/dist/schema-75PJECAE.js +18 -0
  102. package/dist/schema-75PJECAE.js.map +1 -0
  103. package/dist/{self-updating-UCIWESAX.js → self-updating-NTCCWJYZ.js} +4 -4
  104. package/dist/{selftest-VGHWPN72.js → selftest-DIZ5JD2X.js} +12 -12
  105. package/dist/{site-mirror-NIREX5ME.js → site-mirror-Q763PLGG.js} +17 -7
  106. package/dist/site-mirror-Q763PLGG.js.map +1 -0
  107. package/dist/{smoke-suite-U3UKD3NS.js → smoke-suite-4JFZZYVW.js} +4 -4
  108. package/dist/{spam-classifier-XLBFCO7G.js → spam-classifier-T5H4VTP6.js} +4 -2
  109. package/dist/spam-classifier-T5H4VTP6.js.map +1 -0
  110. package/dist/{submissions-IQEJX6FK.js → submissions-TEHPBSZJ.js} +4 -4
  111. package/dist/{svelte-codemods-GOKXTBHW.js → svelte-codemods-QAPDZC6F.js} +4 -4
  112. package/dist/{sync-CTUVXMCK.js → sync-GBFV45WX.js} +4 -2
  113. package/dist/{sync-CTUVXMCK.js.map → sync-GBFV45WX.js.map} +1 -1
  114. package/dist/{sync-configs-SPTLFJCC.js → sync-configs-34JIA6KZ.js} +4 -4
  115. package/dist/{upgrade-W6ZR3FJH.js → upgrade-IR2OO3KP.js} +4 -4
  116. package/dist/{webhook-VOGOW3W6.js → webhook-XM6EV4XT.js} +2 -2
  117. package/dist/{write-audits-to-airtable-CMKHGJNQ.js → write-audits-to-airtable-OPBJXGS4.js} +5 -2
  118. package/dist/write-audits-to-airtable-OPBJXGS4.js.map +1 -0
  119. package/package.json +1 -1
  120. package/dist/airtable-ZBUPQJSC.js +0 -9
  121. package/dist/chunk-C6B4RHXR.js.map +0 -1
  122. package/dist/chunk-DEWJMMG3.js.map +0 -1
  123. package/dist/chunk-DJZWYIPP.js.map +0 -1
  124. package/dist/chunk-HHMMUR4P.js.map +0 -1
  125. package/dist/chunk-HSI6Q7MI.js.map +0 -1
  126. package/dist/chunk-NCMZMLY7.js.map +0 -1
  127. package/dist/chunk-NXNPDCF3.js.map +0 -1
  128. package/dist/chunk-SNSTJLQW.js.map +0 -1
  129. package/dist/chunk-ZBDZTJUO.js.map +0 -1
  130. package/dist/client-AUE3XUPO.js +0 -11
  131. package/dist/db-UXNIZ2RG.js.map +0 -1
  132. package/dist/digest-FW6QMZZJ.js.map +0 -1
  133. package/dist/dump-P6EBSIOH.js.map +0 -1
  134. package/dist/health-mirror-L5JBEJVA.js.map +0 -1
  135. package/dist/migrate-GQH237A6.js +0 -7
  136. package/dist/prospect-audit-KSXUNQ5J.js.map +0 -1
  137. package/dist/render-IFOGFB5T.js +0 -10
  138. package/dist/report-mirror-4MNORYQ4.js.map +0 -1
  139. package/dist/site-mirror-NIREX5ME.js.map +0 -1
  140. /package/dist/{airtable-ZBUPQJSC.js.map → airtable-QG5ICI3Y.js.map} +0 -0
  141. /package/dist/{announce-I2Z4JN3I.js.map → announce-GKD5YJNB.js.map} +0 -0
  142. /package/dist/{blux-ZGDSHI3U.js.map → blux-EATHOHKK.js.map} +0 -0
  143. /package/dist/{bump-deps-CGVELRLK.js.map → bump-deps-KGH5UYSF.js.map} +0 -0
  144. /package/dist/{chunk-SDDPUAWH.js.map → chunk-6I6L3CMX.js.map} +0 -0
  145. /package/dist/{chunk-3JQ2FO3D.js.map → chunk-B23FFWU4.js.map} +0 -0
  146. /package/dist/{chunk-NDWYJLRF.js.map → chunk-DGZWFI2G.js.map} +0 -0
  147. /package/dist/{chunk-BBUSGEES.js.map → chunk-J3WEZGIN.js.map} +0 -0
  148. /package/dist/{chunk-BNM5POSJ.js.map → chunk-JRHIFA5R.js.map} +0 -0
  149. /package/dist/{chunk-MT2JINSS.js.map → chunk-KJGKZLCT.js.map} +0 -0
  150. /package/dist/{chunk-C6CLRKPV.js.map → chunk-PR343K7F.js.map} +0 -0
  151. /package/dist/{chunk-CROKUHJB.js.map → chunk-Q47DU255.js.map} +0 -0
  152. /package/dist/{chunk-VSBFM7DS.js.map → chunk-SJOXWJ5Y.js.map} +0 -0
  153. /package/dist/{chunk-HEWSRXYV.js.map → chunk-ULHPALI5.js.map} +0 -0
  154. /package/dist/{chunk-XUGSZP7Z.js.map → chunk-W45FZEGE.js.map} +0 -0
  155. /package/dist/{chunk-JHGHLKFS.js.map → chunk-WS6NU475.js.map} +0 -0
  156. /package/dist/{chunk-ZGBEIWSC.js.map → chunk-XP5WM2LV.js.map} +0 -0
  157. /package/dist/{chunk-TD2NNXLZ.js.map → chunk-XSOINOPY.js.map} +0 -0
  158. /package/dist/{chunk-6TWWY7ZU.js.map → chunk-Z3JXLULD.js.map} +0 -0
  159. /package/dist/{client-AUE3XUPO.js.map → client-JYKPVCJX.js.map} +0 -0
  160. /package/dist/{convert-to-pnpm-QFJOEP5V.js.map → convert-to-pnpm-NWB5FCQ2.js.map} +0 -0
  161. /package/dist/{fleet-state-7TJ2GYT2.js.map → digest-collectors-NM7WUWQP.js.map} +0 -0
  162. /package/dist/{migrate-GQH237A6.js.map → digest-state-F37XUCS5.js.map} +0 -0
  163. /package/dist/{ensure-site-4DCEBAPM.js.map → ensure-site-FU6527OU.js.map} +0 -0
  164. /package/dist/{notify-C645PGD5.js.map → fleet-state-R5LU7IC3.js.map} +0 -0
  165. /package/dist/{forms-notify-target-2EAWCWBW.js.map → forms-notify-target-SGDGXXK5.js.map} +0 -0
  166. /package/dist/{header-image-6IC53W27.js.map → header-image-7A7XLVEU.js.map} +0 -0
  167. /package/dist/{health-endpoint-RFXL2F4A.js.map → health-endpoint-UVX6R5N5.js.map} +0 -0
  168. /package/dist/{init-XRAQZYID.js.map → init-32AWPS7A.js.map} +0 -0
  169. /package/dist/{orchestrate-YLWQ6KK2.js.map → migrate-YITCXBLS.js.map} +0 -0
  170. /package/dist/{parity-56KA2DGR.js.map → notify-4HP4GZ2H.js.map} +0 -0
  171. /package/dist/{onboard-O4IAHJMO.js.map → onboard-BKCYBAVW.js.map} +0 -0
  172. /package/dist/{pipeline-3AANZFCR.js.map → orchestrate-JNKCWU5K.js.map} +0 -0
  173. /package/dist/{render-IFOGFB5T.js.map → parity-NBYZUBFU.js.map} +0 -0
  174. /package/dist/{render-from-row-BWSOIYDS.js.map → pipeline-YIBIQ7XK.js.map} +0 -0
  175. /package/dist/{preflight-TNORKGCX.js.map → preflight-IMTYDDD4.js.map} +0 -0
  176. /package/dist/{prismic-ci-G2AK53VL.js.map → prismic-ci-MY2U6NN7.js.map} +0 -0
  177. /package/dist/{prismic-models-O6Z3OK3A.js.map → prismic-models-IJWZW2EO.js.map} +0 -0
  178. /package/dist/{spam-classifier-XLBFCO7G.js.map → render-CVKE7IWC.js.map} +0 -0
  179. /package/dist/{write-audits-to-airtable-CMKHGJNQ.js.map → render-from-row-DOYJDO2L.js.map} +0 -0
  180. /package/dist/{replay-C2LXMJZP.js.map → replay-4S6ENYCH.js.map} +0 -0
  181. /package/dist/{report-R3E3Y7HR.js.map → report-2LM2SX67.js.map} +0 -0
  182. /package/dist/{self-updating-UCIWESAX.js.map → self-updating-NTCCWJYZ.js.map} +0 -0
  183. /package/dist/{selftest-VGHWPN72.js.map → selftest-DIZ5JD2X.js.map} +0 -0
  184. /package/dist/{smoke-suite-U3UKD3NS.js.map → smoke-suite-4JFZZYVW.js.map} +0 -0
  185. /package/dist/{submissions-IQEJX6FK.js.map → submissions-TEHPBSZJ.js.map} +0 -0
  186. /package/dist/{svelte-codemods-GOKXTBHW.js.map → svelte-codemods-QAPDZC6F.js.map} +0 -0
  187. /package/dist/{sync-configs-SPTLFJCC.js.map → sync-configs-34JIA6KZ.js.map} +0 -0
  188. /package/dist/{upgrade-W6ZR3FJH.js.map → upgrade-IR2OO3KP.js.map} +0 -0
  189. /package/dist/{webhook-VOGOW3W6.js.map → webhook-XM6EV4XT.js.map} +0 -0
@@ -1,7 +1,10 @@
1
+ import {
2
+ TURSO_IS_AUTHORITATIVE
3
+ } from "./chunk-RUPPWAD6.js";
1
4
  import {
2
5
  mirrorHealthFields,
3
6
  mirrorScheduleFields
4
- } from "./chunk-HHMMUR4P.js";
7
+ } from "./chunk-GQDDWYU2.js";
5
8
  import "./chunk-P6AORG32.js";
6
9
  import "./chunk-F3JWOPTG.js";
7
10
  import "./chunk-VAD43MOR.js";
@@ -9,26 +12,28 @@ import "./chunk-7NYN2P6I.js";
9
12
  import {
10
13
  openDb,
11
14
  readDbConfig
12
- } from "./chunk-JHGHLKFS.js";
13
- import "./chunk-3SF7D4DY.js";
15
+ } from "./chunk-WS6NU475.js";
16
+ import "./chunk-MFHS7IQ2.js";
14
17
  import "./chunk-6VAL7XJT.js";
15
18
 
16
19
  // src/audits/health-mirror.ts
17
- async function makeHealthMirrorBestEffort(open = () => openDb(readDbConfig())) {
20
+ async function makeHealthMirrorBestEffort(open = () => openDb(readDbConfig()), strict = TURSO_IS_AUTHORITATIVE) {
18
21
  let db;
19
22
  try {
20
23
  db = await open();
21
24
  } catch (e) {
25
+ if (strict) throw new Error(`health-mirror unavailable: ${String(e)}`, { cause: e });
22
26
  console.error(`[health-mirror] mirroring disabled: no libSQL (${String(e)})`);
23
27
  return null;
24
28
  }
25
29
  return (siteId, fields) => mirrorHealthFields(db, siteId, fields);
26
30
  }
27
- async function makeScheduleMirrorBestEffort(open = () => openDb(readDbConfig())) {
31
+ async function makeScheduleMirrorBestEffort(open = () => openDb(readDbConfig()), strict = TURSO_IS_AUTHORITATIVE) {
28
32
  let db;
29
33
  try {
30
34
  db = await open();
31
35
  } catch (e) {
36
+ if (strict) throw new Error(`schedule-mirror unavailable: ${String(e)}`, { cause: e });
32
37
  console.error(`[schedule-mirror] mirroring disabled: no libSQL (${String(e)})`);
33
38
  return null;
34
39
  }
@@ -38,4 +43,4 @@ export {
38
43
  makeHealthMirrorBestEffort,
39
44
  makeScheduleMirrorBestEffort
40
45
  };
41
- //# sourceMappingURL=health-mirror-L5JBEJVA.js.map
46
+ //# sourceMappingURL=health-mirror-GCDN7BJL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/audits/health-mirror.ts"],"sourcesContent":["import { openDb, readDbConfig, type Db } from \"../db/client.js\";\nimport { mirrorHealthFields, mirrorScheduleFields } from \"../db/fleet-state.js\";\nimport { TURSO_IS_AUTHORITATIVE } from \"../db/freeze.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 /** #612. `true` = Turso is the only store, so \"mirroring disabled\" is no\n * longer a survivable state: returning null would discard every write in the\n * sweep with nothing to converge it. Throws instead. Injected by tests so\n * both sides stay proven. */\n strict: boolean = TURSO_IS_AUTHORITATIVE,\n): Promise<HealthMirror | null> {\n let db: Db;\n try {\n db = await open();\n } catch (e) {\n if (strict) throw new Error(`health-mirror unavailable: ${String(e)}`, { cause: 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 /** #612 — same contract as {@link makeHealthMirrorBestEffort}'s. */\n strict: boolean = TURSO_IS_AUTHORITATIVE,\n): Promise<ScheduleMirror | null> {\n let db: Db;\n try {\n db = await open();\n } catch (e) {\n if (strict) throw new Error(`schedule-mirror unavailable: ${String(e)}`, { cause: 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":";;;;;;;;;;;;;;;;;;;AA2BA,eAAsB,2BACpB,OAA0B,MAAM,OAAO,aAAa,CAAC,GAKrD,SAAkB,wBACY;AAC9B,MAAI;AACJ,MAAI;AACF,SAAK,MAAM,KAAK;AAAA,EAClB,SAAS,GAAG;AACV,QAAI,OAAQ,OAAM,IAAI,MAAM,8BAA8B,OAAO,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC;AACnF,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,GAErD,SAAkB,wBACc;AAChC,MAAI;AACJ,MAAI;AACF,SAAK,MAAM,KAAK;AAAA,EAClB,SAAS,GAAG;AACV,QAAI,OAAQ,OAAM,IAAI,MAAM,gCAAgC,OAAO,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC;AACrF,YAAQ,MAAM,oDAAoD,OAAO,CAAC,CAAC,GAAG;AAC9E,WAAO;AAAA,EACT;AACA,SAAO,CAAC,QAAQ,QAAQ,eAAe,qBAAqB,IAAI,QAAQ,QAAQ,UAAU;AAC5F;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1500,9 +1500,12 @@ type CockpitModel = {
1500
1500
  /** NEW submissions across the fleet, newest-first (optional for back-compat). */
1501
1501
  submissions?: SubmissionEntry[];
1502
1502
  /** Fleet spam totals over the window (optional; populated by buildCockpitModel). */
1503
+ /** `computedAt` present ⇒ these came from the nightly roll-up and are up to
1504
+ * 24h old; the strip labels them so a stale number is never read as live. */
1503
1505
  spam?: {
1504
1506
  caught: number;
1505
1507
  through: number;
1508
+ computedAt?: string;
1506
1509
  } | null;
1507
1510
  /** Recent fleet-activity events for the "Recently" lane (optional for back-compat). */
1508
1511
  recent?: RecentEntry[];
package/dist/index.js CHANGED
@@ -1,19 +1,18 @@
1
1
  import "./chunk-2SLED3NB.js";
2
- import "./chunk-NCMZMLY7.js";
3
2
  import {
4
3
  draftReportForSite
5
- } from "./chunk-BBUSGEES.js";
6
- import {
7
- approveBlockers
8
- } from "./chunk-SDDPUAWH.js";
9
- import {
10
- findDueReports
11
- } from "./chunk-PZR4EBPY.js";
4
+ } from "./chunk-J3WEZGIN.js";
12
5
  import {
13
6
  DEFAULT_INIT_STEPS,
14
7
  a11yFixturesPage,
15
8
  init
16
- } from "./chunk-TD2NNXLZ.js";
9
+ } from "./chunk-XSOINOPY.js";
10
+ import {
11
+ convertToPnpm
12
+ } from "./chunk-XIDUYLSR.js";
13
+ import {
14
+ svelteCodemods
15
+ } from "./chunk-IYSEC543.js";
17
16
  import "./chunk-BQRFQSGY.js";
18
17
  import "./chunk-Y53WOQIJ.js";
19
18
  import {
@@ -29,18 +28,20 @@ import "./chunk-R53VL3RI.js";
29
28
  import {
30
29
  upgradeSvelte4to5
31
30
  } from "./chunk-Z3KYFUN6.js";
31
+ import "./chunk-J7SZQUCW.js";
32
+ import "./chunk-WPL5GERE.js";
32
33
  import {
33
- convertToPnpm
34
- } from "./chunk-XIDUYLSR.js";
34
+ approveBlockers
35
+ } from "./chunk-6I6L3CMX.js";
35
36
  import {
36
- svelteCodemods
37
- } from "./chunk-IYSEC543.js";
38
- import "./chunk-J7SZQUCW.js";
37
+ findDueReports
38
+ } from "./chunk-PZR4EBPY.js";
39
39
  import {
40
40
  parseAddresses,
41
41
  sendApprovedReports,
42
42
  withGlobalCc
43
- } from "./chunk-NDWYJLRF.js";
43
+ } from "./chunk-DGZWFI2G.js";
44
+ import "./chunk-BRYXFH77.js";
44
45
  import "./chunk-NYUYNYQL.js";
45
46
  import {
46
47
  isPendingApproval
@@ -52,28 +53,28 @@ import {
52
53
  } from "./chunk-VAD43MOR.js";
53
54
  import {
54
55
  fromAirtableBase
55
- } from "./chunk-C6CLRKPV.js";
56
+ } from "./chunk-PR343K7F.js";
56
57
  import "./chunk-IY6256GK.js";
57
58
  import "./chunk-HN6I2XWI.js";
58
- import "./chunk-ZGBEIWSC.js";
59
+ import "./chunk-XP5WM2LV.js";
59
60
  import "./chunk-GECESMVS.js";
61
+ import {
62
+ domainFromUrl,
63
+ generateHeaderImage,
64
+ loadHeadline,
65
+ loadPlate
66
+ } from "./chunk-5UINJNAJ.js";
60
67
  import {
61
68
  BLURRED_CID,
62
69
  CHECK_CID,
63
70
  loadBundledImages,
64
71
  renderReportHtml,
65
72
  rendersCommentary
66
- } from "./chunk-MT2JINSS.js";
73
+ } from "./chunk-KJGKZLCT.js";
67
74
  import {
68
75
  escapeHtml,
69
76
  safeUrl
70
- } from "./chunk-DJZWYIPP.js";
71
- import {
72
- domainFromUrl,
73
- generateHeaderImage,
74
- loadHeadline,
75
- loadPlate
76
- } from "./chunk-5UINJNAJ.js";
77
+ } from "./chunk-BGTYPVLT.js";
77
78
  import {
78
79
  CANONICAL_STATUSES,
79
80
  SEVERITY_RANK,
@@ -95,18 +96,18 @@ import {
95
96
  runAudits,
96
97
  runAuditsAcross,
97
98
  securityAudit
98
- } from "./chunk-3JQ2FO3D.js";
99
+ } from "./chunk-B23FFWU4.js";
99
100
  import "./chunk-LKGVSM2O.js";
100
101
  import "./chunk-GASBX52O.js";
101
102
  import "./chunk-XHHMM5HX.js";
102
103
  import {
103
104
  fromJsonFile,
104
105
  localPath
105
- } from "./chunk-6TWWY7ZU.js";
106
- import "./chunk-XUGSZP7Z.js";
107
- import "./chunk-EVBCK2KC.js";
108
- import "./chunk-JHGHLKFS.js";
109
- import "./chunk-3SF7D4DY.js";
106
+ } from "./chunk-Z3JXLULD.js";
107
+ import "./chunk-W45FZEGE.js";
108
+ import "./chunk-RARREDQE.js";
109
+ import "./chunk-WS6NU475.js";
110
+ import "./chunk-MFHS7IQ2.js";
110
111
  import "./chunk-A3WDWNN5.js";
111
112
  import {
112
113
  isLeadFormType
@@ -121,7 +122,7 @@ import "./chunk-LBYOLBW7.js";
121
122
  import "./chunk-XXTZBPUY.js";
122
123
  import "./chunk-6VAL7XJT.js";
123
124
  import "./chunk-NMAWXBJA.js";
124
- import "./chunk-C6B4RHXR.js";
125
+ import "./chunk-O3GT46R2.js";
125
126
 
126
127
  // src/recipes/index.ts
127
128
  var ALL_RECIPE_NAMES = [
@@ -324,6 +325,7 @@ var WATCH_CONDITION_OPTIONS = [
324
325
  "stale repo",
325
326
  "no custom domain"
326
327
  ];
328
+ var CLEAR_SECRET = "__clear__";
327
329
 
328
330
  // src/dashboard/render.ts
329
331
  var DASH = "\u2014";
@@ -615,8 +617,26 @@ var DETAIL_VALUE_FN = `function detailValue(el) {
615
617
  if (el.multiple) return Array.from(el.selectedOptions).map((o) => o.value).join(",");
616
618
  return el.value;
617
619
  }`;
620
+ var SAVE_DETAIL_FN = `function saveDetail(el, root) {
621
+ const span = root.querySelector('.detail-saved[data-for="' + el.dataset.detailField + '"]');
622
+ return fetch(el.dataset.detailsUrl, {
623
+ method: "POST",
624
+ headers: { "content-type": "application/json" },
625
+ body: JSON.stringify({ field: el.dataset.detailField, value: detailValue(el) }),
626
+ })
627
+ .then((r) => {
628
+ if (span) span.textContent = r.ok ? " \\u2713" : " \\u2717";
629
+ if (r.ok && "defaultValue" in el) el.defaultValue = el.value;
630
+ })
631
+ .catch(() => {
632
+ if (span) span.textContent = " \\u2717";
633
+ });
634
+ }`;
635
+ function toDateInputValue(value) {
636
+ return (value ?? "").slice(0, 10);
637
+ }
618
638
  function dateRow(label, field, value, url) {
619
- return `<div class="detail"><dt><label for="detail-${field}">${escapeHtml(label)}</label></dt><dd><input type="date" id="detail-${field}" data-detail-field="${field}" data-details-url="${url}" value="${escapeHtml(value ?? "")}" />${savedSpan(field)}</dd></div>`;
639
+ return `<div class="detail"><dt><label for="detail-${field}">${escapeHtml(label)}</label></dt><dd><input type="date" id="detail-${field}" data-detail-field="${field}" data-details-url="${url}" value="${escapeHtml(toDateInputValue(value))}" />${savedSpan(field)}</dd></div>`;
620
640
  }
621
641
  function checkboxRow(label, field, checked, url) {
622
642
  return `<div class="detail"><dt><label for="detail-${field}">${escapeHtml(label)}</label></dt><dd><input type="checkbox" id="detail-${field}" data-detail-field="${field}" data-details-url="${url}"${checked ? " checked" : ""} />${savedSpan(field)}</dd></div>`;
@@ -629,7 +649,7 @@ function multiSelectRow(label, field, options, selected, url) {
629
649
  return `<div class="detail wide"><dt><label for="detail-${field}">${escapeHtml(label)}</label></dt><dd><select multiple id="detail-${field}" data-detail-field="${field}" data-details-url="${url}">${opts}</select>${savedSpan(field)}</dd></div>`;
630
650
  }
631
651
  function secretRow(label, field, isSet, url) {
632
- const ph = isSet ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (set \u2014 type to replace)" : "not set";
652
+ const ph = isSet ? `\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (type to replace, or ${CLEAR_SECRET} to erase)` : "not set";
633
653
  return `<div class="detail"><dt><label for="detail-${field}">${escapeHtml(label)}</label></dt><dd><input type="password" autocomplete="off" id="detail-${field}" data-detail-field="${field}" data-details-url="${url}" placeholder="${escapeHtml(ph)}" />${savedSpan(field)}</dd></div>`;
634
654
  }
635
655
  function textareaRow(label, field, value, url) {
@@ -899,9 +919,18 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
899
919
  ${spamScreenSection(spamTotals, submissions, now)}
900
920
  ${submissionsSection(submissions, site)}
901
921
  <script>
922
+ // The SAME pending report is rendered twice \u2014 once in the pending list, once
923
+ // in the reports history \u2014 so one report id owns two Approve buttons. Every
924
+ // state change has to reach both, or the two places you look disagree: one
925
+ // says "Approved", the other still says "Approve" and (with the gate clear)
926
+ // is still enabled.
927
+ function approveButtonsFor(id) {
928
+ return Array.from(document.querySelectorAll('button.approve[data-report-id="' + id + '"]'));
929
+ }
902
930
  document.querySelectorAll("button.approve").forEach((b) => {
903
931
  b.addEventListener("click", async () => {
904
- b.disabled = true;
932
+ const twins = approveButtonsFor(b.dataset.reportId);
933
+ twins.forEach((t) => { t.disabled = true; });
905
934
  // is-loading swaps the label for a spinner; aria-busy is the same news for a
906
935
  // screen reader, which cannot see it. Both are cleared in the finally below so
907
936
  // no exit path can strand the button spinning forever.
@@ -910,30 +939,41 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
910
939
  try {
911
940
  const res = await fetch(b.dataset.approveUrl, { method: "POST" });
912
941
  if (res.ok) {
913
- b.textContent = "Approved";
914
- // Terminal success: darker green, and it stays disabled (approving twice is
915
- // a no-op server-side, but the button should not invite the second click).
916
- b.classList.add("is-approved");
942
+ // Terminal success: darker green, and they stay disabled (approving twice
943
+ // is a no-op server-side, but the button should not invite the second
944
+ // click). Applied to every twin, not just the one that was clicked.
945
+ twins.forEach((t) => {
946
+ t.textContent = "Approved";
947
+ t.classList.add("is-approved");
948
+ });
917
949
  } else {
918
950
  // A 409 carries { reason, blockers } \u2014 surface WHY instead of a bare
919
951
  // "Failed" next to a possibly-stale green chip. textContent/title
920
952
  // assignment only (no innerHTML), so server strings stay inert.
921
953
  const data = await res.json().catch(() => null);
922
- b.textContent = data && data.reason === "send-blocked" ? "Blocked" : "Failed";
954
+ const label = data && data.reason === "send-blocked" ? "Blocked" : "Failed";
923
955
  // Double-backslash, NOT single: this whole block lives inside the renderer's
924
956
  // template literal, so a single-backslash escape is consumed HERE at build
925
957
  // time and emits a real newline into the served HTML \u2014 an unterminated string
926
958
  // literal that kills the parse of the entire script element, taking every
927
959
  // handler on the page with it (exactly how approve/override/renovate went
928
960
  // dead). Note this comment must also avoid backticks for the same reason.
929
- if (data && Array.isArray(data.blockers)) b.title = data.blockers.join("\\n");
930
- b.disabled = false;
961
+ const title = data && Array.isArray(data.blockers) ? data.blockers.join("\\n") : null;
962
+ // Re-enable EVERY twin, not just the clicked one \u2014 leaving the other
963
+ // disabled would strand it reading "Approve" and unusable.
964
+ twins.forEach((t) => {
965
+ t.textContent = label;
966
+ if (title !== null) t.title = title;
967
+ t.disabled = false;
968
+ });
931
969
  }
932
970
  } catch {
933
971
  // Network rejection (offline, DNS, abort): mirror the !res.ok path so
934
- // the button doesn't sit permanently disabled reading "Approve".
935
- b.textContent = "Failed";
936
- b.disabled = false;
972
+ // the buttons don't sit permanently disabled reading "Approve".
973
+ twins.forEach((t) => {
974
+ t.textContent = "Failed";
975
+ t.disabled = false;
976
+ });
937
977
  } finally {
938
978
  b.classList.remove("is-loading");
939
979
  b.removeAttribute("aria-busy");
@@ -946,7 +986,12 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
946
986
  // refused client-side AND server-side).
947
987
  document.querySelectorAll("button.override-toggle").forEach((b) => {
948
988
  b.addEventListener("click", () => {
949
- const form = b.nextElementSibling;
989
+ // Scoped lookup, not nextElementSibling: adjacency is a markup accident,
990
+ // and anything inserted between the toggle and its form would kill "Send
991
+ // anyway\u2026" silently. The submit handler six lines down already uses
992
+ // closest(); this is the same contract from the other end.
993
+ const wrap = b.closest(".override");
994
+ const form = wrap ? wrap.querySelector(".override-form") : null;
950
995
  if (form) form.hidden = !form.hidden;
951
996
  });
952
997
  });
@@ -971,13 +1016,14 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
971
1016
  if (res.ok) {
972
1017
  if (status) status.textContent = "Overridden \u2713";
973
1018
  if (input) input.disabled = true;
974
- // Reflect the send-anyway state on the row's own Approve button too \u2014
975
- // it stays server-gated disabled, but the label should stop implying
976
- // "waiting on you" once an override has gone through.
977
- const approveBtn = document.querySelector(
978
- 'button.approve[data-report-id="' + b.dataset.reportId + '"]',
979
- );
980
- if (approveBtn) approveBtn.textContent = "Overridden";
1019
+ // Reflect the send-anyway state on the row's own Approve buttons too \u2014
1020
+ // they stay server-gated disabled, but the label should stop implying
1021
+ // "waiting on you" once an override has gone through. ALL of them: the
1022
+ // same report is rendered in both the pending list and the history, so
1023
+ // a singular querySelector updated the first and left the second lying.
1024
+ approveButtonsFor(b.dataset.reportId).forEach((t) => {
1025
+ t.textContent = "Overridden";
1026
+ });
981
1027
  } else {
982
1028
  // A 409 carries { reason, blockers } \u2014 surface WHY (textContent only,
983
1029
  // never innerHTML, so server strings stay inert).
@@ -1016,32 +1062,27 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
1016
1062
  // Site-details editor: save on change (selects) / blur (inputs+textareas, only
1017
1063
  // when the value actually changed). The per-field span shows \u2713 / \u2717.
1018
1064
  ${DETAIL_VALUE_FN}
1019
- function saveDetail(el) {
1020
- const span = document.querySelector('.detail-saved[data-for="' + el.dataset.detailField + '"]');
1021
- fetch(el.dataset.detailsUrl, {
1022
- method: "POST",
1023
- headers: { "content-type": "application/json" },
1024
- body: JSON.stringify({ field: el.dataset.detailField, value: detailValue(el) }),
1025
- })
1026
- .then((r) => {
1027
- if (span) span.textContent = r.ok ? " \u2713" : " \u2717";
1028
- })
1029
- .catch(() => {
1030
- if (span) span.textContent = " \u2717";
1031
- });
1032
- }
1065
+ ${SAVE_DETAIL_FN}
1033
1066
  document.querySelectorAll("select[data-detail-field]").forEach((s) => {
1034
- s.addEventListener("change", () => saveDetail(s));
1067
+ s.addEventListener("change", () => saveDetail(s, document));
1035
1068
  });
1036
1069
  // Checkboxes save on CHANGE. They cannot use the blur path below: that path
1037
1070
  // only fires when \`value !== defaultValue\`, and for a checkbox both are the
1038
1071
  // "on" content attribute, so it would never save at all.
1039
1072
  document.querySelectorAll("input[type=checkbox][data-detail-field]").forEach((c) => {
1040
- c.addEventListener("change", () => saveDetail(c));
1073
+ c.addEventListener("change", () => saveDetail(c, document));
1041
1074
  });
1042
1075
  document.querySelectorAll("input:not([type=checkbox])[data-detail-field], textarea[data-detail-field]").forEach((i) => {
1076
+ // A save needs BOTH a real edit gesture and a changed value. \`value !==
1077
+ // defaultValue\` alone is not evidence of an edit: the browser rewrites
1078
+ // \`.value\` on its own when a control is handed a value it cannot represent
1079
+ // (an ISO datetime in a date input sanitizes to ""), and the blur that
1080
+ // follows an untouched tab-through then posts that empty string as if the
1081
+ // operator had cleared the field. Requiring an \`input\` event first means
1082
+ // only a human keystroke or picker choice can arm the save.
1083
+ i.addEventListener("input", () => { i.dataset.edited = "1"; });
1043
1084
  i.addEventListener("blur", () => {
1044
- if (i.value !== i.defaultValue) saveDetail(i);
1085
+ if (i.dataset.edited === "1" && i.value !== i.defaultValue) saveDetail(i, document);
1045
1086
  });
1046
1087
  });
1047
1088
  // Refresh preview: dispatch the re-render workflow, then tell the operator to
@@ -1609,7 +1650,8 @@ function renderInboxLane(model) {
1609
1650
  }).join("");
1610
1651
  const overflow = subs.length - shown.length;
1611
1652
  const more = overflow > 0 ? `<div class="approve-row subm-more muted"><a href="/submissions">+${overflow} more \u2014 view all submissions</a></div>` : `<div class="approve-row subm-more muted"><a href="/submissions">View all submissions \u2192</a></div>`;
1612
- const spamLine = hasSpam ? `<div class="spam-rollup muted">\u{1F6E1} Spam (30d) \u2014 caught ${spam.caught} \xB7 through ${spam.through}</div>` : "";
1653
+ const spamAsOf = spam?.computedAt !== void 0 ? ` \xB7 as of ${escapeHtml(spam.computedAt.slice(0, 16).replace("T", " "))} UTC` : "";
1654
+ const spamLine = hasSpam ? `<div class="spam-rollup muted">\u{1F6E1} Spam (30d) \u2014 caught ${spam.caught} \xB7 through ${spam.through}${spamAsOf}</div>` : "";
1613
1655
  const spamInSummary = hasSpam ? " \xB7 \u{1F6E1} Spam (30d)" : "";
1614
1656
  const autoFilteredLine = hasAutoFiltered ? `<div class="spam-rollup muted">\u{1F6AB} ${autoFiltered} auto-filtered this week \u2014 <a href="/submissions?status=spam_auto">review</a></div>` : "";
1615
1657
  const leads = subs.filter((s) => isLeadFormType(s.formType)).length;
@@ -1634,41 +1676,38 @@ var AUDIT_SCRIPT = `<script>
1634
1676
  function rfStop(){ try { localStorage.removeItem(RF_KEY); } catch(e){} }
1635
1677
  // Per-workflow ETA hint (the lighthouse fleet sweep runs ~48 min; security ~1-2 min).
1636
1678
  function rfEta(label){ return label === 'lighthouse' ? '~48m' : label === 'security' ? '~2m' : ''; }
1637
- // Map a raw GitHub step name to a coarse human phase. Order matters (the audit step
1638
- // name also contains 'browser'; 'playwright install' also contains 'install').
1639
- function rfPhase(step){
1640
- if (!step) return '';
1641
- var s = step.toLowerCase();
1642
- if (s.indexOf('audit') !== -1 || s.indexOf('lighthouse') !== -1) return 'auditing the fleet\u2026';
1643
- if (s.indexOf('build') !== -1) return 'building\u2026';
1644
- if (s.indexOf('playwright') !== -1 || s.indexOf('browser') !== -1) return 'installing browsers\u2026';
1645
- if (s.indexOf('install') !== -1 || s.indexOf('depend') !== -1) return 'installing dependencies\u2026';
1646
- if (s.indexOf('set up') !== -1 || s.indexOf('checkout') !== -1) return 'setting up\u2026';
1647
- return step;
1679
+ // Escape before interpolating. The values here are server-side enums and a
1680
+ // GitHub html_url today, but this builds HTML from a REMOTE API response, and
1681
+ // the house rule everywhere else on these pages is that no server string ever
1682
+ // reaches the browser as markup. Cheaper to escape than to keep re-deciding.
1683
+ function rfEsc(v){
1684
+ return String(v == null ? '' : v)
1685
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
1686
+ .replace(/"/g,'&quot;').replace(/'/g,'&#39;');
1687
+ }
1688
+ // A run link must be a GitHub run URL. Anything else is dropped rather than
1689
+ // rendered, so a compromised or unexpected API response cannot place an
1690
+ // arbitrary href (javascript:, data:, an attacker's host) in the panel.
1691
+ function rfRunUrl(u){
1692
+ return typeof u === 'string' && u.indexOf('https://github.com/') === 0 ? rfEsc(u) : '';
1648
1693
  }
1649
- // Safe to build raw HTML: workflow/state/step are server-fixed (enums + GitHub step
1650
- // names) and url is GitHub's own html_url for our central repo \u2014 none are
1651
- // user-supplied. Don't interpolate untrusted fields here without escaping.
1652
1694
  function rfRender(status, startedAt){
1653
1695
  var failed = function(s){ return s === 'failure' || s === 'cancelled' || s === 'timed_out'; };
1654
1696
  var mins = Math.floor((Date.now() - startedAt) / 60000);
1655
1697
  var elapsed = mins < 1 ? '<1m' : mins + 'm';
1656
1698
  return status.perWorkflow.map(function(w){
1657
- var label = w.workflow.replace('.yml','').replace('fleet-','');
1699
+ var label = String(w.workflow || '').replace('.yml','').replace('fleet-','');
1658
1700
  var done = w.state === 'success';
1659
1701
  var isFailed = failed(w.state);
1660
1702
  var icon = done ? '\u2713' : isFailed ? '\u2717' : '<span class="rf-spin"></span>';
1661
- var line = icon + ' ' + label + ' \u2014 ' + w.state.replace('_',' ');
1703
+ var line = icon + ' ' + rfEsc(label) + ' \u2014 ' + rfEsc(String(w.state || '').replace('_',' '));
1704
+ var url = rfRunUrl(w.url);
1662
1705
  if (!done && !isFailed){
1663
- var phase = rfPhase(w.step);
1664
1706
  var eta = rfEta(label);
1665
- var detail = [];
1666
- if (phase) detail.push(phase);
1667
- detail.push(elapsed + (eta ? ' / ' + eta : ''));
1668
- var link = w.url ? ' \xB7 <a href="'+w.url+'" target="_blank" rel="noopener">view run \u2197</a>' : '';
1669
- line += '<div class="rf-sub">' + detail.join(' \xB7 ') + link + '</div>';
1670
- } else if (isFailed && w.url){
1671
- line += ' <a href="'+w.url+'" target="_blank" rel="noopener">run</a>';
1707
+ var link = url ? ' \xB7 <a href="'+url+'" target="_blank" rel="noopener">view run \u2197</a>' : '';
1708
+ line += '<div class="rf-sub">' + elapsed + (eta ? ' / ' + eta : '') + link + '</div>';
1709
+ } else if (isFailed && url){
1710
+ line += ' <a href="'+url+'" target="_blank" rel="noopener">run</a>';
1672
1711
  }
1673
1712
  return '<div class="rf-row">' + line + '</div>';
1674
1713
  }).join('');
@@ -1797,6 +1836,8 @@ var SESSION_TTL_SECONDS = 30 * 24 * 60 * 60;
1797
1836
 
1798
1837
  // src/dashboard/prospect-audit-trigger.ts
1799
1838
  var PROSPECT_AUDIT_DUPLICATE_WINDOW_MS = 10 * 60 * 1e3;
1839
+ var PROSPECT_AUDIT_DAILY_CAP = 25;
1840
+ var DAILY_CAP_LOOKBACK = PROSPECT_AUDIT_DAILY_CAP * 2;
1800
1841
  export {
1801
1842
  ALL_AUDIT_NAMES,
1802
1843
  ALL_RECIPE_NAMES,