@reddoorla/maintenance 0.63.0 → 0.65.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 (89) hide show
  1. package/dist/airtable-GGZJUGQB.js +9 -0
  2. package/dist/{announce-QEON4O7E.js → announce-GFLKAPTT.js} +4 -4
  3. package/dist/{bump-deps-2U6GIHM5.js → bump-deps-3QQHYDKJ.js} +2 -2
  4. package/dist/{chunk-BMDBOPN3.js → chunk-4A5PYCGB.js} +2 -2
  5. package/dist/{chunk-OSRFDGQM.js → chunk-4GACMVNE.js} +2 -1
  6. package/dist/{chunk-BLD6AYJO.js → chunk-7RGQ4KLE.js} +5 -2
  7. package/dist/chunk-7RGQ4KLE.js.map +1 -0
  8. package/dist/{chunk-YYUY2OVP.js → chunk-CSTLFSGM.js} +2 -2
  9. package/dist/{chunk-SAOCVNX7.js → chunk-FH7WQHD7.js} +2 -2
  10. package/dist/{chunk-KTPPKO2C.js → chunk-FX3QBSUF.js} +2 -2
  11. package/dist/{chunk-L5F664ZA.js → chunk-G2YRXV26.js} +2 -2
  12. package/dist/chunk-GASBX52O.js +12 -0
  13. package/dist/chunk-GASBX52O.js.map +1 -0
  14. package/dist/{chunk-4REUHZAW.js → chunk-LM42CATB.js} +3 -3
  15. package/dist/{chunk-NPJFHBAX.js → chunk-M5LX2VSR.js} +2 -2
  16. package/dist/{chunk-ZEX2T6JH.js → chunk-MQM5YP4J.js} +2 -2
  17. package/dist/{chunk-P7VJM46I.js → chunk-OYN6H4CX.js} +3 -3
  18. package/dist/{chunk-6ZKM7OIJ.js → chunk-PTPTVVY7.js} +6 -4
  19. package/dist/chunk-PTPTVVY7.js.map +1 -0
  20. package/dist/{chunk-J37CZORZ.js → chunk-QSL3EJGI.js} +3 -3
  21. package/dist/{chunk-SS7BKOIX.js → chunk-TC7Z5LG7.js} +237 -1
  22. package/dist/chunk-TC7Z5LG7.js.map +1 -0
  23. package/dist/{chunk-3HM4S422.js → chunk-UATHDNWP.js} +7 -14
  24. package/dist/chunk-UATHDNWP.js.map +1 -0
  25. package/dist/cli/bin.js +16 -16
  26. package/dist/cli/bin.js.map +1 -1
  27. package/dist/cli/commands/audit.js +10 -8
  28. package/dist/cli/commands/audit.js.map +1 -1
  29. package/dist/{client-GTMJTXBK.js → client-C2GOZD2D.js} +2 -2
  30. package/dist/{convert-to-pnpm-JG3767ZQ.js → convert-to-pnpm-ZO673UFB.js} +2 -2
  31. package/dist/{db-Y6BVTNH7.js → db-YBLQR2YY.js} +2 -2
  32. package/dist/{digest-ABJROO4H.js → digest-R3ROSGLP.js} +3 -3
  33. package/dist/{github-signals-XTJLKOXJ.js → github-signals-N5JGWHW2.js} +9 -9
  34. package/dist/index.d.ts +39 -3
  35. package/dist/index.js +86 -40
  36. package/dist/index.js.map +1 -1
  37. package/dist/{init-DMAIKUQE.js → init-FOHNISF2.js} +6 -4
  38. package/dist/{init-DMAIKUQE.js.map → init-FOHNISF2.js.map} +1 -1
  39. package/dist/{launch-IBP23PNY.js → launch-TM3IHSSU.js} +9 -8
  40. package/dist/{launch-IBP23PNY.js.map → launch-TM3IHSSU.js.map} +1 -1
  41. package/dist/{lighthouse-airtable-HTEW6BBT.js → lighthouse-airtable-5FHELRCA.js} +3 -3
  42. package/dist/{onboard-TTIC3YZI.js → onboard-K2EL4MSG.js} +2 -2
  43. package/dist/{orchestrate-CYYZ737H.js → orchestrate-OX3Z4DUI.js} +5 -5
  44. package/dist/{renovate-dispatch-QDDMTATD.js → renovate-dispatch-H25CQ36P.js} +3 -3
  45. package/dist/{report-QNE7HFOZ.js → report-UEYKO6WH.js} +5 -5
  46. package/dist/{self-updating-2RLUP4AA.js → self-updating-YXOGQWH5.js} +5 -4
  47. package/dist/{self-updating-2RLUP4AA.js.map → self-updating-YXOGQWH5.js.map} +1 -1
  48. package/dist/{selftest-5ELLRH6Q.js → selftest-VE7CSGWE.js} +7 -7
  49. package/dist/{svelte-codemods-BPLKSVIA.js → svelte-codemods-745QM25J.js} +2 -2
  50. package/dist/{sync-configs-VFRV3TJ4.js → sync-configs-NYC3Z625.js} +2 -2
  51. package/dist/{upgrade-WCFD2TRN.js → upgrade-UBPPJ6HN.js} +2 -2
  52. package/dist/{websites-HM2NMZMR.js → websites-CSVLSEVL.js} +2 -2
  53. package/dist/{write-audits-to-airtable-QF26PCAR.js → write-audits-to-airtable-JCV7FYAW.js} +4 -4
  54. package/package.json +1 -1
  55. package/dist/airtable-HHAWHBNM.js +0 -9
  56. package/dist/chunk-3HM4S422.js.map +0 -1
  57. package/dist/chunk-6ZKM7OIJ.js.map +0 -1
  58. package/dist/chunk-BLD6AYJO.js.map +0 -1
  59. package/dist/chunk-SS7BKOIX.js.map +0 -1
  60. /package/dist/{airtable-HHAWHBNM.js.map → airtable-GGZJUGQB.js.map} +0 -0
  61. /package/dist/{announce-QEON4O7E.js.map → announce-GFLKAPTT.js.map} +0 -0
  62. /package/dist/{bump-deps-2U6GIHM5.js.map → bump-deps-3QQHYDKJ.js.map} +0 -0
  63. /package/dist/{chunk-BMDBOPN3.js.map → chunk-4A5PYCGB.js.map} +0 -0
  64. /package/dist/{chunk-OSRFDGQM.js.map → chunk-4GACMVNE.js.map} +0 -0
  65. /package/dist/{chunk-YYUY2OVP.js.map → chunk-CSTLFSGM.js.map} +0 -0
  66. /package/dist/{chunk-SAOCVNX7.js.map → chunk-FH7WQHD7.js.map} +0 -0
  67. /package/dist/{chunk-KTPPKO2C.js.map → chunk-FX3QBSUF.js.map} +0 -0
  68. /package/dist/{chunk-L5F664ZA.js.map → chunk-G2YRXV26.js.map} +0 -0
  69. /package/dist/{chunk-4REUHZAW.js.map → chunk-LM42CATB.js.map} +0 -0
  70. /package/dist/{chunk-NPJFHBAX.js.map → chunk-M5LX2VSR.js.map} +0 -0
  71. /package/dist/{chunk-ZEX2T6JH.js.map → chunk-MQM5YP4J.js.map} +0 -0
  72. /package/dist/{chunk-P7VJM46I.js.map → chunk-OYN6H4CX.js.map} +0 -0
  73. /package/dist/{chunk-J37CZORZ.js.map → chunk-QSL3EJGI.js.map} +0 -0
  74. /package/dist/{client-GTMJTXBK.js.map → client-C2GOZD2D.js.map} +0 -0
  75. /package/dist/{convert-to-pnpm-JG3767ZQ.js.map → convert-to-pnpm-ZO673UFB.js.map} +0 -0
  76. /package/dist/{db-Y6BVTNH7.js.map → db-YBLQR2YY.js.map} +0 -0
  77. /package/dist/{digest-ABJROO4H.js.map → digest-R3ROSGLP.js.map} +0 -0
  78. /package/dist/{github-signals-XTJLKOXJ.js.map → github-signals-N5JGWHW2.js.map} +0 -0
  79. /package/dist/{lighthouse-airtable-HTEW6BBT.js.map → lighthouse-airtable-5FHELRCA.js.map} +0 -0
  80. /package/dist/{onboard-TTIC3YZI.js.map → onboard-K2EL4MSG.js.map} +0 -0
  81. /package/dist/{orchestrate-CYYZ737H.js.map → orchestrate-OX3Z4DUI.js.map} +0 -0
  82. /package/dist/{renovate-dispatch-QDDMTATD.js.map → renovate-dispatch-H25CQ36P.js.map} +0 -0
  83. /package/dist/{report-QNE7HFOZ.js.map → report-UEYKO6WH.js.map} +0 -0
  84. /package/dist/{selftest-5ELLRH6Q.js.map → selftest-VE7CSGWE.js.map} +0 -0
  85. /package/dist/{svelte-codemods-BPLKSVIA.js.map → svelte-codemods-745QM25J.js.map} +0 -0
  86. /package/dist/{sync-configs-VFRV3TJ4.js.map → sync-configs-NYC3Z625.js.map} +0 -0
  87. /package/dist/{upgrade-WCFD2TRN.js.map → upgrade-UBPPJ6HN.js.map} +0 -0
  88. /package/dist/{websites-HM2NMZMR.js.map → websites-CSVLSEVL.js.map} +0 -0
  89. /package/dist/{write-audits-to-airtable-QF26PCAR.js.map → write-audits-to-airtable-JCV7FYAW.js.map} +0 -0
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as Site, a as AuditResult, A as AuditName, b as RecipeResult, R as RecipeName, I as InventoryProvider } from './types-Dg4mHvyf.js';
1
+ import { a as AuditResult, S as Site, A as AuditName, b as RecipeResult, R as RecipeName, I as InventoryProvider } from './types-Dg4mHvyf.js';
2
2
  export { C as ConfigName } from './types-Dg4mHvyf.js';
3
3
  import * as airtable_lib_airtable_base_js from 'airtable/lib/airtable_base.js';
4
4
  import { F as FormType } from './types-RXY-vY-5.js';
@@ -91,6 +91,33 @@ type NetlifyDeployDeps = {
91
91
  now: Date;
92
92
  };
93
93
 
94
+ /** One GitHub Dependabot alert, mapped to the fields the security audit needs. */
95
+ type DependabotAlert = {
96
+ /** The vulnerable package name (`dependency.package.name`). */
97
+ package: string;
98
+ /** GitHub's raw severity vocabulary: "low" | "medium" | "high" | "critical". The audit layer
99
+ * maps "medium" → the app's "moderate"; the rest pass through. */
100
+ severity: string;
101
+ /** Human advisory summary (`security_advisory.summary`). */
102
+ summary: string;
103
+ /** CVE id from the advisory's `cve_id`, as a 0- or 1-element array. Empty for GHSA-only
104
+ * advisories (no CVE assigned) — those have no inline identifier here, but `url` still links
105
+ * to the GHSA advisory page. (The GitHub `identifiers[]` array is intentionally not surfaced.) */
106
+ cves: string[];
107
+ /** Link to the alert (`html_url`), or null. */
108
+ url: string | null;
109
+ /** Dependency graph scope, or null when GitHub doesn't classify it. "development" marks a
110
+ * build-time-only dependency (lower live-exploit surface for a static site). */
111
+ scope: "runtime" | "development" | null;
112
+ };
113
+
114
+ /** The Dependabot fetch the security audit needs, injectable for tests. The real default is
115
+ * built from GITHUB_TOKEN; absent token (or a site with no gitRepo) → pnpm/npm audit fallback. */
116
+ type DependabotDeps = {
117
+ listAlerts: (repo: string) => Promise<DependabotAlert[]>;
118
+ };
119
+ declare function securityAudit(ctx: AuditContext): Promise<AuditResult>;
120
+
94
121
  type AuditContext = {
95
122
  site: Site;
96
123
  spawn?: SpawnFn;
@@ -104,14 +131,15 @@ type AuditContext = {
104
131
  browserRunner?: BrowserRunner;
105
132
  /** Netlify API injection for the netlify-deploy audit (tests). Defaults to a real API call. */
106
133
  netlifyDeployDeps?: NetlifyDeployDeps;
134
+ /** GitHub Dependabot fetch injection for the security audit (tests). Defaults to a real client
135
+ * from GITHUB_TOKEN; absent token or no site.gitRepo → the pnpm/npm audit fallback. */
136
+ dependabotDeps?: DependabotDeps;
107
137
  };
108
138
 
109
139
  declare function depsAudit(ctx: AuditContext): Promise<AuditResult>;
110
140
 
111
141
  declare function lintAudit(ctx: AuditContext): Promise<AuditResult>;
112
142
 
113
- declare function securityAudit(ctx: AuditContext): Promise<AuditResult>;
114
-
115
143
  declare function lighthouseAudit(ctx: AuditContext): Promise<AuditResult>;
116
144
 
117
145
  declare function a11yAudit(ctx: AuditContext): Promise<AuditResult>;
@@ -307,6 +335,7 @@ type WebsiteRow = {
307
335
  gitRepo: string | null;
308
336
  reportRecipientsTo: string | null;
309
337
  reportRecipientsCc: string | null;
338
+ acceptedWatchConditions: string[];
310
339
  /** First attachment in the Header image field (Airtable's signed URL — fetch before expiry). */
311
340
  headerImage: {
312
341
  url: string;
@@ -396,6 +425,10 @@ type SecurityAdvisory = {
396
425
  title: string;
397
426
  cves: string[];
398
427
  url: string | null;
428
+ /** GitHub Dependabot dependency scope ("runtime" | "development"), when known. Lets the
429
+ * dashboard flag build-time-only ("development") vulns. Omitted for advisories from the
430
+ * lockfile `pnpm audit` fallback, which carries no per-package graph scope. */
431
+ scope?: "runtime" | "development";
399
432
  };
400
433
 
401
434
  type ResolvedCopy = {
@@ -776,6 +809,9 @@ type SiteCard = {
776
809
  watchReasons: string[];
777
810
  /** Structured watch tags ("lighthouse" / "stale") for the client filter. */
778
811
  watchSignals: string[];
812
+ /** Watch reasons the operator has accepted: suppressed from the band, shown as a
813
+ * muted chip. Populated whenever the underlying condition is currently active. */
814
+ acceptedReasons: string[];
779
815
  /** Count of NEW submissions for this site (optional; populated by buildCockpitModel). */
780
816
  newSubmissions?: number;
781
817
  };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import "./chunk-BMDBOPN3.js";
1
+ import "./chunk-4A5PYCGB.js";
2
2
  import {
3
3
  findDueReports
4
4
  } from "./chunk-WVRPOXKH.js";
@@ -9,7 +9,7 @@ import {
9
9
  DEFAULT_INIT_STEPS,
10
10
  a11yFixturesPage,
11
11
  init
12
- } from "./chunk-KTPPKO2C.js";
12
+ } from "./chunk-FX3QBSUF.js";
13
13
  import {
14
14
  convertToPnpm
15
15
  } from "./chunk-2X322ZES.js";
@@ -24,11 +24,11 @@ import {
24
24
  } from "./chunk-ZYBOT5PM.js";
25
25
  import {
26
26
  draftReportForSite
27
- } from "./chunk-NPJFHBAX.js";
27
+ } from "./chunk-M5LX2VSR.js";
28
28
  import "./chunk-6LIWDIXU.js";
29
29
  import {
30
30
  sendApprovedReports
31
- } from "./chunk-J37CZORZ.js";
31
+ } from "./chunk-QSL3EJGI.js";
32
32
  import "./chunk-OGGRFBL4.js";
33
33
  import "./chunk-GECESMVS.js";
34
34
  import {
@@ -62,11 +62,13 @@ import {
62
62
  runAudits,
63
63
  runAuditsAcross,
64
64
  securityAudit
65
- } from "./chunk-SS7BKOIX.js";
65
+ } from "./chunk-TC7Z5LG7.js";
66
66
  import "./chunk-LKGVSM2O.js";
67
- import "./chunk-L5F664ZA.js";
68
- import "./chunk-6ZKM7OIJ.js";
67
+ import "./chunk-G2YRXV26.js";
68
+ import "./chunk-PTPTVVY7.js";
69
69
  import "./chunk-XKKTK6WA.js";
70
+ import "./chunk-GASBX52O.js";
71
+ import "./chunk-4GACMVNE.js";
70
72
  import {
71
73
  fromJsonFile,
72
74
  localPath
@@ -76,12 +78,12 @@ import "./chunk-XXTZBPUY.js";
76
78
  import "./chunk-XTK5VIQB.js";
77
79
  import {
78
80
  fromAirtableBase
79
- } from "./chunk-ZEX2T6JH.js";
81
+ } from "./chunk-MQM5YP4J.js";
80
82
  import "./chunk-U5SZ4FOP.js";
81
83
  import {
82
84
  SEVERITY_RANK,
83
85
  siteSlug
84
- } from "./chunk-BLD6AYJO.js";
86
+ } from "./chunk-7RGQ4KLE.js";
85
87
  import "./chunk-HN6I2XWI.js";
86
88
  import "./chunk-WEKL2HYJ.js";
87
89
  import "./chunk-6VAL7XJT.js";
@@ -285,12 +287,13 @@ function securitySub(site) {
285
287
  function advisoryRow(a) {
286
288
  const sev = escapeHtml(a.severity);
287
289
  const module = escapeHtml(a.module);
290
+ const scope = a.scope === "development" ? ` <span class="muted">(dev)</span>` : "";
288
291
  const title = a.title ? ` \u2014 ${escapeHtml(a.title)}` : "";
289
292
  const cves = a.cves.length > 0 ? ` <span class="muted">(${escapeHtml(a.cves.join(", "))})</span>` : "";
290
293
  const link = a.url ? ` <a href="${escapeHtml(safeUrl(a.url))}" rel="noopener noreferrer">advisory \u25B8</a>` : "";
291
294
  return `<li class="vuln-item">
292
295
  <span class="pill sev-${sev}">${sev}</span>
293
- <strong>${module}</strong>${title}${cves}${link}
296
+ <strong>${module}</strong>${scope}${title}${cves}${link}
294
297
  </li>`;
295
298
  }
296
299
  function securitySection(site) {
@@ -651,7 +654,7 @@ function isReadyDeployStatus(status) {
651
654
  function isFailedDeployStatus(status) {
652
655
  return status !== null && FAILED_DEPLOY_STATES.has(status.toLowerCase());
653
656
  }
654
- var NEEDS_YOU_GROUP_RANK = { broken: 0, approval: 1, slipping: 2 };
657
+ var NEEDS_YOU_GROUP_RANK = { broken: 0, watch: 1, approval: 2 };
655
658
  function buildNeedsYouFeed(model) {
656
659
  const bySlug = /* @__PURE__ */ new Map();
657
660
  const get = (siteName) => {
@@ -664,8 +667,8 @@ function buildNeedsYouFeed(model) {
664
667
  reasons: [],
665
668
  hasCritical: false,
666
669
  broken: false,
667
- approval: false,
668
- slipping: false
670
+ watch: false,
671
+ approval: false
669
672
  };
670
673
  bySlug.set(slug, a);
671
674
  }
@@ -673,17 +676,25 @@ function buildNeedsYouFeed(model) {
673
676
  };
674
677
  for (const card2 of model.cards) {
675
678
  if (card2.tier === "attention") {
676
- for (const item of card2.items) {
677
- if (item.kind === "vuln" && item.autoFixExhausted !== true) continue;
679
+ const isSelfPatchingVuln = (it) => it.kind === "vuln" && it.autoFixExhausted !== true;
680
+ const hardBroken = card2.items.filter((it) => !isSelfPatchingVuln(it));
681
+ const selfPatchingVulns = card2.items.filter(isSelfPatchingVuln);
682
+ if (hardBroken.length > 0) {
678
683
  const a = get(card2.site.name);
679
- a.reasons.push(item.title);
684
+ for (const it of hardBroken) {
685
+ a.reasons.push(it.title);
686
+ if (it.severity === "critical") a.hasCritical = true;
687
+ }
680
688
  a.broken = true;
681
- if (item.severity === "critical") a.hasCritical = true;
689
+ } else if (selfPatchingVulns.length > 0) {
690
+ const a = get(card2.site.name);
691
+ for (const it of selfPatchingVulns) a.reasons.push(it.title);
692
+ a.watch = true;
682
693
  }
683
694
  } else if (card2.tier === "watch" && card2.watchReasons.length > 0) {
684
695
  const a = get(card2.site.name);
685
696
  for (const r of card2.watchReasons) a.reasons.push(r);
686
- a.slipping = true;
697
+ a.watch = true;
687
698
  }
688
699
  }
689
700
  for (const p of model.pending) {
@@ -694,7 +705,7 @@ function buildNeedsYouFeed(model) {
694
705
  const items = [];
695
706
  for (const a of bySlug.values()) {
696
707
  if (a.reasons.length === 0) continue;
697
- const group = a.broken ? "broken" : a.approval ? "approval" : "slipping";
708
+ const group = a.broken ? "broken" : a.watch ? "watch" : "approval";
698
709
  items.push({
699
710
  group,
700
711
  hasCritical: a.hasCritical,
@@ -824,6 +835,8 @@ function chips(c) {
824
835
  });
825
836
  for (const reason of c.watchReasons)
826
837
  items.push(`<span class="chip">${escapeHtml(reason)}</span>`);
838
+ for (const reason of c.acceptedReasons)
839
+ items.push(`<span class="chip accepted">\u2713 accepted: ${escapeHtml(reason)}</span>`);
827
840
  return items.length ? `<div class="chips">${items.join("")}</div>` : "";
828
841
  }
829
842
  function signalsAttr(c) {
@@ -955,6 +968,8 @@ details.fleet-browse > summary, details.inbox > summary { cursor:pointer; font-w
955
968
  @media (prefers-color-scheme: dark) { .chip { background:#222; } }
956
969
  .chip.critical { background:#fdecea; color:#b00; }
957
970
  .chip.stuck { border:1px solid #b00; font-weight:600; }
971
+ .chip.accepted { background:transparent; border:1px dashed #bbb; color:#888; }
972
+ @media (prefers-color-scheme: dark) { .chip.accepted { border-color:#444; color:#888; } }
958
973
  .badge { font-weight:700; color:#C00; font-size:0.72rem; margin-right:0.25rem; }
959
974
  .verdict { border-radius:8px; padding:0.9rem 1.1rem; margin-bottom:1.25rem; }
960
975
  .verdict .verdict-line { font-weight:800; font-size:1.4rem; }
@@ -963,7 +978,11 @@ details.fleet-browse > summary, details.inbox > summary { cursor:pointer; font-w
963
978
  .verdict.ok .verdict-meta { color:#2e7d32; }
964
979
  .verdict.warn { background:#fdecea; color:#b00; }
965
980
  .verdict.warn .verdict-meta { color:#b00; opacity:0.85; }
966
- @media (prefers-color-scheme: dark) { .verdict.ok { background:#10240f; color:#7fce85; } .verdict.warn { background:#2a0f0d; color:#ff8a80; } }
981
+ .verdict.watch { background:#fff4e5; color:#a65a00; }
982
+ .verdict.watch .verdict-meta { color:#a65a00; opacity:0.85; }
983
+ .verdict.soft { background:#e7f1ff; color:#1c5d99; }
984
+ .verdict.soft .verdict-meta { color:#1c5d99; opacity:0.85; }
985
+ @media (prefers-color-scheme: dark) { .verdict.ok { background:#10240f; color:#7fce85; } .verdict.warn { background:#2a0f0d; color:#ff8a80; } .verdict.watch { background:#2a2410; color:#ffd454; } .verdict.soft { background:#0f1d2a; color:#7fb6e8; } }
967
986
  .verdict .fleet-actions { margin:0.6rem 0 0; }
968
987
  .needs-you { border:1px solid #e5e5e5; border-radius:8px; padding:0.75rem 1rem; margin-bottom:1.25rem; }
969
988
  @media (prefers-color-scheme: dark) { .needs-you { border-color:#2a2a2a; background:#181818; } }
@@ -977,39 +996,66 @@ details.fleet-browse > summary, details.inbox > summary { cursor:pointer; font-w
977
996
  .dot { width:0.55rem; height:0.55rem; border-radius:50%; display:inline-block; flex:0 0 auto; }
978
997
  .dot.broken { background:#dc2626; }
979
998
  .dot.approval { background:#2563eb; }
980
- .dot.slipping { background:#f59e0b; }
999
+ .dot.watch { background:#f59e0b; }
981
1000
  `;
982
- function verdictBar(model, feedCount) {
1001
+ function needsYouCounts(feed) {
1002
+ let broken = 0;
1003
+ let watch = 0;
1004
+ let approval = 0;
1005
+ for (const i of feed) {
1006
+ if (i.group === "broken") broken++;
1007
+ else if (i.group === "watch") watch++;
1008
+ else approval++;
1009
+ }
1010
+ return { broken, watch, approval };
1011
+ }
1012
+ function verdictBar(model, feed) {
983
1013
  const auditedIso = fleetLastAuditedAt(model.cards);
984
- const audited = auditedIso ? ` \xB7 fleet last audited ${escapeHtml(relativeTimeFromNow(auditedIso))}` : "";
985
- const sites = model.cards.length;
986
- const sitesWord = `${sites} site${sites === 1 ? "" : "s"}`;
1014
+ const auditedTerm = auditedIso ? `fleet last audited ${escapeHtml(relativeTimeFromNow(auditedIso))}` : null;
1015
+ const total = model.cards.length;
1016
+ const { broken, watch, approval } = needsYouCounts(feed);
1017
+ const healthy = Math.max(0, total - (broken + watch + approval));
987
1018
  const actions = `<div class="fleet-actions">
988
1019
  <button type="button" class="refresh-fleet" data-refresh-url="/api/fleet/refresh">\u21BB Audit fleet</button>
989
1020
  <div id="rf-status" class="rf-status" aria-live="polite"></div>
990
1021
  </div>`;
991
- if (feedCount === 0) {
992
- return `<div class="verdict ok">
993
- <div class="verdict-line">\u2713 All clear</div>
994
- <div class="verdict-meta">${sitesWord} healthy${audited}</div>
995
- ${actions}
996
- </div>`;
997
- }
998
- const noun = feedCount === 1 ? "site needs" : "sites need";
999
- return `<div class="verdict warn">
1000
- <div class="verdict-line">\u26A0 ${feedCount} ${noun} you</div>
1001
- <div class="verdict-meta">${sitesWord}${audited}</div>
1022
+ const render = (cls, line, terms) => `<div class="verdict ${cls}">
1023
+ <div class="verdict-line">${line}</div>
1024
+ <div class="verdict-meta">${terms.filter(Boolean).join(" \xB7 ")}</div>
1002
1025
  ${actions}
1003
1026
  </div>`;
1027
+ if (broken === 0 && watch === 0 && approval === 0) {
1028
+ const sitesWord = `${total} site${total === 1 ? "" : "s"}`;
1029
+ return render("ok", "\u2713 All clear", [`${sitesWord} healthy`, auditedTerm]);
1030
+ }
1031
+ const watchTerm = watch > 0 ? `${watch} watching` : null;
1032
+ const approvalTerm = approval > 0 ? `${approval} waiting on you` : null;
1033
+ const healthyTerm = healthy > 0 ? `${healthy} healthy` : null;
1034
+ if (broken > 0) {
1035
+ return render("warn", `\u26A0 ${broken} site${broken === 1 ? "" : "s"} broken`, [
1036
+ watchTerm,
1037
+ approvalTerm,
1038
+ healthyTerm,
1039
+ auditedTerm
1040
+ ]);
1041
+ }
1042
+ if (watch > 0) {
1043
+ return render("watch", `${watch} site${watch === 1 ? "" : "s"} to watch`, [
1044
+ approvalTerm,
1045
+ healthyTerm,
1046
+ auditedTerm
1047
+ ]);
1048
+ }
1049
+ return render("soft", `${approval} waiting on you`, [healthyTerm, auditedTerm]);
1004
1050
  }
1005
1051
  var NEEDS_YOU_GROUP_LABEL = {
1006
1052
  broken: "Broken",
1007
- approval: "Waiting on your yes",
1008
- slipping: "Slipping"
1053
+ watch: "Watch",
1054
+ approval: "Waiting on your yes"
1009
1055
  };
1010
1056
  function renderNeedsYouFeed(feed) {
1011
1057
  if (feed.length === 0) return "";
1012
- const groups = ["broken", "approval", "slipping"];
1058
+ const groups = ["broken", "watch", "approval"];
1013
1059
  const blocks = groups.map((g) => {
1014
1060
  const rows = feed.filter((i) => i.group === g);
1015
1061
  if (rows.length === 0) return "";
@@ -1213,7 +1259,7 @@ function renderCockpitHtml(model) {
1213
1259
  <body>
1214
1260
  <h1>Reddoor fleet cockpit</h1>
1215
1261
  <div class="meta">${total} site${total === 1 ? "" : "s"} on the Reddoor stack.</div>
1216
- ${verdictBar(model, feed.length)}
1262
+ ${verdictBar(model, feed)}
1217
1263
  ${renderNeedsYouFeed(feed)}
1218
1264
  ${renderFleetBrowsePanel(model)}
1219
1265
  ${renderRecentlyLane(model)}