@reddoorla/maintenance 0.69.0 → 0.71.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 (130) hide show
  1. package/dist/airtable-2PJ4NSAV.js +9 -0
  2. package/dist/{announce-72R3LRNT.js → announce-3JE5FKW7.js} +6 -6
  3. package/dist/blux-FKZYMKLF.js +2205 -0
  4. package/dist/blux-FKZYMKLF.js.map +1 -0
  5. package/dist/{bump-deps-QNM3H4GI.js → bump-deps-K3JMLMBB.js} +3 -2
  6. package/dist/bump-deps-K3JMLMBB.js.map +1 -0
  7. package/dist/{chunk-RINXPU3H.js → chunk-2PWQ6MPG.js} +2 -2
  8. package/dist/{chunk-2DCA7FHZ.js → chunk-52TDNN7K.js} +34 -12
  9. package/dist/chunk-52TDNN7K.js.map +1 -0
  10. package/dist/{chunk-XWPZ46IN.js → chunk-5QC5KJHR.js} +2 -2
  11. package/dist/{chunk-O6G4HHMF.js → chunk-6J25IIAI.js} +162 -42
  12. package/dist/chunk-6J25IIAI.js.map +1 -0
  13. package/dist/chunk-7MJBTV2R.js +145 -0
  14. package/dist/chunk-7MJBTV2R.js.map +1 -0
  15. package/dist/{chunk-6JFOAPW4.js → chunk-AYM2EFSK.js} +38 -6
  16. package/dist/chunk-AYM2EFSK.js.map +1 -0
  17. package/dist/{chunk-GKSXZIMX.js → chunk-CMQMNFFJ.js} +77 -3
  18. package/dist/chunk-CMQMNFFJ.js.map +1 -0
  19. package/dist/{chunk-6MT5W6UF.js → chunk-F6OE6MGQ.js} +6 -3
  20. package/dist/chunk-F6OE6MGQ.js.map +1 -0
  21. package/dist/{chunk-OTBA7DON.js → chunk-J2SHWT2O.js} +1 -1
  22. package/dist/chunk-J2SHWT2O.js.map +1 -0
  23. package/dist/{chunk-AESJBEM7.js → chunk-KOAGMTRI.js} +3 -3
  24. package/dist/chunk-KRDLHAKK.js +17 -0
  25. package/dist/chunk-KRDLHAKK.js.map +1 -0
  26. package/dist/{chunk-LYCKN7ZR.js → chunk-LPMQXS2N.js} +8 -4
  27. package/dist/chunk-LPMQXS2N.js.map +1 -0
  28. package/dist/{chunk-6G7BC5SN.js → chunk-M7HOQ4W7.js} +385 -5
  29. package/dist/chunk-M7HOQ4W7.js.map +1 -0
  30. package/dist/{chunk-42KKVLYN.js → chunk-MPRLMGJU.js} +10 -2
  31. package/dist/chunk-MPRLMGJU.js.map +1 -0
  32. package/dist/chunk-PKVZMFEB.js +315 -0
  33. package/dist/chunk-PKVZMFEB.js.map +1 -0
  34. package/dist/{chunk-A5KYRGQA.js → chunk-PSJQE6T6.js} +4 -4
  35. package/dist/chunk-SL24DCGC.js +41 -0
  36. package/dist/chunk-SL24DCGC.js.map +1 -0
  37. package/dist/{chunk-E2GKKUEX.js → chunk-VR246V3G.js} +25 -5
  38. package/dist/chunk-VR246V3G.js.map +1 -0
  39. package/dist/{chunk-45AUGDFR.js → chunk-WN4NEHE7.js} +2 -7
  40. package/dist/chunk-WN4NEHE7.js.map +1 -0
  41. package/dist/{chunk-YQMYQAJX.js → chunk-Y7LZHPDF.js} +2 -2
  42. package/dist/{chunk-C3PDS623.js → chunk-YWRQEDB3.js} +53 -3
  43. package/dist/chunk-YWRQEDB3.js.map +1 -0
  44. package/dist/cli/bin.js +63 -19
  45. package/dist/cli/bin.js.map +1 -1
  46. package/dist/cli/commands/audit.d.ts +1 -1
  47. package/dist/cli/commands/audit.js +11 -9
  48. package/dist/cli/commands/audit.js.map +1 -1
  49. package/dist/configs/playwright-a11y.js +1 -1
  50. package/dist/{convert-to-pnpm-XH7NYRVK.js → convert-to-pnpm-6F4RAFEJ.js} +3 -2
  51. package/dist/convert-to-pnpm-6F4RAFEJ.js.map +1 -0
  52. package/dist/{digest-Q2TAQHXF.js → digest-GZCZYJKC.js} +8 -8
  53. package/dist/{ensure-site-2PF522VL.js → ensure-site-H5VMBPLF.js} +2 -2
  54. package/dist/forms/index.d.ts +2 -0
  55. package/dist/forms/index.js +7 -1
  56. package/dist/forms/index.js.map +1 -1
  57. package/dist/{github-signals-CLQC2IPY.js → github-signals-SQECQNST.js} +5 -5
  58. package/dist/health-endpoint-QXWM6GW2.js +54 -0
  59. package/dist/health-endpoint-QXWM6GW2.js.map +1 -0
  60. package/dist/index.d.ts +132 -17
  61. package/dist/index.js +207 -64
  62. package/dist/index.js.map +1 -1
  63. package/dist/{init-UA6D7GKW.js → init-KQOIPLVT.js} +9 -5
  64. package/dist/init-KQOIPLVT.js.map +1 -0
  65. package/dist/{launch-AENAS2O2.js → launch-RX4VI7F2.js} +9 -9
  66. package/dist/{lighthouse-airtable-UBSXKHAQ.js → lighthouse-airtable-TTXULEGU.js} +3 -3
  67. package/dist/{onboard-X6KJ5Z2V.js → onboard-TFBL62J3.js} +2 -2
  68. package/dist/{orchestrate-GADNFGDH.js → orchestrate-DYA4XAQJ.js} +5 -5
  69. package/dist/{preflight-D5RQGX4V.js → preflight-S3GYME3Q.js} +7 -7
  70. package/dist/probe-DJAMLVTT.js +46 -0
  71. package/dist/probe-DJAMLVTT.js.map +1 -0
  72. package/dist/recipes/sync-configs.d.ts +1 -1
  73. package/dist/{renovate-dispatch-LG4RJGDY.js → renovate-dispatch-MZPAWVBL.js} +2 -2
  74. package/dist/{report-3FJ224H5.js → report-BLZ67QAQ.js} +12 -12
  75. package/dist/run-migration-LJNNZWIV.js +205 -0
  76. package/dist/run-migration-LJNNZWIV.js.map +1 -0
  77. package/dist/{self-updating-JTTFOJ7Q.js → self-updating-GSH427XP.js} +2 -2
  78. package/dist/{selftest-55OJIWEB.js → selftest-KPTL6KC4.js} +8 -8
  79. package/dist/smoke-suite-GP3W57JP.js +54 -0
  80. package/dist/smoke-suite-GP3W57JP.js.map +1 -0
  81. package/dist/{svelte-codemods-KJLNH6FQ.js → svelte-codemods-GYZZX742.js} +3 -2
  82. package/dist/svelte-codemods-GYZZX742.js.map +1 -0
  83. package/dist/{sync-configs-TIBO6DUJ.js → sync-configs-CQNOTJ2B.js} +3 -2
  84. package/dist/sync-configs-CQNOTJ2B.js.map +1 -0
  85. package/dist/{types-Dg4mHvyf.d.ts → types-0jreMCF5.d.ts} +2 -2
  86. package/dist/{upgrade-TPTE26FG.js → upgrade-RTAJZD56.js} +3 -2
  87. package/dist/upgrade-RTAJZD56.js.map +1 -0
  88. package/dist/{websites-6YTT5ZTO.js → websites-SVI4KYSH.js} +8 -2
  89. package/dist/{write-audits-to-airtable-IWOO4FFH.js → write-audits-to-airtable-3XFNWMUE.js} +4 -4
  90. package/package.json +4 -1
  91. package/dist/airtable-WGQQ3KAD.js +0 -9
  92. package/dist/bump-deps-QNM3H4GI.js.map +0 -1
  93. package/dist/chunk-2DCA7FHZ.js.map +0 -1
  94. package/dist/chunk-42KKVLYN.js.map +0 -1
  95. package/dist/chunk-45AUGDFR.js.map +0 -1
  96. package/dist/chunk-6G7BC5SN.js.map +0 -1
  97. package/dist/chunk-6JFOAPW4.js.map +0 -1
  98. package/dist/chunk-6MT5W6UF.js.map +0 -1
  99. package/dist/chunk-C3PDS623.js.map +0 -1
  100. package/dist/chunk-E2GKKUEX.js.map +0 -1
  101. package/dist/chunk-GKSXZIMX.js.map +0 -1
  102. package/dist/chunk-LYCKN7ZR.js.map +0 -1
  103. package/dist/chunk-O6G4HHMF.js.map +0 -1
  104. package/dist/chunk-OTBA7DON.js.map +0 -1
  105. package/dist/convert-to-pnpm-XH7NYRVK.js.map +0 -1
  106. package/dist/init-UA6D7GKW.js.map +0 -1
  107. package/dist/svelte-codemods-KJLNH6FQ.js.map +0 -1
  108. package/dist/sync-configs-TIBO6DUJ.js.map +0 -1
  109. package/dist/upgrade-TPTE26FG.js.map +0 -1
  110. /package/dist/{airtable-WGQQ3KAD.js.map → airtable-2PJ4NSAV.js.map} +0 -0
  111. /package/dist/{announce-72R3LRNT.js.map → announce-3JE5FKW7.js.map} +0 -0
  112. /package/dist/{chunk-RINXPU3H.js.map → chunk-2PWQ6MPG.js.map} +0 -0
  113. /package/dist/{chunk-XWPZ46IN.js.map → chunk-5QC5KJHR.js.map} +0 -0
  114. /package/dist/{chunk-AESJBEM7.js.map → chunk-KOAGMTRI.js.map} +0 -0
  115. /package/dist/{chunk-A5KYRGQA.js.map → chunk-PSJQE6T6.js.map} +0 -0
  116. /package/dist/{chunk-YQMYQAJX.js.map → chunk-Y7LZHPDF.js.map} +0 -0
  117. /package/dist/{digest-Q2TAQHXF.js.map → digest-GZCZYJKC.js.map} +0 -0
  118. /package/dist/{ensure-site-2PF522VL.js.map → ensure-site-H5VMBPLF.js.map} +0 -0
  119. /package/dist/{github-signals-CLQC2IPY.js.map → github-signals-SQECQNST.js.map} +0 -0
  120. /package/dist/{launch-AENAS2O2.js.map → launch-RX4VI7F2.js.map} +0 -0
  121. /package/dist/{lighthouse-airtable-UBSXKHAQ.js.map → lighthouse-airtable-TTXULEGU.js.map} +0 -0
  122. /package/dist/{onboard-X6KJ5Z2V.js.map → onboard-TFBL62J3.js.map} +0 -0
  123. /package/dist/{orchestrate-GADNFGDH.js.map → orchestrate-DYA4XAQJ.js.map} +0 -0
  124. /package/dist/{preflight-D5RQGX4V.js.map → preflight-S3GYME3Q.js.map} +0 -0
  125. /package/dist/{renovate-dispatch-LG4RJGDY.js.map → renovate-dispatch-MZPAWVBL.js.map} +0 -0
  126. /package/dist/{report-3FJ224H5.js.map → report-BLZ67QAQ.js.map} +0 -0
  127. /package/dist/{self-updating-JTTFOJ7Q.js.map → self-updating-GSH427XP.js.map} +0 -0
  128. /package/dist/{selftest-55OJIWEB.js.map → selftest-KPTL6KC4.js.map} +0 -0
  129. /package/dist/{websites-6YTT5ZTO.js.map → websites-SVI4KYSH.js.map} +0 -0
  130. /package/dist/{write-audits-to-airtable-IWOO4FFH.js.map → write-audits-to-airtable-3XFNWMUE.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,10 +1,14 @@
1
- import "./chunk-AESJBEM7.js";
1
+ import "./chunk-KOAGMTRI.js";
2
2
  import {
3
3
  approveBlockers
4
- } from "./chunk-E2GKKUEX.js";
4
+ } from "./chunk-VR246V3G.js";
5
+ import {
6
+ draftReportForSite
7
+ } from "./chunk-6J25IIAI.js";
8
+ import "./chunk-2PWQ6MPG.js";
5
9
  import {
6
10
  findDueReports
7
- } from "./chunk-45AUGDFR.js";
11
+ } from "./chunk-WN4NEHE7.js";
8
12
  import {
9
13
  upgradeSvelte4to5
10
14
  } from "./chunk-ZO7WNY57.js";
@@ -12,7 +16,7 @@ import {
12
16
  DEFAULT_INIT_STEPS,
13
17
  a11yFixturesPage,
14
18
  init
15
- } from "./chunk-42KKVLYN.js";
19
+ } from "./chunk-MPRLMGJU.js";
16
20
  import {
17
21
  convertToPnpm
18
22
  } from "./chunk-2X322ZES.js";
@@ -20,32 +24,32 @@ import {
20
24
  svelteCodemods
21
25
  } from "./chunk-BTEBRTW4.js";
22
26
  import "./chunk-J7SZQUCW.js";
27
+ import "./chunk-7MJBTV2R.js";
28
+ import "./chunk-PKVZMFEB.js";
29
+ import "./chunk-KRDLHAKK.js";
23
30
  import {
24
31
  onboard,
25
32
  selfCaretRange,
26
33
  selfPackageVersion
27
34
  } from "./chunk-ZYBOT5PM.js";
28
- import {
29
- draftReportForSite
30
- } from "./chunk-O6G4HHMF.js";
31
- import "./chunk-RINXPU3H.js";
32
35
  import {
33
36
  parseAddresses,
34
37
  sendApprovedReports,
35
38
  withGlobalCc
36
- } from "./chunk-2DCA7FHZ.js";
39
+ } from "./chunk-52TDNN7K.js";
37
40
  import "./chunk-GECESMVS.js";
38
41
  import {
39
42
  BLURRED_CID,
40
43
  CHECK_CID,
41
44
  checklistFor,
42
45
  escapeHtml,
43
- isChecklistComplete,
46
+ gatingFields,
47
+ isHealthGateClear,
44
48
  isPendingApproval,
45
49
  loadBundledImages,
46
50
  renderReportHtml,
47
51
  safeUrl
48
- } from "./chunk-6JFOAPW4.js";
52
+ } from "./chunk-AYM2EFSK.js";
49
53
  import "./chunk-WEKL2HYJ.js";
50
54
  import {
51
55
  bumpDeps
@@ -66,11 +70,11 @@ import {
66
70
  runAudits,
67
71
  runAuditsAcross,
68
72
  securityAudit
69
- } from "./chunk-6G7BC5SN.js";
73
+ } from "./chunk-M7HOQ4W7.js";
70
74
  import "./chunk-GASBX52O.js";
71
75
  import "./chunk-YEGHUODU.js";
72
76
  import "./chunk-LKGVSM2O.js";
73
- import "./chunk-OTBA7DON.js";
77
+ import "./chunk-J2SHWT2O.js";
74
78
  import "./chunk-PH3AGF4Z.js";
75
79
  import "./chunk-HIN3P4ZN.js";
76
80
  import {
@@ -82,16 +86,16 @@ import "./chunk-XXTZBPUY.js";
82
86
  import "./chunk-XTK5VIQB.js";
83
87
  import {
84
88
  fromAirtableBase
85
- } from "./chunk-6MT5W6UF.js";
89
+ } from "./chunk-F6OE6MGQ.js";
86
90
  import "./chunk-U5SZ4FOP.js";
87
91
  import {
88
92
  SEVERITY_RANK,
89
93
  siteSlug
90
- } from "./chunk-C3PDS623.js";
94
+ } from "./chunk-YWRQEDB3.js";
91
95
  import "./chunk-HN6I2XWI.js";
92
96
  import "./chunk-6VAL7XJT.js";
93
97
  import "./chunk-EYODPUHV.js";
94
- import "./chunk-LYCKN7ZR.js";
98
+ import "./chunk-LPMQXS2N.js";
95
99
 
96
100
  // src/recipes/index.ts
97
101
  var ALL_RECIPE_NAMES = [
@@ -102,6 +106,8 @@ var ALL_RECIPE_NAMES = [
102
106
  "convert-to-pnpm",
103
107
  "onboard",
104
108
  "a11y-fixtures-page",
109
+ "health-endpoint",
110
+ "smoke-suite",
105
111
  "self-updating",
106
112
  "init"
107
113
  ];
@@ -316,24 +322,59 @@ function securitySection(site) {
316
322
  <ul class="vuln-list">${sorted.map(advisoryRow).join("")}</ul>
317
323
  </div>`;
318
324
  }
325
+ function healthPresentation(status) {
326
+ switch (status) {
327
+ case "pass":
328
+ return { cls: "healthy", word: "clear" };
329
+ case "fail":
330
+ return { cls: "attention", word: "blocks" };
331
+ case "n/a":
332
+ return { cls: "na", word: "n/a" };
333
+ default:
334
+ return { cls: "watch", word: "needs you" };
335
+ }
336
+ }
319
337
  function checklistBlock(r) {
320
338
  const items = checklistFor(r.reportType);
321
339
  if (items.length === 0) return "";
340
+ const gating = new Set(gatingFields(r.reportType));
322
341
  const rid = escapeHtml(r.id);
323
- const url = `/api/reports/${encodeURIComponent(r.id)}/checklist`;
324
- const boxes = items.map((item) => {
325
- const checked = r.checklist[item.field] === true ? " checked" : "";
342
+ const rows = items.map((item) => {
326
343
  const ev = r.autoEvidence?.[item.field];
327
- const badge = ev ? ev.result === "pass" ? ` <span class="auto-badge auto-pass" title="${escapeHtml(ev.note)}">auto \u2713</span>` : ` <span class="auto-badge auto-amber" title="${escapeHtml(ev.note)}">auto: ${escapeHtml(ev.note)}</span>` : "";
328
- return `<label class="check-item"><input type="checkbox" class="checklist-checkbox" data-checklist-report-id="${rid}" data-field="${escapeHtml(item.field)}" data-checklist-url="${escapeHtml(url)}"${checked} /> ${escapeHtml(item.label)}${badge}</label>`;
344
+ const status = ev?.result ?? "unknown";
345
+ const { cls, word } = healthPresentation(status);
346
+ const advisory = gating.has(item.field) ? "" : ` <span class="advisory">advisory \u2014 never blocks</span>`;
347
+ const note = ev?.note ? ` <span class="check-note">${escapeHtml(ev.note)}</span>` : "";
348
+ return `<li class="check-item"><span class="pill ${cls}" title="${escapeHtml(ev?.note ?? "not yet measured")}">${word}</span> ${escapeHtml(item.label)}${advisory}${note}</li>`;
329
349
  }).join("");
330
- return `<div class="checklist" data-checklist-for="${rid}">${boxes}</div>`;
350
+ return `<ul class="checklist" data-checklist-for="${rid}">${rows}</ul>`;
331
351
  }
332
352
  function approveButton(r, blocked) {
333
- const disabled = isChecklistComplete(r) && !blocked ? "" : " disabled";
353
+ const gateClear = isHealthGateClear({
354
+ reportType: r.reportType,
355
+ autoEvidence: r.autoEvidence ?? {}
356
+ });
357
+ const disabled = gateClear && !blocked ? "" : " disabled";
334
358
  const blockedAttr = blocked ? ` data-send-blocked="1"` : "";
335
359
  return `<button class="approve" data-report-id="${escapeHtml(r.id)}" data-approve-url="${escapeHtml(`/api/reports/${encodeURIComponent(r.id)}/approve`)}"${blockedAttr}${disabled}>Approve</button>`;
336
360
  }
361
+ function overrideControl(r) {
362
+ const gateClear = isHealthGateClear({
363
+ reportType: r.reportType,
364
+ autoEvidence: r.autoEvidence ?? {}
365
+ });
366
+ if (gateClear) return "";
367
+ const rid = escapeHtml(r.id);
368
+ const overrideUrl = escapeHtml(`/api/reports/${encodeURIComponent(r.id)}/approve?override=1`);
369
+ return `<div class="override" data-override-for="${rid}">
370
+ <button type="button" class="override-toggle" data-report-id="${rid}">Send anyway\u2026</button>
371
+ <div class="override-form" hidden>
372
+ <input type="text" class="override-reason" placeholder="Reason for overriding the health gate (required)" />
373
+ <button type="button" class="override-submit" data-report-id="${rid}" data-override-url="${overrideUrl}">Confirm override</button>
374
+ <span class="override-status"></span>
375
+ </div>
376
+ </div>`;
377
+ }
337
378
  function preflightChip(findings) {
338
379
  const fails = findings.filter((f) => f.level === "fail");
339
380
  const warns = findings.filter((f) => f.level === "warn");
@@ -381,7 +422,7 @@ function pendingRow(r, site, now) {
381
422
  const blocked = findings.some((f) => f.level === "fail");
382
423
  const preview = r.renderedHtmlAttachment ? `<a href="${escapeHtml(safeUrl(r.renderedHtmlAttachment.url))}" rel="noopener noreferrer" title="rendered at draft time \u2014 Commentary/subject edits after drafting are not reflected">draft preview \u25B8</a>` : `<span class="muted">no preview yet</span>`;
383
424
  const sendLine = sendTimingLine(now);
384
- return `<li><div class="pending-head"><strong>${type}</strong> <span class="muted">${period}</span> ${preflightChip(findings)} ${preview} ${approveButton(r, blocked)}</div><div class="pending-info">${recipientsLine(site)} ${sendLine}</div>${checklistBlock(r)}</li>`;
425
+ return `<li><div class="pending-head"><strong>${type}</strong> <span class="muted">${period}</span> ${preflightChip(findings)} ${preview} ${approveButton(r, blocked)}</div><div class="pending-info">${recipientsLine(site)} ${sendLine}</div>${checklistBlock(r)}${overrideControl(r)}</li>`;
385
426
  }
386
427
  function pendingSection(reports, site, now) {
387
428
  const pending = reports.filter(isPendingApproval);
@@ -531,13 +572,73 @@ button.approve:disabled { opacity: 0.6; cursor: default; }
531
572
  .pending-list li { padding: 0.5rem; border-bottom: 1px solid #eee; }
532
573
  @media (prefers-color-scheme: dark) { .pending-list li { border-color: #2a2a2a; } }
533
574
  .pending-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; row-gap: 0.25rem; }
534
- .checklist { display: flex; flex-wrap: wrap; gap: 0.25rem 1.25rem; margin: 0.5rem 0 0.25rem 0.25rem; }
535
- .check-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; }
536
- .check-item input { margin: 0; }
537
- .auto-badge { font-size: 0.72rem; border-radius: 0.25rem; padding: 0 0.35rem; white-space: nowrap; }
538
- .auto-pass { background: #e6f4ea; color: #137333; }
539
- .auto-amber { background: #fef7e0; color: #b06000; }
575
+ .checklist {
576
+ list-style: none;
577
+ padding: 0;
578
+ margin: 0.5rem 0 0.25rem 0.25rem;
579
+ display: flex;
580
+ flex-direction: column;
581
+ gap: 0.3rem;
582
+ }
583
+ .check-item {
584
+ display: flex;
585
+ align-items: center;
586
+ gap: 0.5rem;
587
+ font-size: 0.9rem;
588
+ }
589
+ .pill.healthy {
590
+ background: #e8f5e9;
591
+ color: #1b7a2f;
592
+ }
593
+ .pill.watch {
594
+ background: #fff4e5;
595
+ color: #a65a00;
596
+ }
597
+ .pill.attention {
598
+ background: #fdecea;
599
+ color: #b00;
600
+ }
601
+ .pill.na {
602
+ background: #f0f0f0;
603
+ color: #666;
604
+ }
605
+ .advisory {
606
+ font-size: 0.72rem;
607
+ color: #999;
608
+ font-style: italic;
609
+ }
610
+ .check-note {
611
+ font-size: 0.75rem;
612
+ color: #999;
613
+ }
614
+ @media (prefers-color-scheme: dark) {
615
+ .pill.healthy {
616
+ background: #10240f;
617
+ color: #7fce85;
618
+ }
619
+ .pill.watch {
620
+ background: #2a2410;
621
+ color: #ffd454;
622
+ }
623
+ .pill.attention {
624
+ background: #2a0f0d;
625
+ color: #ff8a80;
626
+ }
627
+ .pill.na {
628
+ background: #222;
629
+ color: #999;
630
+ }
631
+ }
540
632
  .pill { font-size: 0.75rem; padding: 0.1rem 0.5rem; border-radius: 999px; font-weight: 700; }
633
+ .override { margin: 0.4rem 0 0.15rem 0.25rem; }
634
+ button.override-toggle { font: inherit; font-size: 0.78rem; padding: 0.15rem 0.6rem; border: 1px solid #b00; border-radius: 6px; background: transparent; color: #b00; cursor: pointer; }
635
+ @media (prefers-color-scheme: dark) { button.override-toggle { border-color: #ff8a80; color: #ff8a80; } }
636
+ .override-form { margin-top: 0.35rem; display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
637
+ .override-reason { font: inherit; font-size: 0.85rem; padding: 0.25rem 0.45rem; border: 1px solid #ccc; border-radius: 4px; background: transparent; color: inherit; min-width: 220px; flex: 1 1 220px; }
638
+ @media (prefers-color-scheme: dark) { .override-reason { border-color: #444; } }
639
+ button.override-submit { font: inherit; font-size: 0.8rem; padding: 0.25rem 0.7rem; border: 1px solid #b00; border-radius: 6px; background: #b00; color: #fff; cursor: pointer; }
640
+ button.override-submit:disabled { opacity: 0.6; cursor: default; }
641
+ .override-status { font-size: 0.78rem; color: #999; }
541
642
  .vuln-list { list-style: none; padding: 0; margin: 0; }
542
643
  .vuln-item { padding: 0.45rem 0; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline; }
543
644
  @media (prefers-color-scheme: dark) { .vuln-item { border-color: #2a2a2a; } }
@@ -645,6 +746,64 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
645
746
  }
646
747
  });
647
748
  });
749
+ // Health-gate override affordance: the toggle reveals a required-reason field; the
750
+ // submit POSTs the server-provided override URL with { reason } as the body \u2014 a
751
+ // distinct, deliberate action, never a silent default (an empty reason is
752
+ // refused client-side AND server-side).
753
+ document.querySelectorAll("button.override-toggle").forEach((b) => {
754
+ b.addEventListener("click", () => {
755
+ const form = b.nextElementSibling;
756
+ if (form) form.hidden = !form.hidden;
757
+ });
758
+ });
759
+ document.querySelectorAll("button.override-submit").forEach((b) => {
760
+ b.addEventListener("click", async () => {
761
+ const wrap = b.closest(".override-form");
762
+ const input = wrap ? wrap.querySelector(".override-reason") : null;
763
+ const status = wrap ? wrap.querySelector(".override-status") : null;
764
+ const reason = input ? input.value.trim() : "";
765
+ if (!reason) {
766
+ if (status) status.textContent = "Reason required";
767
+ return;
768
+ }
769
+ b.disabled = true;
770
+ if (status) status.textContent = "Sending\u2026";
771
+ try {
772
+ const res = await fetch(b.dataset.overrideUrl, {
773
+ method: "POST",
774
+ headers: { "content-type": "application/json" },
775
+ body: JSON.stringify({ reason }),
776
+ });
777
+ if (res.ok) {
778
+ if (status) status.textContent = "Overridden \u2713";
779
+ if (input) input.disabled = true;
780
+ // Reflect the send-anyway state on the row's own Approve button too \u2014
781
+ // it stays server-gated disabled, but the label should stop implying
782
+ // "waiting on you" once an override has gone through.
783
+ const approveBtn = document.querySelector(
784
+ 'button.approve[data-report-id="' + b.dataset.reportId + '"]',
785
+ );
786
+ if (approveBtn) approveBtn.textContent = "Overridden";
787
+ } else {
788
+ // A 409 carries { reason, blockers } \u2014 surface WHY (textContent only,
789
+ // never innerHTML, so server strings stay inert).
790
+ const data = await res.json().catch(() => null);
791
+ if (status) {
792
+ status.textContent =
793
+ data && Array.isArray(data.blockers) && data.blockers.length > 0
794
+ ? data.blockers.join("; ")
795
+ : data && data.reason === "override-reason-required"
796
+ ? "Reason required"
797
+ : "Failed";
798
+ }
799
+ b.disabled = false;
800
+ }
801
+ } catch {
802
+ if (status) status.textContent = "Failed";
803
+ b.disabled = false;
804
+ }
805
+ });
806
+ });
648
807
  // Trigger-renovate button: async on-demand dispatch (mirrors the cockpit).
649
808
  document.querySelectorAll("button.trigger-renovate").forEach((b) => {
650
809
  b.addEventListener("click", async () => {
@@ -685,34 +844,6 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
685
844
  });
686
845
  });
687
846
  ${SUBMISSION_STATUS_SCRIPT}
688
- // Checklist gate: ticking a box POSTs the one field; the response { complete }
689
- // decides whether THIS report's Approve button is enabled. Scoped per report by
690
- // matching the checkbox's report id to the Approve button's id, so multiple
691
- // pending reports on one page never cross-toggle. On failure the checkbox reverts.
692
- document.querySelectorAll("input.checklist-checkbox").forEach((cb) => {
693
- cb.addEventListener("change", async () => {
694
- const reportId = cb.dataset.checklistReportId;
695
- const approveBtn = document.querySelector(
696
- 'button.approve[data-report-id="' + (window.CSS && CSS.escape ? CSS.escape(reportId) : reportId) + '"]',
697
- );
698
- cb.disabled = true;
699
- try {
700
- const res = await fetch(cb.dataset.checklistUrl, {
701
- method: "POST",
702
- headers: { "content-type": "application/json" },
703
- body: JSON.stringify({ reportId, field: cb.dataset.field, value: cb.checked }),
704
- });
705
- if (!res.ok) throw new Error("bad status");
706
- const data = await res.json();
707
- if (approveBtn) approveBtn.disabled = !data.complete || approveBtn.dataset.sendBlocked === "1";
708
- } catch {
709
- // Revert the optimistic flip so the box reflects the (unchanged) server state.
710
- cb.checked = !cb.checked;
711
- } finally {
712
- cb.disabled = false;
713
- }
714
- });
715
- });
716
847
  </script>
717
848
  </body>
718
849
  </html>`;
@@ -896,7 +1027,12 @@ function submBadge(c) {
896
1027
  const n = c.newSubmissions ?? 0;
897
1028
  return n > 0 ? `<span class="chip">\u{1F4E5} ${n} new</span>` : "";
898
1029
  }
899
- var PILL_LABEL = { attention: "failing", watch: "watch", healthy: "ok" };
1030
+ var PILL_LABEL = {
1031
+ attention: "failing",
1032
+ watch: "watch",
1033
+ healthy: "ok",
1034
+ "pre-launch": "pre-launch"
1035
+ };
900
1036
  function attentionBadge(status) {
901
1037
  if (status === "new") return `<span class="badge">NEW</span>`;
902
1038
  if (status === "worse") return `<span class="badge">WORSE</span>`;
@@ -1037,6 +1173,8 @@ details.fleet-browse > summary, details.inbox > summary { cursor:pointer; font-w
1037
1173
  .pill.attention { background:#fdecea; color:#b00; }
1038
1174
  .pill.watch { background:#fff4e5; color:#a65a00; }
1039
1175
  .pill.healthy { background:#e8f5e9; color:#1b7a2f; }
1176
+ .pill.pre-launch { background:#eef1f5; color:#5b6570; }
1177
+ @media (prefers-color-scheme: dark) { .pill.pre-launch { background:#1c2530; color:#9aa7b4; } }
1040
1178
  .chips { display:flex; flex-wrap:wrap; gap:0.4rem; margin-top:0.5rem; }
1041
1179
  .chip { font-size:0.8rem; padding:0.1rem 0.5rem; border-radius:6px; background:#f0f0f0; }
1042
1180
  @media (prefers-color-scheme: dark) { .chip { background:#222; } }
@@ -1088,7 +1226,9 @@ function verdictBar(model, feed) {
1088
1226
  const auditedTerm = auditedIso ? `fleet last audited ${escapeHtml(relativeTimeFromNow(auditedIso))}` : null;
1089
1227
  const total = model.cards.length;
1090
1228
  const { broken, watch, approval } = needsYouCounts(feed);
1091
- const healthy = Math.max(0, total - (broken + watch + approval));
1229
+ const preLaunch = model.summary.preLaunch;
1230
+ const preLaunchTerm = preLaunch > 0 ? `${preLaunch} pre-launch` : null;
1231
+ const healthy = Math.max(0, total - (broken + watch + approval + preLaunch));
1092
1232
  const actions = `<div class="fleet-actions">
1093
1233
  <button type="button" class="refresh-fleet" data-refresh-url="/api/fleet/refresh">\u21BB Audit fleet</button>
1094
1234
  <div id="rf-status" class="rf-status" aria-live="polite"></div>
@@ -1099,8 +1239,8 @@ function verdictBar(model, feed) {
1099
1239
  ${actions}
1100
1240
  </div>`;
1101
1241
  if (broken === 0 && watch === 0 && approval === 0) {
1102
- const sitesWord = `${total} site${total === 1 ? "" : "s"}`;
1103
- return render("ok", "\u2713 All clear", [`${sitesWord} healthy`, auditedTerm]);
1242
+ const healthyWord = healthy > 0 ? `${healthy} site${healthy === 1 ? "" : "s"} healthy` : null;
1243
+ return render("ok", "\u2713 All clear", [healthyWord, preLaunchTerm, auditedTerm]);
1104
1244
  }
1105
1245
  const watchTerm = watch > 0 ? `${watch} watching` : null;
1106
1246
  const approvalTerm = approval > 0 ? `${approval} waiting on you` : null;
@@ -1110,6 +1250,7 @@ function verdictBar(model, feed) {
1110
1250
  watchTerm,
1111
1251
  approvalTerm,
1112
1252
  healthyTerm,
1253
+ preLaunchTerm,
1113
1254
  auditedTerm
1114
1255
  ]);
1115
1256
  }
@@ -1117,10 +1258,11 @@ function verdictBar(model, feed) {
1117
1258
  return render("watch", `${watch} site${watch === 1 ? "" : "s"} to watch`, [
1118
1259
  approvalTerm,
1119
1260
  healthyTerm,
1261
+ preLaunchTerm,
1120
1262
  auditedTerm
1121
1263
  ]);
1122
1264
  }
1123
- return render("soft", `${approval} waiting on you`, [healthyTerm, auditedTerm]);
1265
+ return render("soft", `${approval} waiting on you`, [healthyTerm, preLaunchTerm, auditedTerm]);
1124
1266
  }
1125
1267
  var NEEDS_YOU_GROUP_LABEL = {
1126
1268
  broken: "Broken",
@@ -1151,7 +1293,8 @@ var RECENT_ICON = {
1151
1293
  ci_recovered: "\u2705",
1152
1294
  site_launched: "\u{1F680}",
1153
1295
  cert_renewed: "\u{1F512}",
1154
- fleet_swept: "\u{1F504}"
1296
+ fleet_swept: "\u{1F504}",
1297
+ report_sent_with_override: "\u2733\uFE0F"
1155
1298
  };
1156
1299
  function renderRecentlyLane(model) {
1157
1300
  const events = model.recent ?? [];