@reddoorla/maintenance 0.60.0 → 0.61.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.
- package/dist/chunk-VDBENQUA.js +211 -0
- package/dist/chunk-VDBENQUA.js.map +1 -0
- package/dist/cli/bin.js +1 -1
- package/dist/{digest-RVIOHUXR.js → digest-WAS3EJNC.js} +12 -196
- package/dist/digest-WAS3EJNC.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +300 -196
- package/dist/index.js.map +1 -1
- package/dist/{report-CN56TGMK.js → report-EPHXV2T2.js} +2 -2
- package/package.json +1 -1
- package/dist/digest-RVIOHUXR.js.map +0 -1
- /package/dist/{report-CN56TGMK.js.map → report-EPHXV2T2.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
selfCaretRange,
|
|
12
12
|
selfPackageVersion
|
|
13
13
|
} from "./chunk-ZYBOT5PM.js";
|
|
14
|
+
import "./chunk-VDBENQUA.js";
|
|
14
15
|
import {
|
|
15
16
|
findDueReports
|
|
16
17
|
} from "./chunk-WVRPOXKH.js";
|
|
@@ -636,7 +637,86 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
|
|
|
636
637
|
</html>`;
|
|
637
638
|
}
|
|
638
639
|
|
|
639
|
-
// src/dashboard/fleet-
|
|
640
|
+
// src/dashboard/fleet-cockpit.ts
|
|
641
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
642
|
+
var NEEDS_YOU_GROUP_RANK = { broken: 0, approval: 1, slipping: 2 };
|
|
643
|
+
function buildNeedsYouFeed(model) {
|
|
644
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
645
|
+
const get = (siteName) => {
|
|
646
|
+
const slug = siteSlug(siteName);
|
|
647
|
+
let a = bySlug.get(slug);
|
|
648
|
+
if (!a) {
|
|
649
|
+
a = {
|
|
650
|
+
slug,
|
|
651
|
+
siteName,
|
|
652
|
+
reasons: [],
|
|
653
|
+
hasCritical: false,
|
|
654
|
+
broken: false,
|
|
655
|
+
approval: false,
|
|
656
|
+
slipping: false
|
|
657
|
+
};
|
|
658
|
+
bySlug.set(slug, a);
|
|
659
|
+
}
|
|
660
|
+
return a;
|
|
661
|
+
};
|
|
662
|
+
for (const card2 of model.cards) {
|
|
663
|
+
if (card2.tier === "attention") {
|
|
664
|
+
for (const item of card2.items) {
|
|
665
|
+
if (item.kind === "vuln" && item.autoFixExhausted !== true) continue;
|
|
666
|
+
const a = get(card2.site.name);
|
|
667
|
+
a.reasons.push(item.title);
|
|
668
|
+
a.broken = true;
|
|
669
|
+
if (item.severity === "critical") a.hasCritical = true;
|
|
670
|
+
}
|
|
671
|
+
} else if (card2.tier === "watch" && card2.watchReasons.length > 0) {
|
|
672
|
+
const a = get(card2.site.name);
|
|
673
|
+
for (const r of card2.watchReasons) a.reasons.push(r);
|
|
674
|
+
a.slipping = true;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
for (const p of model.pending) {
|
|
678
|
+
const a = get(p.siteName);
|
|
679
|
+
a.reasons.push(`${p.reportType} ${p.period} ready`);
|
|
680
|
+
a.approval = true;
|
|
681
|
+
}
|
|
682
|
+
const items = [];
|
|
683
|
+
for (const a of bySlug.values()) {
|
|
684
|
+
if (a.reasons.length === 0) continue;
|
|
685
|
+
const group = a.broken ? "broken" : a.approval ? "approval" : "slipping";
|
|
686
|
+
items.push({
|
|
687
|
+
group,
|
|
688
|
+
hasCritical: a.hasCritical,
|
|
689
|
+
slug: a.slug,
|
|
690
|
+
siteName: a.siteName,
|
|
691
|
+
reasons: a.reasons,
|
|
692
|
+
url: `/s/${a.slug}`
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
items.sort((x, y) => {
|
|
696
|
+
if (NEEDS_YOU_GROUP_RANK[x.group] !== NEEDS_YOU_GROUP_RANK[y.group])
|
|
697
|
+
return NEEDS_YOU_GROUP_RANK[x.group] - NEEDS_YOU_GROUP_RANK[y.group];
|
|
698
|
+
if (x.group === "broken" && x.hasCritical !== y.hasCritical) return x.hasCritical ? -1 : 1;
|
|
699
|
+
return x.siteName.toLowerCase().localeCompare(y.siteName.toLowerCase());
|
|
700
|
+
});
|
|
701
|
+
return items;
|
|
702
|
+
}
|
|
703
|
+
function fleetLastAuditedAt(cards) {
|
|
704
|
+
let latestIso = null;
|
|
705
|
+
let latestMs = -Infinity;
|
|
706
|
+
for (const c of cards) {
|
|
707
|
+
const iso = c.site.lastLighthouseAuditAt;
|
|
708
|
+
if (!iso) continue;
|
|
709
|
+
const ms = Date.parse(iso);
|
|
710
|
+
if (!Number.isFinite(ms)) continue;
|
|
711
|
+
if (ms > latestMs) {
|
|
712
|
+
latestMs = ms;
|
|
713
|
+
latestIso = iso;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return latestIso;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/dashboard/fleet-browse-render.ts
|
|
640
720
|
var DASH2 = "\u2014";
|
|
641
721
|
function scoreSpan(category, value) {
|
|
642
722
|
const display = value === null ? DASH2 : String(value);
|
|
@@ -700,6 +780,96 @@ function card(site) {
|
|
|
700
780
|
</div>
|
|
701
781
|
</article>`;
|
|
702
782
|
}
|
|
783
|
+
function submBadge(c) {
|
|
784
|
+
const n = c.newSubmissions ?? 0;
|
|
785
|
+
return n > 0 ? `<span class="chip">\u{1F4E5} ${n} new</span>` : "";
|
|
786
|
+
}
|
|
787
|
+
var PILL_LABEL = { attention: "failing", watch: "watch", healthy: "ok" };
|
|
788
|
+
function attentionBadge(status) {
|
|
789
|
+
if (status === "new") return `<span class="badge">NEW</span>`;
|
|
790
|
+
if (status === "worse") return `<span class="badge">WORSE</span>`;
|
|
791
|
+
return "";
|
|
792
|
+
}
|
|
793
|
+
function chips(c) {
|
|
794
|
+
const items = c.items.map((it) => {
|
|
795
|
+
const cls = it.autoFixExhausted ? "chip critical stuck" : it.severity === "critical" ? "chip critical" : "chip";
|
|
796
|
+
return `<span class="${cls}">${attentionBadge(it.status)}${escapeHtml(it.title)}</span>`;
|
|
797
|
+
});
|
|
798
|
+
for (const reason of c.watchReasons)
|
|
799
|
+
items.push(`<span class="chip">${escapeHtml(reason)}</span>`);
|
|
800
|
+
return items.length ? `<div class="chips">${items.join("")}</div>` : "";
|
|
801
|
+
}
|
|
802
|
+
function signalsAttr(c) {
|
|
803
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
804
|
+
for (const it of c.items) {
|
|
805
|
+
kinds.add(it.kind === "vuln" ? "vulns" : it.kind === "renovate" ? "prs" : it.kind);
|
|
806
|
+
}
|
|
807
|
+
if (c.items.some((it) => it.autoFixExhausted)) kinds.add("auto-fix-failed");
|
|
808
|
+
for (const sig of c.watchSignals) kinds.add(sig);
|
|
809
|
+
return [...kinds].join(" ");
|
|
810
|
+
}
|
|
811
|
+
function triggerRenovateBtn(c) {
|
|
812
|
+
if (!c.site.gitRepo?.trim()) return "";
|
|
813
|
+
const url = `/api/sites/${escapeHtml(siteSlug(c.site.name))}/trigger-renovate`;
|
|
814
|
+
return `<button class="trigger-renovate" data-trigger-url="${url}">Trigger Renovate</button>`;
|
|
815
|
+
}
|
|
816
|
+
function cockpitCard(c) {
|
|
817
|
+
const base = card(c.site);
|
|
818
|
+
const pill = `<span class="pill ${c.tier}">${PILL_LABEL[c.tier]}</span>`;
|
|
819
|
+
const extra = `${pill}${chips(c)}${submBadge(c)}${triggerRenovateBtn(c)}`;
|
|
820
|
+
const opening = `<article class="card" data-signals="${signalsAttr(c)}">`;
|
|
821
|
+
return base.replace('<article class="card">', () => opening).replace("</article>", () => `${extra}</article>`);
|
|
822
|
+
}
|
|
823
|
+
var FLEET_FILTERS = [
|
|
824
|
+
"all",
|
|
825
|
+
"vulns",
|
|
826
|
+
"lighthouse",
|
|
827
|
+
"delivery",
|
|
828
|
+
"prs",
|
|
829
|
+
"ci",
|
|
830
|
+
"auto-fix-failed",
|
|
831
|
+
"stale",
|
|
832
|
+
"no-domain"
|
|
833
|
+
];
|
|
834
|
+
function renderFleetBrowsePanel(model) {
|
|
835
|
+
const total = model.cards.length;
|
|
836
|
+
const chips2 = FLEET_FILTERS.map(
|
|
837
|
+
(f) => `<button type="button" data-filter="${f}" aria-pressed="${f === "all" ? "true" : "false"}">${f}</button>`
|
|
838
|
+
).join("");
|
|
839
|
+
const body = total === 0 ? `<div class="empty">No sites on the fleet view yet.</div>` : `<div class="cards">${model.cards.map(cockpitCard).join("")}</div>`;
|
|
840
|
+
return `<details class="fleet-browse">
|
|
841
|
+
<summary>Fleet (${total})</summary>
|
|
842
|
+
<div class="filters">${chips2}</div>
|
|
843
|
+
${body}
|
|
844
|
+
</details>`;
|
|
845
|
+
}
|
|
846
|
+
var FLEET_BROWSE_SCRIPT = `<script>
|
|
847
|
+
(function(){
|
|
848
|
+
var btns = document.querySelectorAll('.fleet-browse .filters button');
|
|
849
|
+
var cards = document.querySelectorAll('.fleet-browse .cards .card');
|
|
850
|
+
btns.forEach(function(b){
|
|
851
|
+
b.addEventListener('click', function(){
|
|
852
|
+
var f = b.getAttribute('data-filter');
|
|
853
|
+
btns.forEach(function(x){ x.setAttribute('aria-pressed', x===b ? 'true':'false'); });
|
|
854
|
+
cards.forEach(function(c){
|
|
855
|
+
var sig = (c.getAttribute('data-signals')||'').split(' ');
|
|
856
|
+
c.style.display = (f==='all' || sig.indexOf(f)!==-1) ? '' : 'none';
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
document.querySelectorAll('.fleet-browse button.trigger-renovate').forEach(function(b){
|
|
861
|
+
b.addEventListener('click', async function(){
|
|
862
|
+
b.disabled = true; b.textContent = 'Dispatching\u2026';
|
|
863
|
+
try { var res = await fetch(b.dataset.triggerUrl, { method: 'POST' });
|
|
864
|
+
b.textContent = res.ok ? 'Dispatched \u2713' : 'Failed';
|
|
865
|
+
if (!res.ok) b.disabled = false; }
|
|
866
|
+
catch(e){ b.textContent = 'Failed'; b.disabled = false; }
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
})();
|
|
870
|
+
</script>`;
|
|
871
|
+
|
|
872
|
+
// src/dashboard/fleet-render.ts
|
|
703
873
|
var STYLES2 = `
|
|
704
874
|
:root { color-scheme: light dark; }
|
|
705
875
|
body { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 1100px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }
|
|
@@ -720,12 +890,8 @@ h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
|
|
|
720
890
|
.cluster.lighthouse .score { display: inline-block; min-width: 2.25rem; text-align: right; }
|
|
721
891
|
.metric-label { color: #999; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
722
892
|
.metric { font-feature-settings: "tnum"; }
|
|
723
|
-
.summary { display:flex; flex-wrap:wrap; gap:0.5rem 1.25rem; align-items:baseline; margin-bottom:0.5rem; }
|
|
724
|
-
.summary .tier { font-weight:700; }
|
|
725
|
-
.summary .heads { color:#666; font-size:0.9rem; }
|
|
726
893
|
.spam-rollup { font-size:0.9rem; margin-bottom:1rem; }
|
|
727
894
|
.muted { color:#999; }
|
|
728
|
-
.subm-viewall { font-size:0.8rem; font-weight:normal; margin-left:0.4rem; white-space:nowrap; }
|
|
729
895
|
.filters { display:flex; flex-wrap:wrap; gap:0.4rem; margin-bottom:1.25rem; }
|
|
730
896
|
.filters button { font:inherit; font-size:0.85rem; padding:0.25rem 0.7rem; border:1px solid #ccc; border-radius:999px; background:transparent; color:inherit; cursor:pointer; }
|
|
731
897
|
.filters button[aria-pressed="true"] { background:#1a1a1a; color:#fff; border-color:#1a1a1a; }
|
|
@@ -737,13 +903,11 @@ h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
|
|
|
737
903
|
.rf-status { margin-top:0.6rem; font-size:0.85rem; }
|
|
738
904
|
.rf-row { padding:0.1rem 0; }
|
|
739
905
|
.rf-row a { margin-left:0.3rem; }
|
|
906
|
+
.rf-sub { color:#999; font-size:0.8rem; margin-left:1.1rem; }
|
|
740
907
|
.rf-spin { display:inline-block; width:0.8em; height:0.8em; border:2px solid #999; border-top-color:transparent; border-radius:50%; animation:rf-spin 0.8s linear infinite; vertical-align:-0.1em; }
|
|
741
908
|
@keyframes rf-spin { to { transform:rotate(360deg); } }
|
|
742
|
-
details.
|
|
743
|
-
details.
|
|
744
|
-
.approve-strip { border:1px solid #ffe08a; background:#fff8e1; border-radius:8px; padding:0.75rem 1rem; margin-bottom:1.25rem; }
|
|
745
|
-
@media (prefers-color-scheme: dark) { .approve-strip { background:#241f00; border-color:#5a4d00; } }
|
|
746
|
-
.approve-strip h2 { font-size:1rem; margin:0 0 0.5rem; }
|
|
909
|
+
details.fleet-browse, details.inbox { margin:0.75rem 0; }
|
|
910
|
+
details.fleet-browse > summary, details.inbox > summary { cursor:pointer; font-weight:700; font-size:1.05rem; padding:0.35rem 0; list-style:none; }
|
|
747
911
|
.approve-row { display:flex; flex-wrap:wrap; gap:0.5rem 1rem; align-items:center; padding:0.25rem 0; }
|
|
748
912
|
.pill { font-size:0.75rem; padding:0.1rem 0.5rem; border-radius:999px; font-weight:700; }
|
|
749
913
|
.pill.attention { background:#fdecea; color:#b00; }
|
|
@@ -755,85 +919,80 @@ details.tier > summary { cursor:pointer; font-weight:700; font-size:1.05rem; pad
|
|
|
755
919
|
.chip.critical { background:#fdecea; color:#b00; }
|
|
756
920
|
.chip.stuck { border:1px solid #b00; font-weight:600; }
|
|
757
921
|
.badge { font-weight:700; color:#C00; font-size:0.72rem; margin-right:0.25rem; }
|
|
758
|
-
.
|
|
759
|
-
|
|
922
|
+
.verdict { border-radius:8px; padding:0.9rem 1.1rem; margin-bottom:1.25rem; }
|
|
923
|
+
.verdict .verdict-line { font-weight:800; font-size:1.4rem; }
|
|
924
|
+
.verdict .verdict-meta { color:#666; font-size:0.9rem; margin-top:0.2rem; }
|
|
925
|
+
.verdict.ok { background:#e8f5e9; color:#1b7a2f; }
|
|
926
|
+
.verdict.ok .verdict-meta { color:#2e7d32; }
|
|
927
|
+
.verdict.warn { background:#fdecea; color:#b00; }
|
|
928
|
+
.verdict.warn .verdict-meta { color:#b00; opacity:0.85; }
|
|
929
|
+
@media (prefers-color-scheme: dark) { .verdict.ok { background:#10240f; color:#7fce85; } .verdict.warn { background:#2a0f0d; color:#ff8a80; } }
|
|
930
|
+
.verdict .fleet-actions { margin:0.6rem 0 0; }
|
|
931
|
+
.needs-you { border:1px solid #e5e5e5; border-radius:8px; padding:0.75rem 1rem; margin-bottom:1.25rem; }
|
|
932
|
+
@media (prefers-color-scheme: dark) { .needs-you { border-color:#2a2a2a; background:#181818; } }
|
|
933
|
+
.needs-you h2 { font-size:1.05rem; margin:0 0 0.5rem; }
|
|
934
|
+
.feed-group-label { text-transform:uppercase; letter-spacing:0.04em; font-size:0.72rem; color:#999; margin:0.6rem 0 0.2rem; }
|
|
935
|
+
.feed-row { display:flex; gap:0.5rem; align-items:center; padding:0.3rem 0; border-bottom:1px dashed #eee; }
|
|
936
|
+
.feed-row:last-child { border-bottom:0; }
|
|
937
|
+
@media (prefers-color-scheme: dark) { .feed-row { border-bottom-color:#262626; } }
|
|
938
|
+
.feed-what { flex:1; }
|
|
939
|
+
.feed-open { white-space:nowrap; }
|
|
940
|
+
.dot { width:0.55rem; height:0.55rem; border-radius:50%; display:inline-block; flex:0 0 auto; }
|
|
941
|
+
.dot.broken { background:#dc2626; }
|
|
942
|
+
.dot.approval { background:#2563eb; }
|
|
943
|
+
.dot.slipping { background:#f59e0b; }
|
|
760
944
|
`;
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
"vulns",
|
|
769
|
-
"lighthouse",
|
|
770
|
-
"delivery",
|
|
771
|
-
"prs",
|
|
772
|
-
"ci",
|
|
773
|
-
"auto-fix-failed",
|
|
774
|
-
"stale",
|
|
775
|
-
"no-domain",
|
|
776
|
-
"pending",
|
|
777
|
-
"submissions"
|
|
778
|
-
];
|
|
779
|
-
function summaryBar(model) {
|
|
780
|
-
const s = model.summary;
|
|
781
|
-
const heads = [
|
|
782
|
-
`${s.criticalHighVulns} critical/high vuln${s.criticalHighVulns === 1 ? "" : "s"}`,
|
|
783
|
-
`${s.lighthouseBelowFloor} Lighthouse<75`,
|
|
784
|
-
`${s.deliveryFailures} delivery`,
|
|
785
|
-
`${s.renovateFailing} PRs failing`,
|
|
786
|
-
`${s.ciRed} CI red`,
|
|
787
|
-
`${s.autoFixStuck} auto-fix stuck`,
|
|
788
|
-
`${s.pending} pending`,
|
|
789
|
-
`${s.newSubmissions ?? 0} new`
|
|
790
|
-
].join(" \xB7 ");
|
|
791
|
-
const chips2 = FILTERS.map(
|
|
792
|
-
(f) => `<button type="button" data-filter="${f}" aria-pressed="${f === "all" ? "true" : "false"}">${f}</button>`
|
|
793
|
-
).join("");
|
|
794
|
-
return `<div class="summary">
|
|
795
|
-
<span class="tier">\u{1F534} ${s.attention} needs attention</span>
|
|
796
|
-
<span class="tier">\u{1F7E1} ${s.watch} watch</span>
|
|
797
|
-
<span class="tier">\u{1F7E2} ${s.healthy} healthy</span>
|
|
798
|
-
</div>
|
|
799
|
-
<div class="summary heads">${escapeHtml(heads)}</div>
|
|
800
|
-
<div class="filters">${chips2}</div>
|
|
801
|
-
<div class="fleet-actions">
|
|
802
|
-
<button type="button" class="refresh-fleet" data-refresh-url="/api/fleet/refresh">\u21BB Refresh fleet state</button>
|
|
945
|
+
function verdictBar(model, feedCount) {
|
|
946
|
+
const auditedIso = fleetLastAuditedAt(model.cards);
|
|
947
|
+
const audited = auditedIso ? ` \xB7 fleet last audited ${escapeHtml(relativeTimeFromNow(auditedIso))}` : "";
|
|
948
|
+
const sites = model.cards.length;
|
|
949
|
+
const sitesWord = `${sites} site${sites === 1 ? "" : "s"}`;
|
|
950
|
+
const actions = `<div class="fleet-actions">
|
|
951
|
+
<button type="button" class="refresh-fleet" data-refresh-url="/api/fleet/refresh">\u21BB Audit fleet</button>
|
|
803
952
|
<div id="rf-status" class="rf-status" aria-live="polite"></div>
|
|
804
953
|
</div>`;
|
|
954
|
+
if (feedCount === 0) {
|
|
955
|
+
return `<div class="verdict ok">
|
|
956
|
+
<div class="verdict-line">\u2713 All clear</div>
|
|
957
|
+
<div class="verdict-meta">${sitesWord} healthy${audited}</div>
|
|
958
|
+
${actions}
|
|
959
|
+
</div>`;
|
|
960
|
+
}
|
|
961
|
+
const noun = feedCount === 1 ? "site needs" : "sites need";
|
|
962
|
+
return `<div class="verdict warn">
|
|
963
|
+
<div class="verdict-line">\u26A0 ${feedCount} ${noun} you</div>
|
|
964
|
+
<div class="verdict-meta">${sitesWord}${audited}</div>
|
|
965
|
+
${actions}
|
|
966
|
+
</div>`;
|
|
805
967
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
function
|
|
812
|
-
if (
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
<a href="${href}">open \u25B8</a>
|
|
826
|
-
</div>`;
|
|
968
|
+
var NEEDS_YOU_GROUP_LABEL = {
|
|
969
|
+
broken: "Broken",
|
|
970
|
+
approval: "Waiting on your yes",
|
|
971
|
+
slipping: "Slipping"
|
|
972
|
+
};
|
|
973
|
+
function renderNeedsYouFeed(feed) {
|
|
974
|
+
if (feed.length === 0) return "";
|
|
975
|
+
const groups = ["broken", "approval", "slipping"];
|
|
976
|
+
const blocks = groups.map((g) => {
|
|
977
|
+
const rows = feed.filter((i) => i.group === g);
|
|
978
|
+
if (rows.length === 0) return "";
|
|
979
|
+
const lis = rows.map(
|
|
980
|
+
(i) => `<div class="feed-row" data-group="${g}">
|
|
981
|
+
<span class="dot ${g}"></span>
|
|
982
|
+
<span class="feed-what"><strong>${escapeHtml(i.siteName)}</strong> \u2014 ${escapeHtml(i.reasons.join(" \xB7 "))}</span>
|
|
983
|
+
<a class="feed-open" href="${escapeHtml(i.url)}">Open \u25B8</a>
|
|
984
|
+
</div>`
|
|
985
|
+
).join("");
|
|
986
|
+
return `<div class="feed-group"><div class="feed-group-label">${NEEDS_YOU_GROUP_LABEL[g]}</div>${lis}</div>`;
|
|
827
987
|
}).join("");
|
|
828
|
-
return `<section class="
|
|
829
|
-
<h2>Approve (${model.pending.length}) \u2014 your daily yes</h2>
|
|
830
|
-
${rows}
|
|
831
|
-
</section>`;
|
|
988
|
+
return `<section class="needs-you"><h2>Needs you (${feed.length})</h2>${blocks}</section>`;
|
|
832
989
|
}
|
|
833
990
|
var SUBMISSIONS_STRIP_CAP = 10;
|
|
834
|
-
function
|
|
991
|
+
function renderInboxLane(model) {
|
|
835
992
|
const subs = model.submissions ?? [];
|
|
836
|
-
|
|
993
|
+
const spam = model.spam;
|
|
994
|
+
const hasSpam = !!spam && (spam.caught > 0 || spam.through > 0);
|
|
995
|
+
if (subs.length === 0 && !hasSpam) return "";
|
|
837
996
|
const shown = [...subs].sort((a, b) => (b.submittedAt ?? "").localeCompare(a.submittedAt ?? "")).slice(0, SUBMISSIONS_STRIP_CAP);
|
|
838
997
|
const rows = shown.map((sub) => {
|
|
839
998
|
const href = `/s/${escapeHtml(sub.slug)}`;
|
|
@@ -847,91 +1006,17 @@ function submissionsStrip(model) {
|
|
|
847
1006
|
</div>`;
|
|
848
1007
|
}).join("");
|
|
849
1008
|
const overflow = subs.length - shown.length;
|
|
850
|
-
const more = overflow > 0 ? `<div class="approve-row subm-more muted"><a href="/submissions">+${overflow} more \u2014 view all submissions</a></div>` : ""
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
return n > 0 ? `<span class="chip">\u{1F4E5} ${n} new</span>` : "";
|
|
859
|
-
}
|
|
860
|
-
var PILL_LABEL = { attention: "failing", watch: "watch", healthy: "ok" };
|
|
861
|
-
function attentionBadge(status) {
|
|
862
|
-
if (status === "new") return `<span class="badge">NEW</span>`;
|
|
863
|
-
if (status === "worse") return `<span class="badge">WORSE</span>`;
|
|
864
|
-
return "";
|
|
1009
|
+
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>`;
|
|
1010
|
+
const spamLine = hasSpam ? `<div class="spam-rollup muted">\u{1F6E1} Spam (30d) \u2014 caught ${spam.caught} \xB7 through ${spam.through}</div>` : "";
|
|
1011
|
+
const spamInSummary = hasSpam ? " \xB7 \u{1F6E1} Spam (30d)" : "";
|
|
1012
|
+
return `<details class="inbox">
|
|
1013
|
+
<summary>\u{1F4E5} Submissions (${subs.length} new)${spamInSummary}</summary>
|
|
1014
|
+
${rows}${subs.length > 0 ? more : ""}
|
|
1015
|
+
${spamLine}
|
|
1016
|
+
</details>`;
|
|
865
1017
|
}
|
|
866
|
-
|
|
867
|
-
const items = c.items.map((it) => {
|
|
868
|
-
const cls = it.autoFixExhausted ? "chip critical stuck" : it.severity === "critical" ? "chip critical" : "chip";
|
|
869
|
-
return `<span class="${cls}">${attentionBadge(it.status)}${escapeHtml(it.title)}</span>`;
|
|
870
|
-
});
|
|
871
|
-
for (const reason of c.watchReasons)
|
|
872
|
-
items.push(`<span class="chip">${escapeHtml(reason)}</span>`);
|
|
873
|
-
return items.length ? `<div class="chips">${items.join("")}</div>` : "";
|
|
874
|
-
}
|
|
875
|
-
function signalsAttr(c) {
|
|
876
|
-
const kinds = /* @__PURE__ */ new Set();
|
|
877
|
-
for (const it of c.items) {
|
|
878
|
-
kinds.add(it.kind === "vuln" ? "vulns" : it.kind === "renovate" ? "prs" : it.kind);
|
|
879
|
-
}
|
|
880
|
-
if (c.items.some((it) => it.autoFixExhausted)) kinds.add("auto-fix-failed");
|
|
881
|
-
for (const sig of c.watchSignals) kinds.add(sig);
|
|
882
|
-
return [...kinds].join(" ");
|
|
883
|
-
}
|
|
884
|
-
function triggerRenovateBtn(c) {
|
|
885
|
-
if (!c.site.gitRepo?.trim()) return "";
|
|
886
|
-
const url = `/api/sites/${escapeHtml(siteSlug(c.site.name))}/trigger-renovate`;
|
|
887
|
-
return `<button class="trigger-renovate" data-trigger-url="${url}">Trigger Renovate</button>`;
|
|
888
|
-
}
|
|
889
|
-
function cockpitCard(c) {
|
|
890
|
-
const base = card(c.site);
|
|
891
|
-
const pill = `<span class="pill ${c.tier}">${PILL_LABEL[c.tier]}</span>`;
|
|
892
|
-
const extra = `${pill}${chips(c)}${submBadge(c)}${triggerRenovateBtn(c)}`;
|
|
893
|
-
const opening = `<article class="card" data-signals="${signalsAttr(c)}">`;
|
|
894
|
-
return base.replace('<article class="card">', () => opening).replace("</article>", () => `${extra}</article>`);
|
|
895
|
-
}
|
|
896
|
-
var FILTER_SCRIPT = `<script>
|
|
1018
|
+
var AUDIT_SCRIPT = `<script>
|
|
897
1019
|
(function(){
|
|
898
|
-
var btns = document.querySelectorAll('.filters button');
|
|
899
|
-
var cards = document.querySelectorAll('.cards .card');
|
|
900
|
-
var details = document.querySelectorAll('details.tier');
|
|
901
|
-
btns.forEach(function(b){
|
|
902
|
-
b.addEventListener('click', function(){
|
|
903
|
-
var f = b.getAttribute('data-filter');
|
|
904
|
-
btns.forEach(function(x){ x.setAttribute('aria-pressed', x===b ? 'true':'false'); });
|
|
905
|
-
// "pending" lives on the approve strip, not on tier cards \u2014 just jump to it,
|
|
906
|
-
// never hide the triage cards (else the whole board blanks).
|
|
907
|
-
if (f === 'pending') { var s = document.querySelector('.approve-strip'); if (s) s.scrollIntoView({behavior:'smooth'}); return; }
|
|
908
|
-
if (f === 'submissions') { var ss = document.querySelector('[data-tier="submissions"]'); if (ss) ss.scrollIntoView({behavior:'smooth'}); return; }
|
|
909
|
-
if (f !== 'all') details.forEach(function(d){ d.open = true; });
|
|
910
|
-
cards.forEach(function(c){
|
|
911
|
-
var sig = (c.getAttribute('data-signals')||'').split(' ');
|
|
912
|
-
c.style.display = (f==='all' || sig.indexOf(f)!==-1) ? '' : 'none';
|
|
913
|
-
});
|
|
914
|
-
});
|
|
915
|
-
});
|
|
916
|
-
// approve buttons: mirror the per-site dashboard's inline POST.
|
|
917
|
-
document.querySelectorAll('button.approve').forEach(function(b){
|
|
918
|
-
b.addEventListener('click', async function(){
|
|
919
|
-
b.disabled = true; b.textContent = 'Approving\u2026';
|
|
920
|
-
try { var res = await fetch(b.dataset.approveUrl, { method: 'POST' });
|
|
921
|
-
b.textContent = res.ok ? 'Approved \u2713' : 'Failed'; }
|
|
922
|
-
catch(e){ b.textContent = 'Failed'; b.disabled = false; }
|
|
923
|
-
});
|
|
924
|
-
});
|
|
925
|
-
// trigger-renovate buttons: fire the on-demand dispatch (async, fire-and-forget).
|
|
926
|
-
document.querySelectorAll('button.trigger-renovate').forEach(function(b){
|
|
927
|
-
b.addEventListener('click', async function(){
|
|
928
|
-
b.disabled = true; b.textContent = 'Dispatching\u2026';
|
|
929
|
-
try { var res = await fetch(b.dataset.triggerUrl, { method: 'POST' });
|
|
930
|
-
b.textContent = res.ok ? 'Dispatched \u2713' : 'Failed';
|
|
931
|
-
if (!res.ok) b.disabled = false; }
|
|
932
|
-
catch(e){ b.textContent = 'Failed'; b.disabled = false; }
|
|
933
|
-
});
|
|
934
|
-
});
|
|
935
1020
|
// fleet-refresh live status: dispatch, then poll the actual runs and follow them.
|
|
936
1021
|
// Vanilla JS, string-concat only (no template literals) \u2014 this lives inside a TS
|
|
937
1022
|
// template string, so backticks or interpolation syntax would break the server render.
|
|
@@ -940,16 +1025,45 @@ var FILTER_SCRIPT = `<script>
|
|
|
940
1025
|
var RF_MAX_MS = 90 * 60 * 1000; // safety ceiling; a full fleet Lighthouse run was ~48 min (2026-06-24)
|
|
941
1026
|
function rfPanel(){ return document.getElementById('rf-status'); }
|
|
942
1027
|
function rfStop(){ try { localStorage.removeItem(RF_KEY); } catch(e){} }
|
|
943
|
-
//
|
|
944
|
-
|
|
945
|
-
//
|
|
946
|
-
|
|
1028
|
+
// Per-workflow ETA hint (the lighthouse fleet sweep runs ~48 min; security ~1-2 min).
|
|
1029
|
+
function rfEta(label){ return label === 'lighthouse' ? '~48m' : label === 'security' ? '~2m' : ''; }
|
|
1030
|
+
// Map a raw GitHub step name to a coarse human phase. Order matters (the audit step
|
|
1031
|
+
// name also contains 'browser'; 'playwright install' also contains 'install').
|
|
1032
|
+
function rfPhase(step){
|
|
1033
|
+
if (!step) return '';
|
|
1034
|
+
var s = step.toLowerCase();
|
|
1035
|
+
if (s.indexOf('audit') !== -1 || s.indexOf('lighthouse') !== -1) return 'auditing the fleet\u2026';
|
|
1036
|
+
if (s.indexOf('build') !== -1) return 'building\u2026';
|
|
1037
|
+
if (s.indexOf('playwright') !== -1 || s.indexOf('browser') !== -1) return 'installing browsers\u2026';
|
|
1038
|
+
if (s.indexOf('install') !== -1 || s.indexOf('depend') !== -1) return 'installing dependencies\u2026';
|
|
1039
|
+
if (s.indexOf('set up') !== -1 || s.indexOf('checkout') !== -1) return 'setting up\u2026';
|
|
1040
|
+
return step;
|
|
1041
|
+
}
|
|
1042
|
+
// Safe to build raw HTML: workflow/state/step are server-fixed (enums + GitHub step
|
|
1043
|
+
// names) and url is GitHub's own html_url for our central repo \u2014 none are
|
|
1044
|
+
// user-supplied. Don't interpolate untrusted fields here without escaping.
|
|
1045
|
+
function rfRender(status, startedAt){
|
|
947
1046
|
var failed = function(s){ return s === 'failure' || s === 'cancelled' || s === 'timed_out'; };
|
|
1047
|
+
var mins = Math.floor((Date.now() - startedAt) / 60000);
|
|
1048
|
+
var elapsed = mins < 1 ? '<1m' : mins + 'm';
|
|
948
1049
|
return status.perWorkflow.map(function(w){
|
|
949
1050
|
var label = w.workflow.replace('.yml','').replace('fleet-','');
|
|
950
|
-
var
|
|
951
|
-
var
|
|
952
|
-
|
|
1051
|
+
var done = w.state === 'success';
|
|
1052
|
+
var isFailed = failed(w.state);
|
|
1053
|
+
var icon = done ? '\u2713' : isFailed ? '\u2717' : '<span class="rf-spin"></span>';
|
|
1054
|
+
var line = icon + ' ' + label + ' \u2014 ' + w.state.replace('_',' ');
|
|
1055
|
+
if (!done && !isFailed){
|
|
1056
|
+
var phase = rfPhase(w.step);
|
|
1057
|
+
var eta = rfEta(label);
|
|
1058
|
+
var detail = [];
|
|
1059
|
+
if (phase) detail.push(phase);
|
|
1060
|
+
detail.push(elapsed + (eta ? ' / ' + eta : ''));
|
|
1061
|
+
var link = w.url ? ' \xB7 <a href="'+w.url+'" target="_blank" rel="noopener">view run \u2197</a>' : '';
|
|
1062
|
+
line += '<div class="rf-sub">' + detail.join(' \xB7 ') + link + '</div>';
|
|
1063
|
+
} else if (isFailed && w.url){
|
|
1064
|
+
line += ' <a href="'+w.url+'" target="_blank" rel="noopener">run</a>';
|
|
1065
|
+
}
|
|
1066
|
+
return '<div class="rf-row">' + line + '</div>';
|
|
953
1067
|
}).join('');
|
|
954
1068
|
}
|
|
955
1069
|
function rfPoll(since, startedAt){
|
|
@@ -960,24 +1074,24 @@ var FILTER_SCRIPT = `<script>
|
|
|
960
1074
|
var p = rfPanel();
|
|
961
1075
|
if (data && data.authFail){
|
|
962
1076
|
if (p) p.innerHTML += '<div class="rf-row">Session expired \u2014 reload to sign in.</div>';
|
|
963
|
-
if (rf){ rf.disabled = false; rf.textContent = '\u21BB
|
|
1077
|
+
if (rf){ rf.disabled = false; rf.textContent = '\u21BB Audit fleet'; }
|
|
964
1078
|
rfStop(); return;
|
|
965
1079
|
}
|
|
966
1080
|
if (data && data.status){
|
|
967
|
-
if (p) p.innerHTML = rfRender(data.status);
|
|
1081
|
+
if (p) p.innerHTML = rfRender(data.status, startedAt);
|
|
968
1082
|
if (data.status.allDone){
|
|
969
1083
|
if (!data.status.anyFailure){
|
|
970
1084
|
if (p) p.innerHTML += '<div class="rf-row">\u2713 Done \u2014 reloading\u2026</div>';
|
|
971
1085
|
rfStop(); setTimeout(function(){ location.reload(); }, 2000); return;
|
|
972
1086
|
}
|
|
973
1087
|
if (p) p.innerHTML += '<div class="rf-row"><button type="button" onclick="location.reload()">Reload</button></div>';
|
|
974
|
-
if (rf){ rf.disabled = false; rf.textContent = '\u21BB
|
|
1088
|
+
if (rf){ rf.disabled = false; rf.textContent = '\u21BB Audit fleet'; }
|
|
975
1089
|
rfStop(); return;
|
|
976
1090
|
}
|
|
977
1091
|
}
|
|
978
1092
|
if (Date.now() - startedAt > RF_MAX_MS){
|
|
979
1093
|
if (p) p.innerHTML += '<div class="rf-row">Still running \u2014 reload later.</div>';
|
|
980
|
-
if (rf){ rf.disabled = false; rf.textContent = '\u21BB
|
|
1094
|
+
if (rf){ rf.disabled = false; rf.textContent = '\u21BB Audit fleet'; }
|
|
981
1095
|
rfStop(); return;
|
|
982
1096
|
}
|
|
983
1097
|
setTimeout(function(){ rfPoll(since, startedAt); }, RF_POLL_MS);
|
|
@@ -985,7 +1099,7 @@ var FILTER_SCRIPT = `<script>
|
|
|
985
1099
|
var p = rfPanel();
|
|
986
1100
|
if (Date.now() - startedAt > RF_MAX_MS){
|
|
987
1101
|
if (p) p.innerHTML += '<div class="rf-row">Still running \u2014 reload later.</div>';
|
|
988
|
-
if (rf){ rf.disabled = false; rf.textContent = '\u21BB
|
|
1102
|
+
if (rf){ rf.disabled = false; rf.textContent = '\u21BB Audit fleet'; }
|
|
989
1103
|
rfStop(); return;
|
|
990
1104
|
}
|
|
991
1105
|
setTimeout(function(){ rfPoll(since, startedAt); }, RF_POLL_MS);
|
|
@@ -999,12 +1113,12 @@ var FILTER_SCRIPT = `<script>
|
|
|
999
1113
|
var rf = document.querySelector('button.refresh-fleet');
|
|
1000
1114
|
if (rf) rf.addEventListener('click', async function(){
|
|
1001
1115
|
if (!confirm('Kick off the security + Lighthouse sweeps for the whole fleet? They take a few minutes.')) return;
|
|
1002
|
-
rf.disabled = true; rf.textContent = '
|
|
1116
|
+
rf.disabled = true; rf.textContent = 'Auditing\u2026';
|
|
1003
1117
|
try {
|
|
1004
1118
|
var res = await fetch(rf.dataset.refreshUrl, { method: 'POST' });
|
|
1005
1119
|
if (res.ok){
|
|
1006
1120
|
var data = await res.json();
|
|
1007
|
-
rf.textContent = '\u21BB
|
|
1121
|
+
rf.textContent = '\u21BB Audit running\u2026';
|
|
1008
1122
|
if (data && data.since) rfBegin(data.since, Date.now());
|
|
1009
1123
|
} else { rf.textContent = 'Failed to start'; rf.disabled = false; }
|
|
1010
1124
|
} catch(e){ rf.textContent = 'Failed to start'; rf.disabled = false; }
|
|
@@ -1013,7 +1127,7 @@ var FILTER_SCRIPT = `<script>
|
|
|
1013
1127
|
try {
|
|
1014
1128
|
var rfSaved = JSON.parse(localStorage.getItem(RF_KEY) || 'null');
|
|
1015
1129
|
if (rfSaved && rfSaved.since && rfSaved.startedAt && (Date.now() - rfSaved.startedAt) < RF_MAX_MS){
|
|
1016
|
-
if (rf){ rf.disabled = true; rf.textContent = '\u21BB
|
|
1130
|
+
if (rf){ rf.disabled = true; rf.textContent = '\u21BB Audit running\u2026'; }
|
|
1017
1131
|
rfBegin(rfSaved.since, rfSaved.startedAt);
|
|
1018
1132
|
} else if (rfSaved) { rfStop(); }
|
|
1019
1133
|
} catch(e){}
|
|
@@ -1021,16 +1135,7 @@ var FILTER_SCRIPT = `<script>
|
|
|
1021
1135
|
</script>`;
|
|
1022
1136
|
function renderCockpitHtml(model) {
|
|
1023
1137
|
const total = model.cards.length;
|
|
1024
|
-
const
|
|
1025
|
-
const sections = tiers.map((tier) => {
|
|
1026
|
-
const cards = model.cards.filter((c) => c.tier === tier);
|
|
1027
|
-
const meta = TIER_META[tier];
|
|
1028
|
-
const body = cards.length === 0 ? `<div class="empty">None.</div>` : `<div class="cards">${cards.map(cockpitCard).join("")}</div>`;
|
|
1029
|
-
return `<details class="tier" data-tier="${tier}"${meta.open ? " open" : ""}>
|
|
1030
|
-
<summary>${meta.emoji} ${meta.label} (${cards.length})</summary>
|
|
1031
|
-
${body}
|
|
1032
|
-
</details>`;
|
|
1033
|
-
}).join("");
|
|
1138
|
+
const feed = buildNeedsYouFeed(model);
|
|
1034
1139
|
return `<!doctype html>
|
|
1035
1140
|
<html lang="en">
|
|
1036
1141
|
<head>
|
|
@@ -1043,13 +1148,12 @@ function renderCockpitHtml(model) {
|
|
|
1043
1148
|
<body>
|
|
1044
1149
|
<h1>Reddoor fleet cockpit</h1>
|
|
1045
1150
|
<div class="meta">${total} site${total === 1 ? "" : "s"} on the Reddoor stack.</div>
|
|
1046
|
-
${
|
|
1047
|
-
${
|
|
1048
|
-
${
|
|
1049
|
-
${
|
|
1050
|
-
${
|
|
1051
|
-
${
|
|
1052
|
-
${FILTER_SCRIPT}
|
|
1151
|
+
${verdictBar(model, feed.length)}
|
|
1152
|
+
${renderNeedsYouFeed(feed)}
|
|
1153
|
+
${renderFleetBrowsePanel(model)}
|
|
1154
|
+
${renderInboxLane(model)}
|
|
1155
|
+
${AUDIT_SCRIPT}
|
|
1156
|
+
${FLEET_BROWSE_SCRIPT}
|
|
1053
1157
|
</body>
|
|
1054
1158
|
</html>`;
|
|
1055
1159
|
}
|