@reddoorla/maintenance 0.72.0 → 0.75.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 (106) hide show
  1. package/dist/airtable-6L3JD7FY.js +9 -0
  2. package/dist/{announce-HJCGEVJA.js → announce-V6EYPPUR.js} +4 -4
  3. package/dist/{blux-FKZYMKLF.js → blux-C24OVDDW.js} +1044 -69
  4. package/dist/blux-C24OVDDW.js.map +1 -0
  5. package/dist/{bump-deps-GZEJAUKI.js → bump-deps-OQR3IXLN.js} +2 -2
  6. package/dist/chunk-2OP4JKIQ.js +19 -0
  7. package/dist/chunk-2OP4JKIQ.js.map +1 -0
  8. package/dist/{chunk-BJCA7O4A.js → chunk-3PU2QWOJ.js} +2 -2
  9. package/dist/{chunk-GI4M2AMO.js → chunk-4BHNP6Y2.js} +2 -2
  10. package/dist/{chunk-KRHPS3R4.js → chunk-6OYICQSJ.js} +47 -3
  11. package/dist/chunk-6OYICQSJ.js.map +1 -0
  12. package/dist/{chunk-BWKK2ZXS.js → chunk-GXVX4HUH.js} +32 -2
  13. package/dist/chunk-GXVX4HUH.js.map +1 -0
  14. package/dist/{chunk-KLB2OLPW.js → chunk-HBFMTM2E.js} +3 -3
  15. package/dist/{chunk-SQJ53Z7R.js → chunk-I55YWV6T.js} +3 -3
  16. package/dist/{chunk-7P7YETHG.js → chunk-IDBMYDVH.js} +2 -2
  17. package/dist/{chunk-JGLAWXUC.js → chunk-JR67FZVC.js} +7 -3
  18. package/dist/chunk-JR67FZVC.js.map +1 -0
  19. package/dist/{chunk-APTAITXZ.js → chunk-KKL3RYFQ.js} +2 -2
  20. package/dist/{chunk-NRISG4XD.js → chunk-NUIBQPB3.js} +42 -11
  21. package/dist/chunk-NUIBQPB3.js.map +1 -0
  22. package/dist/{chunk-BHE5CUWI.js → chunk-PERP7LSS.js} +269 -18
  23. package/dist/chunk-PERP7LSS.js.map +1 -0
  24. package/dist/chunk-RHPDQW6D.js +29 -0
  25. package/dist/chunk-RHPDQW6D.js.map +1 -0
  26. package/dist/{chunk-HR5SONJC.js → chunk-SK44IFZN.js} +2 -2
  27. package/dist/cli/bin.js +31 -19
  28. package/dist/cli/bin.js.map +1 -1
  29. package/dist/cli/commands/audit.js +6 -6
  30. package/dist/{convert-to-pnpm-SVFDLXZS.js → convert-to-pnpm-OVMASATZ.js} +2 -2
  31. package/dist/{digest-VX7Q4MNF.js → digest-LEMZS5ZK.js} +99 -7
  32. package/dist/digest-LEMZS5ZK.js.map +1 -0
  33. package/dist/{ensure-site-O7G5ZJS7.js → ensure-site-3SOAWTZF.js} +2 -2
  34. package/dist/forms/index.js +1 -1
  35. package/dist/{github-signals-QVW2LB7N.js → github-signals-TF3VSNIN.js} +4 -4
  36. package/dist/{health-endpoint-IYR5VBUT.js → health-endpoint-JZGYCMTV.js} +2 -2
  37. package/dist/index.d.ts +104 -19
  38. package/dist/index.js +122 -30
  39. package/dist/index.js.map +1 -1
  40. package/dist/{init-CU6PVZRG.js → init-7PTP4O2K.js} +4 -4
  41. package/dist/{launch-JWXCTJRG.js → launch-W33FWWOZ.js} +6 -6
  42. package/dist/{lighthouse-airtable-J5TLASJC.js → lighthouse-airtable-ZXNMDEPE.js} +3 -3
  43. package/dist/{onboard-6UQE5QIO.js → onboard-PX36YDZA.js} +2 -2
  44. package/dist/{orchestrate-ZDH24Z6Y.js → orchestrate-NYRYEKQM.js} +3 -3
  45. package/dist/{preflight-BNTMWTPG.js → preflight-JCQFKB6M.js} +4 -4
  46. package/dist/{renovate-dispatch-C3ALLACX.js → renovate-dispatch-6II2JVOX.js} +29 -27
  47. package/dist/renovate-dispatch-6II2JVOX.js.map +1 -0
  48. package/dist/{report-YLFSGNWR.js → report-POHQEV6F.js} +21 -5
  49. package/dist/report-POHQEV6F.js.map +1 -0
  50. package/dist/screenouts-7P5GOZMV.js +57 -0
  51. package/dist/screenouts-7P5GOZMV.js.map +1 -0
  52. package/dist/{self-updating-64FNRQB5.js → self-updating-2YSVNMPP.js} +2 -2
  53. package/dist/{selftest-7DPNGL6R.js → selftest-OO2WDSDM.js} +5 -5
  54. package/dist/{smoke-suite-TWILWDSF.js → smoke-suite-CSP23NZM.js} +2 -2
  55. package/dist/spam-classifier-VG4ICIWS.js +217 -0
  56. package/dist/spam-classifier-VG4ICIWS.js.map +1 -0
  57. package/dist/submissions-BNRGY3JQ.js +122 -0
  58. package/dist/submissions-BNRGY3JQ.js.map +1 -0
  59. package/dist/submissions-MDKWJYMM.js +281 -0
  60. package/dist/submissions-MDKWJYMM.js.map +1 -0
  61. package/dist/{svelte-codemods-RVDTTXFD.js → svelte-codemods-V54KOBWU.js} +2 -2
  62. package/dist/{sync-configs-BUUD6L5Q.js → sync-configs-UBCJRASZ.js} +2 -2
  63. package/dist/{upgrade-HHKWYJRN.js → upgrade-WR6ICPAG.js} +2 -2
  64. package/dist/{websites-EH2ULELZ.js → websites-U6642XSE.js} +10 -2
  65. package/dist/{write-audits-to-airtable-LZDYTNX5.js → write-audits-to-airtable-22T6D733.js} +4 -4
  66. package/package.json +1 -1
  67. package/dist/airtable-U2GTDTK5.js +0 -9
  68. package/dist/blux-FKZYMKLF.js.map +0 -1
  69. package/dist/chunk-BHE5CUWI.js.map +0 -1
  70. package/dist/chunk-BWKK2ZXS.js.map +0 -1
  71. package/dist/chunk-JGLAWXUC.js.map +0 -1
  72. package/dist/chunk-KRHPS3R4.js.map +0 -1
  73. package/dist/chunk-NRISG4XD.js.map +0 -1
  74. package/dist/chunk-QTHKOIFJ.js +0 -13
  75. package/dist/chunk-QTHKOIFJ.js.map +0 -1
  76. package/dist/digest-VX7Q4MNF.js.map +0 -1
  77. package/dist/renovate-dispatch-C3ALLACX.js.map +0 -1
  78. package/dist/report-YLFSGNWR.js.map +0 -1
  79. /package/dist/{airtable-U2GTDTK5.js.map → airtable-6L3JD7FY.js.map} +0 -0
  80. /package/dist/{announce-HJCGEVJA.js.map → announce-V6EYPPUR.js.map} +0 -0
  81. /package/dist/{bump-deps-GZEJAUKI.js.map → bump-deps-OQR3IXLN.js.map} +0 -0
  82. /package/dist/{chunk-BJCA7O4A.js.map → chunk-3PU2QWOJ.js.map} +0 -0
  83. /package/dist/{chunk-GI4M2AMO.js.map → chunk-4BHNP6Y2.js.map} +0 -0
  84. /package/dist/{chunk-KLB2OLPW.js.map → chunk-HBFMTM2E.js.map} +0 -0
  85. /package/dist/{chunk-SQJ53Z7R.js.map → chunk-I55YWV6T.js.map} +0 -0
  86. /package/dist/{chunk-7P7YETHG.js.map → chunk-IDBMYDVH.js.map} +0 -0
  87. /package/dist/{chunk-APTAITXZ.js.map → chunk-KKL3RYFQ.js.map} +0 -0
  88. /package/dist/{chunk-HR5SONJC.js.map → chunk-SK44IFZN.js.map} +0 -0
  89. /package/dist/{convert-to-pnpm-SVFDLXZS.js.map → convert-to-pnpm-OVMASATZ.js.map} +0 -0
  90. /package/dist/{ensure-site-O7G5ZJS7.js.map → ensure-site-3SOAWTZF.js.map} +0 -0
  91. /package/dist/{github-signals-QVW2LB7N.js.map → github-signals-TF3VSNIN.js.map} +0 -0
  92. /package/dist/{health-endpoint-IYR5VBUT.js.map → health-endpoint-JZGYCMTV.js.map} +0 -0
  93. /package/dist/{init-CU6PVZRG.js.map → init-7PTP4O2K.js.map} +0 -0
  94. /package/dist/{launch-JWXCTJRG.js.map → launch-W33FWWOZ.js.map} +0 -0
  95. /package/dist/{lighthouse-airtable-J5TLASJC.js.map → lighthouse-airtable-ZXNMDEPE.js.map} +0 -0
  96. /package/dist/{onboard-6UQE5QIO.js.map → onboard-PX36YDZA.js.map} +0 -0
  97. /package/dist/{orchestrate-ZDH24Z6Y.js.map → orchestrate-NYRYEKQM.js.map} +0 -0
  98. /package/dist/{preflight-BNTMWTPG.js.map → preflight-JCQFKB6M.js.map} +0 -0
  99. /package/dist/{self-updating-64FNRQB5.js.map → self-updating-2YSVNMPP.js.map} +0 -0
  100. /package/dist/{selftest-7DPNGL6R.js.map → selftest-OO2WDSDM.js.map} +0 -0
  101. /package/dist/{smoke-suite-TWILWDSF.js.map → smoke-suite-CSP23NZM.js.map} +0 -0
  102. /package/dist/{svelte-codemods-RVDTTXFD.js.map → svelte-codemods-V54KOBWU.js.map} +0 -0
  103. /package/dist/{sync-configs-BUUD6L5Q.js.map → sync-configs-UBCJRASZ.js.map} +0 -0
  104. /package/dist/{upgrade-HHKWYJRN.js.map → upgrade-WR6ICPAG.js.map} +0 -0
  105. /package/dist/{websites-EH2ULELZ.js.map → websites-U6642XSE.js.map} +0 -0
  106. /package/dist/{write-audits-to-airtable-LZDYTNX5.js.map → write-audits-to-airtable-22T6D733.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
- import "./chunk-KRHPS3R4.js";
2
- import {
3
- approveBlockers
4
- } from "./chunk-KLB2OLPW.js";
5
1
  import {
6
2
  draftReportForSite
7
- } from "./chunk-NRISG4XD.js";
3
+ } from "./chunk-NUIBQPB3.js";
8
4
  import "./chunk-KLVMJYZZ.js";
5
+ import "./chunk-RHPDQW6D.js";
6
+ import "./chunk-6OYICQSJ.js";
7
+ import {
8
+ approveBlockers
9
+ } from "./chunk-HBFMTM2E.js";
9
10
  import {
10
11
  findDueReports
11
12
  } from "./chunk-D4FVAIEJ.js";
@@ -16,7 +17,7 @@ import {
16
17
  DEFAULT_INIT_STEPS,
17
18
  a11yFixturesPage,
18
19
  init
19
- } from "./chunk-APTAITXZ.js";
20
+ } from "./chunk-KKL3RYFQ.js";
20
21
  import {
21
22
  convertToPnpm
22
23
  } from "./chunk-2X322ZES.js";
@@ -36,7 +37,7 @@ import {
36
37
  parseAddresses,
37
38
  sendApprovedReports,
38
39
  withGlobalCc
39
- } from "./chunk-7P7YETHG.js";
40
+ } from "./chunk-IDBMYDVH.js";
40
41
  import "./chunk-GECESMVS.js";
41
42
  import {
42
43
  BLURRED_CID,
@@ -54,7 +55,9 @@ import "./chunk-WEKL2HYJ.js";
54
55
  import {
55
56
  bumpDeps
56
57
  } from "./chunk-D3DQFC32.js";
57
- import "./chunk-QTHKOIFJ.js";
58
+ import {
59
+ isLeadFormType
60
+ } from "./chunk-2OP4JKIQ.js";
58
61
  import {
59
62
  syncConfigs
60
63
  } from "./chunk-JNXCDUYB.js";
@@ -70,7 +73,7 @@ import {
70
73
  runAudits,
71
74
  runAuditsAcross,
72
75
  securityAudit
73
- } from "./chunk-BHE5CUWI.js";
76
+ } from "./chunk-PERP7LSS.js";
74
77
  import "./chunk-GASBX52O.js";
75
78
  import "./chunk-YEGHUODU.js";
76
79
  import "./chunk-LKGVSM2O.js";
@@ -86,12 +89,12 @@ import "./chunk-XXTZBPUY.js";
86
89
  import "./chunk-XTK5VIQB.js";
87
90
  import {
88
91
  fromAirtableBase
89
- } from "./chunk-HR5SONJC.js";
92
+ } from "./chunk-SK44IFZN.js";
90
93
  import "./chunk-U5SZ4FOP.js";
91
94
  import {
92
95
  SEVERITY_RANK,
93
96
  siteSlug
94
- } from "./chunk-BWKK2ZXS.js";
97
+ } from "./chunk-GXVX4HUH.js";
95
98
  import "./chunk-HN6I2XWI.js";
96
99
  import "./chunk-6VAL7XJT.js";
97
100
  import "./chunk-EYODPUHV.js";
@@ -183,7 +186,16 @@ function extraFieldsList(raw) {
183
186
  function isVisibleInStrip(s) {
184
187
  return s.status !== "spam_auto";
185
188
  }
186
- function renderSubmissionRow(s) {
189
+ function spamReasonTokens(reason) {
190
+ return (reason ?? "").split(",").map((t) => t.trim()).filter((t) => t !== "");
191
+ }
192
+ var REASON_CHIP_MAX_TOKENS = 3;
193
+ function reasonChipText(tokens) {
194
+ const shown = tokens.slice(0, REASON_CHIP_MAX_TOKENS).join(" \xB7 ");
195
+ const more = tokens.length - REASON_CHIP_MAX_TOKENS;
196
+ return more > 0 ? `${shown} +${more} more` : shown;
197
+ }
198
+ function renderSubmissionRowInner(s) {
187
199
  const when = s.submittedAt ? escapeHtml(relativeTimeFromNow(s.submittedAt)) : "\u2014";
188
200
  const type = escapeHtml(s.formType);
189
201
  const who = escapeHtml(s.name || "(no name)");
@@ -192,28 +204,35 @@ function renderSubmissionRow(s) {
192
204
  const id = escapeHtml(s.id);
193
205
  const url = `/api/submissions/${encodeURIComponent(s.id)}/status`;
194
206
  const btn = (label, action) => `<button class="subm-status" data-id="${id}" data-status="${action}" data-url="${url}">${label}</button>`;
195
- const provenance = s.status === "spam_auto" && s.spamScore !== null && s.spamScore !== void 0 ? ` <span class="subm-provenance" title="${escapeHtml(s.spamReason ?? "")}">auto-spam \xB7 ${escapeHtml(String(s.spamScore))}</span>` : "";
207
+ const reasonTokens = spamReasonTokens(s.spamReason);
208
+ const reasonChip = reasonTokens.length > 0 ? ` <span class="subm-reasons">${escapeHtml(reasonChipText(reasonTokens))}</span>` : "";
209
+ const provenance = s.status === "spam_auto" && s.spamScore !== null && s.spamScore !== void 0 ? ` <span class="subm-provenance" title="${escapeHtml(s.spamReason ?? "")}">auto-spam \xB7 ${escapeHtml(String(s.spamScore))}</span>${reasonChip}` : "";
196
210
  const recover = s.status === "spam_auto" ? btn("Not spam \u2192 new", "new") : "";
211
+ const bounceChip = s.notifyStatus === "bounced" ? ` <span class="subm-bounce" title="The lead notification email bounced \u2014 the point-of-contact address may be dead">notify bounced</span>` : "";
197
212
  const kv = (label, value) => value === null || value === "" ? "" : `<div class="subm-kv"><span class="k">${label}</span> ${escapeHtml(String(value))}</div>`;
198
213
  const sourceLink = s.sourceUrl ? `<div class="subm-kv"><span class="k">Source</span> <a href="${escapeHtml(safeUrl(s.sourceUrl))}" rel="noopener noreferrer">${escapeHtml(s.sourceUrl)}</a></div>` : "";
199
214
  const messageBlock = s.message ? `<div class="subm-kv"><span class="k">Message</span></div><div class="subm-msg">${escapeHtml(s.message)}</div>` : "";
215
+ const hasScore = s.spamScore !== null && s.spamScore !== void 0;
216
+ const spamDetail = hasScore ? reasonTokens.length > 0 ? `score ${s.spamScore} \u2014 ${reasonTokens.join(", ")}` : `score ${s.spamScore}` : reasonTokens.join(", ");
200
217
  const details = [
201
218
  kv("Phone", s.phone),
202
219
  messageBlock,
203
220
  sourceLink,
204
221
  kv("UTM", s.utm),
205
222
  extraFieldsList(s.extraFields),
223
+ kv("Spam", spamDetail),
206
224
  kv("Notify", s.notifyStatus),
207
225
  kv("Resend ID", s.resendMessageId),
208
226
  kv("Submission #", s.submissionId)
209
227
  ].join("");
210
- return `<li class="subm-item">
211
- <details>
212
- <summary class="subm-head"><strong>${type}</strong> \xB7 ${who} <span class="muted">${email}</span> <span class="pill subm-${status}">${status}</span>${provenance} <span class="muted">${when}</span></summary>
228
+ return `<details>
229
+ <summary class="subm-head"><strong>${type}</strong> \xB7 ${who} <span class="muted">${email}</span> <span class="pill subm-${status}">${status}</span>${provenance}${bounceChip} <span class="muted">${when}</span></summary>
213
230
  <div class="subm-detail">${details}</div>
214
231
  </details>
215
- <div class="subm-actions">${recover}${btn("Read", "read")}${btn("Archive", "archived")}${btn("Spam", "spam")}</div>
216
- </li>`;
232
+ <div class="subm-actions">${recover}${btn("Read", "read")}${btn("Archive", "archived")}${btn("Spam", "spam")}</div>`;
233
+ }
234
+ function renderSubmissionRow(s) {
235
+ return `<li class="subm-item">${renderSubmissionRowInner(s)}</li>`;
217
236
  }
218
237
  var SUBMISSION_STYLES = `.subm-list { list-style: none; padding: 0; margin: 0; }
219
238
  .subm-item { padding: 0.6rem 0; border-bottom: 1px solid #eee; }
@@ -224,9 +243,10 @@ var SUBMISSION_STYLES = `.subm-list { list-style: none; padding: 0; margin: 0; }
224
243
  .subm-kv { font-size: 0.9rem; margin: 0.15rem 0; }
225
244
  .subm-kv .k { color: #888; margin-right: 0.4rem; }
226
245
  summary.subm-head { cursor: pointer; }
227
- .subm-actions { display: flex; gap: 0.4rem; }
246
+ .subm-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; }
228
247
  button.subm-status { font: inherit; padding: 0.25rem 0.7rem; border: 1px solid #888; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; }
229
248
  button.subm-status:disabled { opacity: 0.6; cursor: default; }
249
+ @media (pointer: coarse) { button.subm-status { padding: 0.55rem 0.9rem; } } /* >=40px tap target on touch devices */
230
250
  .spam-screen .spam-kv { font-size: 0.95rem; margin: 0.2rem 0; }
231
251
  .spam-screen .spam-kv .k { color: #888; display: inline-block; min-width: 11rem; }
232
252
  .pill.subm-new { background: #e8f0fe; color: #1a56db; }
@@ -235,6 +255,8 @@ button.subm-status:disabled { opacity: 0.6; cursor: default; }
235
255
  .pill.subm-spam { background: #fdecea; color: #b00; }
236
256
  .pill.subm-spam_auto { background: #fff4e5; color: #a65a00; }
237
257
  .subm-provenance { font-size: 0.72rem; border-radius: 0.25rem; padding: 0 0.35rem; white-space: nowrap; background: #fff4e5; color: #a65a00; }
258
+ .subm-bounce { font-size: 0.72rem; border-radius: 0.25rem; padding: 0 0.35rem; white-space: nowrap; background: #fdecea; color: #b00; font-weight: 700; }
259
+ .subm-reasons { font-size: 0.72rem; color: #999; }
238
260
  .subm-viewall { font-size: 0.8rem; font-weight: normal; margin-left: 0.4rem; white-space: nowrap; }`;
239
261
  var SUBMISSION_STATUS_SCRIPT = `document.querySelectorAll("button.subm-status").forEach((b) => {
240
262
  b.addEventListener("click", async () => {
@@ -474,16 +496,20 @@ function submissionsSection(submissions, site) {
474
496
  var SPAM_WINDOW_DAYS = 30;
475
497
  function spamScreenSection(totals, submissions, now) {
476
498
  const sinceMs = now.getTime() - SPAM_WINDOW_DAYS * 24 * 60 * 60 * 1e3;
477
- const delivered = submissions.filter(
499
+ const inWindow = submissions.filter(
478
500
  (s) => s.submittedAt !== null && Date.parse(s.submittedAt) >= sinceMs
479
- ).length;
501
+ );
502
+ const delivered = inWindow.filter((s) => s.status !== "spam_auto" && s.status !== "spam").length;
503
+ const autoFiltered = inWindow.filter((s) => s.status === "spam_auto").length;
480
504
  const t = totals ?? { honeypot: 0, tooFast: 0, markedSpam: 0 };
481
- if (delivered === 0 && t.honeypot === 0 && t.tooFast === 0 && t.markedSpam === 0) return "";
505
+ if (delivered === 0 && autoFiltered === 0 && t.honeypot === 0 && t.tooFast === 0 && t.markedSpam === 0)
506
+ return "";
482
507
  const row = (label, n) => `<div class="spam-kv"><span class="k">${label}</span> ${escapeHtml(String(n))}</div>`;
483
508
  return `<div class="section spam-screen">
484
509
  <h2>Spam screen (30d)</h2>
485
510
  ${row("Caught \u2014 honeypot", t.honeypot)}
486
511
  ${row("Caught \u2014 too-fast", t.tooFast)}
512
+ ${row("Auto-filtered", autoFiltered)}
487
513
  ${row("Delivered", delivered)}
488
514
  ${row("Marked spam", t.markedSpam)}
489
515
  </div>`;
@@ -494,6 +520,26 @@ function setupSection(site) {
494
520
  const detail = missing.length === 0 ? `<span class="setup-ok">complete</span>` : `<span class="setup-missing">Missing: ${escapeHtml(missing.join(", "))}</span>`;
495
521
  return `<div class="setup-line">Setup ${score}/${total} \u2014 ${detail}</div>`;
496
522
  }
523
+ function alarmSection(alarm) {
524
+ if (!alarm) return "";
525
+ const chips2 = [];
526
+ for (const it of alarm.items) {
527
+ const cls = it.autoFixExhausted ? "chip critical stuck" : it.severity === "critical" ? "chip critical" : "chip";
528
+ chips2.push(`<span class="${cls}">${escapeHtml(it.title)}</span>`);
529
+ }
530
+ alarm.watchReasons.forEach((reason, i) => {
531
+ const key = alarm.watchAcceptKeys[i];
532
+ const hint = key ? ` \xB7 accept: "${escapeHtml(key)}"` : "";
533
+ chips2.push(
534
+ `<span class="chip" title="Add this exact text to the site's Accepted Watch Conditions to mute this watch">${escapeHtml(reason)}${hint}</span>`
535
+ );
536
+ });
537
+ for (const reason of alarm.acceptedReasons)
538
+ chips2.push(`<span class="chip accepted">\u2713 accepted: ${escapeHtml(reason)}</span>`);
539
+ if (chips2.length === 0) return "";
540
+ const label = alarm.tier === "attention" ? "Needs attention" : alarm.tier === "watch" ? "Watch" : "Status";
541
+ return `<div class="section alarm"><h2>${label}</h2><div class="chips">${chips2.join("")}</div></div>`;
542
+ }
497
543
  function detailRow(label, value) {
498
544
  const display = typeof value === "string" && value.trim().length > 0 ? escapeHtml(value.trim()) : DASH;
499
545
  return `<div class="detail"><dt>${escapeHtml(label)}</dt><dd>${display}</dd></div>`;
@@ -659,8 +705,16 @@ button.override-submit:disabled { opacity: 0.6; cursor: default; }
659
705
  .detail dd textarea { min-height: 3.5rem; resize: vertical; }
660
706
  .detail-saved { font-size: 0.8rem; color: #2a8; }
661
707
  button.trigger-renovate { font: inherit; font-size: 0.8rem; padding: 0.15rem 0.6rem; margin-left: 0.5rem; border: 1px solid #888; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; }
708
+ /* Alarm chip strip \u2014 mirrors fleet-render.ts chip styles (NOT the page's .pill classes). */
709
+ .chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
710
+ .chip { font-size: 0.8rem; padding: 0.1rem 0.5rem; border-radius: 6px; background: #f0f0f0; }
711
+ @media (prefers-color-scheme: dark) { .chip { background: #222; } }
712
+ .chip.critical { background: #fdecea; color: #b00; }
713
+ .chip.stuck { border: 1px solid #b00; font-weight: 600; }
714
+ .chip.accepted { background: transparent; border: 1px dashed #bbb; color: #888; }
715
+ @media (prefers-color-scheme: dark) { .chip.accepted { border-color: #444; } .chip.critical { background: #2a0f0d; color: #ff8a80; } }
662
716
  `;
663
- function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = null, now = /* @__PURE__ */ new Date()) {
717
+ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = null, now = /* @__PURE__ */ new Date(), alarm = null) {
664
718
  const name = escapeHtml(site.name);
665
719
  const urlSafe = safeUrl(site.url);
666
720
  const allScoresNull = site.pScore === null && site.rScore === null && site.bpScore === null && site.seoScore === null;
@@ -698,6 +752,7 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
698
752
  <div class="meta"><a href="${escapeHtml(urlSafe)}">${escapeHtml(site.url)}</a></div>
699
753
  ${auditedLine}
700
754
  ${setupSection(site)}
755
+ ${alarmSection(alarm)}
701
756
  ${pendingSection(reports, site, now)}
702
757
 
703
758
  <div class="section">
@@ -907,6 +962,13 @@ function buildNeedsYouFeed(model) {
907
962
  a.reasons.push(`${p.reportType} ${p.period} ready`);
908
963
  a.approval = true;
909
964
  }
965
+ for (const u of model.unrecognizedStatus ?? []) {
966
+ const a = get(u.name);
967
+ a.reasons.push(
968
+ `Status "${u.status}" is not a recognized value \u2014 fix it in Airtable (site is invisible to fleet ops)`
969
+ );
970
+ a.watch = true;
971
+ }
910
972
  const items = [];
911
973
  for (const a of bySlug.values()) {
912
974
  if (a.reasons.length === 0) continue;
@@ -1024,8 +1086,11 @@ function deployBadge(site) {
1024
1086
  return `<span class="metric ${cls}">${label}</span>`;
1025
1087
  }
1026
1088
  function submBadge(c) {
1027
- const n = c.newSubmissions ?? 0;
1028
- return n > 0 ? `<span class="chip">\u{1F4E5} ${n} new</span>` : "";
1089
+ const leads = c.newLeads ?? c.newSubmissions ?? 0;
1090
+ const signups = c.newSignups ?? 0;
1091
+ if (leads === 0 && signups === 0) return "";
1092
+ const label = leads > 0 && signups > 0 ? `\u{1F4E5} ${leads} new \xB7 +${signups} newsletter` : leads > 0 ? `\u{1F4E5} ${leads} new` : `\u{1F4E5} +${signups} newsletter`;
1093
+ return `<span class="chip">${label}</span>`;
1029
1094
  }
1030
1095
  var PILL_LABEL = {
1031
1096
  attention: "failing",
@@ -1043,8 +1108,14 @@ function chips(c) {
1043
1108
  const cls = it.autoFixExhausted ? "chip critical stuck" : it.severity === "critical" ? "chip critical" : "chip";
1044
1109
  return `<span class="${cls}">${attentionBadge(it.status)}${escapeHtml(it.title)}</span>`;
1045
1110
  });
1046
- for (const reason of c.watchReasons)
1047
- items.push(`<span class="chip">${escapeHtml(reason)}</span>`);
1111
+ const acceptKeys = c.watchAcceptKeys ?? [];
1112
+ c.watchReasons.forEach((reason, i) => {
1113
+ const key = acceptKeys[i];
1114
+ const hint = key ? ` \xB7 accept: "${escapeHtml(key)}"` : "";
1115
+ items.push(
1116
+ `<span class="chip" title="Add this exact text to the site's Accepted Watch Conditions to mute this watch">${escapeHtml(reason)}${hint}</span>`
1117
+ );
1118
+ });
1048
1119
  for (const reason of c.acceptedReasons)
1049
1120
  items.push(`<span class="chip accepted">\u2713 accepted: ${escapeHtml(reason)}</span>`);
1050
1121
  return items.length ? `<div class="chips">${items.join("")}</div>` : "";
@@ -1228,6 +1299,8 @@ function verdictBar(model, feed) {
1228
1299
  const { broken, watch, approval } = needsYouCounts(feed);
1229
1300
  const preLaunch = model.summary.preLaunch;
1230
1301
  const preLaunchTerm = preLaunch > 0 ? `${preLaunch} pre-launch` : null;
1302
+ const archivedCount = model.archived?.length ?? 0;
1303
+ const archivedTerm = archivedCount > 0 ? `${archivedCount} archived` : null;
1231
1304
  const healthy = Math.max(0, total - (broken + watch + approval + preLaunch));
1232
1305
  const actions = `<div class="fleet-actions">
1233
1306
  <button type="button" class="refresh-fleet" data-refresh-url="/api/fleet/refresh">\u21BB Audit fleet</button>
@@ -1240,7 +1313,7 @@ function verdictBar(model, feed) {
1240
1313
  </div>`;
1241
1314
  if (broken === 0 && watch === 0 && approval === 0) {
1242
1315
  const healthyWord = healthy > 0 ? `${healthy} site${healthy === 1 ? "" : "s"} healthy` : null;
1243
- return render("ok", "\u2713 All clear", [healthyWord, preLaunchTerm, auditedTerm]);
1316
+ return render("ok", "\u2713 All clear", [healthyWord, preLaunchTerm, archivedTerm, auditedTerm]);
1244
1317
  }
1245
1318
  const watchTerm = watch > 0 ? `${watch} watching` : null;
1246
1319
  const approvalTerm = approval > 0 ? `${approval} waiting on you` : null;
@@ -1251,6 +1324,7 @@ function verdictBar(model, feed) {
1251
1324
  approvalTerm,
1252
1325
  healthyTerm,
1253
1326
  preLaunchTerm,
1327
+ archivedTerm,
1254
1328
  auditedTerm
1255
1329
  ]);
1256
1330
  }
@@ -1259,10 +1333,16 @@ function verdictBar(model, feed) {
1259
1333
  approvalTerm,
1260
1334
  healthyTerm,
1261
1335
  preLaunchTerm,
1336
+ archivedTerm,
1262
1337
  auditedTerm
1263
1338
  ]);
1264
1339
  }
1265
- return render("soft", `${approval} waiting on you`, [healthyTerm, preLaunchTerm, auditedTerm]);
1340
+ return render("soft", `${approval} waiting on you`, [
1341
+ healthyTerm,
1342
+ preLaunchTerm,
1343
+ archivedTerm,
1344
+ auditedTerm
1345
+ ]);
1266
1346
  }
1267
1347
  var NEEDS_YOU_GROUP_LABEL = {
1268
1348
  broken: "Broken",
@@ -1316,6 +1396,14 @@ function renderRecentlyLane(model) {
1316
1396
  ${rows}
1317
1397
  </details>`;
1318
1398
  }
1399
+ function renderArchivedLane(model) {
1400
+ const rows = model.archived ?? [];
1401
+ if (rows.length === 0) return "";
1402
+ const lis = rows.map(
1403
+ (s) => `<div class="approve-row"><strong>${escapeHtml(s.name)}</strong> <span class="muted">${escapeHtml(s.status)}</span> <a href="/s/${escapeHtml(s.slug)}">open \u25B8</a></div>`
1404
+ ).join("");
1405
+ return `<details class="archived"><summary>\u{1F5C4} Archived (${rows.length})</summary>${lis}</details>`;
1406
+ }
1319
1407
  function renderInboxLane(model) {
1320
1408
  const subs = model.submissions ?? [];
1321
1409
  const spam = model.spam;
@@ -1340,8 +1428,11 @@ function renderInboxLane(model) {
1340
1428
  const spamLine = hasSpam ? `<div class="spam-rollup muted">\u{1F6E1} Spam (30d) \u2014 caught ${spam.caught} \xB7 through ${spam.through}</div>` : "";
1341
1429
  const spamInSummary = hasSpam ? " \xB7 \u{1F6E1} Spam (30d)" : "";
1342
1430
  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>` : "";
1431
+ const leads = subs.filter((s) => isLeadFormType(s.formType)).length;
1432
+ const signups = subs.length - leads;
1433
+ const summaryCount = signups === 0 ? `${leads} new` : `${leads} new \xB7 +${signups} newsletter/rsvp`;
1343
1434
  return `<details class="inbox">
1344
- <summary>\u{1F4E5} Submissions (${subs.length} new)${spamInSummary}</summary>
1435
+ <summary>\u{1F4E5} Submissions (${summaryCount})${spamInSummary}</summary>
1345
1436
  ${rows}${subs.length > 0 ? more : ""}
1346
1437
  ${spamLine}
1347
1438
  ${autoFilteredLine}
@@ -1484,6 +1575,7 @@ function renderCockpitHtml(model) {
1484
1575
  ${renderNeedsYouFeed(feed)}
1485
1576
  ${renderFleetBrowsePanel(model)}
1486
1577
  ${renderRecentlyLane(model)}
1578
+ ${renderArchivedLane(model)}
1487
1579
  ${renderInboxLane(model)}
1488
1580
  ${AUDIT_SCRIPT}
1489
1581
  ${FLEET_BROWSE_SCRIPT}