@reddoorla/maintenance 0.60.1 → 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 +262 -188
- 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; }
|
|
@@ -740,11 +906,8 @@ h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
|
|
|
740
906
|
.rf-sub { color:#999; font-size:0.8rem; margin-left:1.1rem; }
|
|
741
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; }
|
|
742
908
|
@keyframes rf-spin { to { transform:rotate(360deg); } }
|
|
743
|
-
details.
|
|
744
|
-
details.
|
|
745
|
-
.approve-strip { border:1px solid #ffe08a; background:#fff8e1; border-radius:8px; padding:0.75rem 1rem; margin-bottom:1.25rem; }
|
|
746
|
-
@media (prefers-color-scheme: dark) { .approve-strip { background:#241f00; border-color:#5a4d00; } }
|
|
747
|
-
.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; }
|
|
748
911
|
.approve-row { display:flex; flex-wrap:wrap; gap:0.5rem 1rem; align-items:center; padding:0.25rem 0; }
|
|
749
912
|
.pill { font-size:0.75rem; padding:0.1rem 0.5rem; border-radius:999px; font-weight:700; }
|
|
750
913
|
.pill.attention { background:#fdecea; color:#b00; }
|
|
@@ -756,85 +919,80 @@ details.tier > summary { cursor:pointer; font-weight:700; font-size:1.05rem; pad
|
|
|
756
919
|
.chip.critical { background:#fdecea; color:#b00; }
|
|
757
920
|
.chip.stuck { border:1px solid #b00; font-weight:600; }
|
|
758
921
|
.badge { font-weight:700; color:#C00; font-size:0.72rem; margin-right:0.25rem; }
|
|
759
|
-
.
|
|
760
|
-
|
|
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; }
|
|
761
944
|
`;
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
"vulns",
|
|
770
|
-
"lighthouse",
|
|
771
|
-
"delivery",
|
|
772
|
-
"prs",
|
|
773
|
-
"ci",
|
|
774
|
-
"auto-fix-failed",
|
|
775
|
-
"stale",
|
|
776
|
-
"no-domain",
|
|
777
|
-
"pending",
|
|
778
|
-
"submissions"
|
|
779
|
-
];
|
|
780
|
-
function summaryBar(model) {
|
|
781
|
-
const s = model.summary;
|
|
782
|
-
const heads = [
|
|
783
|
-
`${s.criticalHighVulns} critical/high vuln${s.criticalHighVulns === 1 ? "" : "s"}`,
|
|
784
|
-
`${s.lighthouseBelowFloor} Lighthouse<75`,
|
|
785
|
-
`${s.deliveryFailures} delivery`,
|
|
786
|
-
`${s.renovateFailing} PRs failing`,
|
|
787
|
-
`${s.ciRed} CI red`,
|
|
788
|
-
`${s.autoFixStuck} auto-fix stuck`,
|
|
789
|
-
`${s.pending} pending`,
|
|
790
|
-
`${s.newSubmissions ?? 0} new`
|
|
791
|
-
].join(" \xB7 ");
|
|
792
|
-
const chips2 = FILTERS.map(
|
|
793
|
-
(f) => `<button type="button" data-filter="${f}" aria-pressed="${f === "all" ? "true" : "false"}">${f}</button>`
|
|
794
|
-
).join("");
|
|
795
|
-
return `<div class="summary">
|
|
796
|
-
<span class="tier">\u{1F534} ${s.attention} needs attention</span>
|
|
797
|
-
<span class="tier">\u{1F7E1} ${s.watch} watch</span>
|
|
798
|
-
<span class="tier">\u{1F7E2} ${s.healthy} healthy</span>
|
|
799
|
-
</div>
|
|
800
|
-
<div class="summary heads">${escapeHtml(heads)}</div>
|
|
801
|
-
<div class="filters">${chips2}</div>
|
|
802
|
-
<div class="fleet-actions">
|
|
803
|
-
<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>
|
|
804
952
|
<div id="rf-status" class="rf-status" aria-live="polite"></div>
|
|
805
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>`;
|
|
806
967
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
function
|
|
813
|
-
if (
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
<a href="${href}">open \u25B8</a>
|
|
827
|
-
</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>`;
|
|
828
987
|
}).join("");
|
|
829
|
-
return `<section class="
|
|
830
|
-
<h2>Approve (${model.pending.length}) \u2014 your daily yes</h2>
|
|
831
|
-
${rows}
|
|
832
|
-
</section>`;
|
|
988
|
+
return `<section class="needs-you"><h2>Needs you (${feed.length})</h2>${blocks}</section>`;
|
|
833
989
|
}
|
|
834
990
|
var SUBMISSIONS_STRIP_CAP = 10;
|
|
835
|
-
function
|
|
991
|
+
function renderInboxLane(model) {
|
|
836
992
|
const subs = model.submissions ?? [];
|
|
837
|
-
|
|
993
|
+
const spam = model.spam;
|
|
994
|
+
const hasSpam = !!spam && (spam.caught > 0 || spam.through > 0);
|
|
995
|
+
if (subs.length === 0 && !hasSpam) return "";
|
|
838
996
|
const shown = [...subs].sort((a, b) => (b.submittedAt ?? "").localeCompare(a.submittedAt ?? "")).slice(0, SUBMISSIONS_STRIP_CAP);
|
|
839
997
|
const rows = shown.map((sub) => {
|
|
840
998
|
const href = `/s/${escapeHtml(sub.slug)}`;
|
|
@@ -848,91 +1006,17 @@ function submissionsStrip(model) {
|
|
|
848
1006
|
</div>`;
|
|
849
1007
|
}).join("");
|
|
850
1008
|
const overflow = subs.length - shown.length;
|
|
851
|
-
const more = overflow > 0 ? `<div class="approve-row subm-more muted"><a href="/submissions">+${overflow} more \u2014 view all submissions</a></div>` : ""
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
return n > 0 ? `<span class="chip">\u{1F4E5} ${n} new</span>` : "";
|
|
860
|
-
}
|
|
861
|
-
var PILL_LABEL = { attention: "failing", watch: "watch", healthy: "ok" };
|
|
862
|
-
function attentionBadge(status) {
|
|
863
|
-
if (status === "new") return `<span class="badge">NEW</span>`;
|
|
864
|
-
if (status === "worse") return `<span class="badge">WORSE</span>`;
|
|
865
|
-
return "";
|
|
866
|
-
}
|
|
867
|
-
function chips(c) {
|
|
868
|
-
const items = c.items.map((it) => {
|
|
869
|
-
const cls = it.autoFixExhausted ? "chip critical stuck" : it.severity === "critical" ? "chip critical" : "chip";
|
|
870
|
-
return `<span class="${cls}">${attentionBadge(it.status)}${escapeHtml(it.title)}</span>`;
|
|
871
|
-
});
|
|
872
|
-
for (const reason of c.watchReasons)
|
|
873
|
-
items.push(`<span class="chip">${escapeHtml(reason)}</span>`);
|
|
874
|
-
return items.length ? `<div class="chips">${items.join("")}</div>` : "";
|
|
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>`;
|
|
875
1017
|
}
|
|
876
|
-
|
|
877
|
-
const kinds = /* @__PURE__ */ new Set();
|
|
878
|
-
for (const it of c.items) {
|
|
879
|
-
kinds.add(it.kind === "vuln" ? "vulns" : it.kind === "renovate" ? "prs" : it.kind);
|
|
880
|
-
}
|
|
881
|
-
if (c.items.some((it) => it.autoFixExhausted)) kinds.add("auto-fix-failed");
|
|
882
|
-
for (const sig of c.watchSignals) kinds.add(sig);
|
|
883
|
-
return [...kinds].join(" ");
|
|
884
|
-
}
|
|
885
|
-
function triggerRenovateBtn(c) {
|
|
886
|
-
if (!c.site.gitRepo?.trim()) return "";
|
|
887
|
-
const url = `/api/sites/${escapeHtml(siteSlug(c.site.name))}/trigger-renovate`;
|
|
888
|
-
return `<button class="trigger-renovate" data-trigger-url="${url}">Trigger Renovate</button>`;
|
|
889
|
-
}
|
|
890
|
-
function cockpitCard(c) {
|
|
891
|
-
const base = card(c.site);
|
|
892
|
-
const pill = `<span class="pill ${c.tier}">${PILL_LABEL[c.tier]}</span>`;
|
|
893
|
-
const extra = `${pill}${chips(c)}${submBadge(c)}${triggerRenovateBtn(c)}`;
|
|
894
|
-
const opening = `<article class="card" data-signals="${signalsAttr(c)}">`;
|
|
895
|
-
return base.replace('<article class="card">', () => opening).replace("</article>", () => `${extra}</article>`);
|
|
896
|
-
}
|
|
897
|
-
var FILTER_SCRIPT = `<script>
|
|
1018
|
+
var AUDIT_SCRIPT = `<script>
|
|
898
1019
|
(function(){
|
|
899
|
-
var btns = document.querySelectorAll('.filters button');
|
|
900
|
-
var cards = document.querySelectorAll('.cards .card');
|
|
901
|
-
var details = document.querySelectorAll('details.tier');
|
|
902
|
-
btns.forEach(function(b){
|
|
903
|
-
b.addEventListener('click', function(){
|
|
904
|
-
var f = b.getAttribute('data-filter');
|
|
905
|
-
btns.forEach(function(x){ x.setAttribute('aria-pressed', x===b ? 'true':'false'); });
|
|
906
|
-
// "pending" lives on the approve strip, not on tier cards \u2014 just jump to it,
|
|
907
|
-
// never hide the triage cards (else the whole board blanks).
|
|
908
|
-
if (f === 'pending') { var s = document.querySelector('.approve-strip'); if (s) s.scrollIntoView({behavior:'smooth'}); return; }
|
|
909
|
-
if (f === 'submissions') { var ss = document.querySelector('[data-tier="submissions"]'); if (ss) ss.scrollIntoView({behavior:'smooth'}); return; }
|
|
910
|
-
if (f !== 'all') details.forEach(function(d){ d.open = true; });
|
|
911
|
-
cards.forEach(function(c){
|
|
912
|
-
var sig = (c.getAttribute('data-signals')||'').split(' ');
|
|
913
|
-
c.style.display = (f==='all' || sig.indexOf(f)!==-1) ? '' : 'none';
|
|
914
|
-
});
|
|
915
|
-
});
|
|
916
|
-
});
|
|
917
|
-
// approve buttons: mirror the per-site dashboard's inline POST.
|
|
918
|
-
document.querySelectorAll('button.approve').forEach(function(b){
|
|
919
|
-
b.addEventListener('click', async function(){
|
|
920
|
-
b.disabled = true; b.textContent = 'Approving\u2026';
|
|
921
|
-
try { var res = await fetch(b.dataset.approveUrl, { method: 'POST' });
|
|
922
|
-
b.textContent = res.ok ? 'Approved \u2713' : 'Failed'; }
|
|
923
|
-
catch(e){ b.textContent = 'Failed'; b.disabled = false; }
|
|
924
|
-
});
|
|
925
|
-
});
|
|
926
|
-
// trigger-renovate buttons: fire the on-demand dispatch (async, fire-and-forget).
|
|
927
|
-
document.querySelectorAll('button.trigger-renovate').forEach(function(b){
|
|
928
|
-
b.addEventListener('click', async function(){
|
|
929
|
-
b.disabled = true; b.textContent = 'Dispatching\u2026';
|
|
930
|
-
try { var res = await fetch(b.dataset.triggerUrl, { method: 'POST' });
|
|
931
|
-
b.textContent = res.ok ? 'Dispatched \u2713' : 'Failed';
|
|
932
|
-
if (!res.ok) b.disabled = false; }
|
|
933
|
-
catch(e){ b.textContent = 'Failed'; b.disabled = false; }
|
|
934
|
-
});
|
|
935
|
-
});
|
|
936
1020
|
// fleet-refresh live status: dispatch, then poll the actual runs and follow them.
|
|
937
1021
|
// Vanilla JS, string-concat only (no template literals) \u2014 this lives inside a TS
|
|
938
1022
|
// template string, so backticks or interpolation syntax would break the server render.
|
|
@@ -990,7 +1074,7 @@ var FILTER_SCRIPT = `<script>
|
|
|
990
1074
|
var p = rfPanel();
|
|
991
1075
|
if (data && data.authFail){
|
|
992
1076
|
if (p) p.innerHTML += '<div class="rf-row">Session expired \u2014 reload to sign in.</div>';
|
|
993
|
-
if (rf){ rf.disabled = false; rf.textContent = '\u21BB
|
|
1077
|
+
if (rf){ rf.disabled = false; rf.textContent = '\u21BB Audit fleet'; }
|
|
994
1078
|
rfStop(); return;
|
|
995
1079
|
}
|
|
996
1080
|
if (data && data.status){
|
|
@@ -1001,13 +1085,13 @@ var FILTER_SCRIPT = `<script>
|
|
|
1001
1085
|
rfStop(); setTimeout(function(){ location.reload(); }, 2000); return;
|
|
1002
1086
|
}
|
|
1003
1087
|
if (p) p.innerHTML += '<div class="rf-row"><button type="button" onclick="location.reload()">Reload</button></div>';
|
|
1004
|
-
if (rf){ rf.disabled = false; rf.textContent = '\u21BB
|
|
1088
|
+
if (rf){ rf.disabled = false; rf.textContent = '\u21BB Audit fleet'; }
|
|
1005
1089
|
rfStop(); return;
|
|
1006
1090
|
}
|
|
1007
1091
|
}
|
|
1008
1092
|
if (Date.now() - startedAt > RF_MAX_MS){
|
|
1009
1093
|
if (p) p.innerHTML += '<div class="rf-row">Still running \u2014 reload later.</div>';
|
|
1010
|
-
if (rf){ rf.disabled = false; rf.textContent = '\u21BB
|
|
1094
|
+
if (rf){ rf.disabled = false; rf.textContent = '\u21BB Audit fleet'; }
|
|
1011
1095
|
rfStop(); return;
|
|
1012
1096
|
}
|
|
1013
1097
|
setTimeout(function(){ rfPoll(since, startedAt); }, RF_POLL_MS);
|
|
@@ -1015,7 +1099,7 @@ var FILTER_SCRIPT = `<script>
|
|
|
1015
1099
|
var p = rfPanel();
|
|
1016
1100
|
if (Date.now() - startedAt > RF_MAX_MS){
|
|
1017
1101
|
if (p) p.innerHTML += '<div class="rf-row">Still running \u2014 reload later.</div>';
|
|
1018
|
-
if (rf){ rf.disabled = false; rf.textContent = '\u21BB
|
|
1102
|
+
if (rf){ rf.disabled = false; rf.textContent = '\u21BB Audit fleet'; }
|
|
1019
1103
|
rfStop(); return;
|
|
1020
1104
|
}
|
|
1021
1105
|
setTimeout(function(){ rfPoll(since, startedAt); }, RF_POLL_MS);
|
|
@@ -1029,12 +1113,12 @@ var FILTER_SCRIPT = `<script>
|
|
|
1029
1113
|
var rf = document.querySelector('button.refresh-fleet');
|
|
1030
1114
|
if (rf) rf.addEventListener('click', async function(){
|
|
1031
1115
|
if (!confirm('Kick off the security + Lighthouse sweeps for the whole fleet? They take a few minutes.')) return;
|
|
1032
|
-
rf.disabled = true; rf.textContent = '
|
|
1116
|
+
rf.disabled = true; rf.textContent = 'Auditing\u2026';
|
|
1033
1117
|
try {
|
|
1034
1118
|
var res = await fetch(rf.dataset.refreshUrl, { method: 'POST' });
|
|
1035
1119
|
if (res.ok){
|
|
1036
1120
|
var data = await res.json();
|
|
1037
|
-
rf.textContent = '\u21BB
|
|
1121
|
+
rf.textContent = '\u21BB Audit running\u2026';
|
|
1038
1122
|
if (data && data.since) rfBegin(data.since, Date.now());
|
|
1039
1123
|
} else { rf.textContent = 'Failed to start'; rf.disabled = false; }
|
|
1040
1124
|
} catch(e){ rf.textContent = 'Failed to start'; rf.disabled = false; }
|
|
@@ -1043,7 +1127,7 @@ var FILTER_SCRIPT = `<script>
|
|
|
1043
1127
|
try {
|
|
1044
1128
|
var rfSaved = JSON.parse(localStorage.getItem(RF_KEY) || 'null');
|
|
1045
1129
|
if (rfSaved && rfSaved.since && rfSaved.startedAt && (Date.now() - rfSaved.startedAt) < RF_MAX_MS){
|
|
1046
|
-
if (rf){ rf.disabled = true; rf.textContent = '\u21BB
|
|
1130
|
+
if (rf){ rf.disabled = true; rf.textContent = '\u21BB Audit running\u2026'; }
|
|
1047
1131
|
rfBegin(rfSaved.since, rfSaved.startedAt);
|
|
1048
1132
|
} else if (rfSaved) { rfStop(); }
|
|
1049
1133
|
} catch(e){}
|
|
@@ -1051,16 +1135,7 @@ var FILTER_SCRIPT = `<script>
|
|
|
1051
1135
|
</script>`;
|
|
1052
1136
|
function renderCockpitHtml(model) {
|
|
1053
1137
|
const total = model.cards.length;
|
|
1054
|
-
const
|
|
1055
|
-
const sections = tiers.map((tier) => {
|
|
1056
|
-
const cards = model.cards.filter((c) => c.tier === tier);
|
|
1057
|
-
const meta = TIER_META[tier];
|
|
1058
|
-
const body = cards.length === 0 ? `<div class="empty">None.</div>` : `<div class="cards">${cards.map(cockpitCard).join("")}</div>`;
|
|
1059
|
-
return `<details class="tier" data-tier="${tier}"${meta.open ? " open" : ""}>
|
|
1060
|
-
<summary>${meta.emoji} ${meta.label} (${cards.length})</summary>
|
|
1061
|
-
${body}
|
|
1062
|
-
</details>`;
|
|
1063
|
-
}).join("");
|
|
1138
|
+
const feed = buildNeedsYouFeed(model);
|
|
1064
1139
|
return `<!doctype html>
|
|
1065
1140
|
<html lang="en">
|
|
1066
1141
|
<head>
|
|
@@ -1073,13 +1148,12 @@ function renderCockpitHtml(model) {
|
|
|
1073
1148
|
<body>
|
|
1074
1149
|
<h1>Reddoor fleet cockpit</h1>
|
|
1075
1150
|
<div class="meta">${total} site${total === 1 ? "" : "s"} on the Reddoor stack.</div>
|
|
1076
|
-
${
|
|
1077
|
-
${
|
|
1078
|
-
${
|
|
1079
|
-
${
|
|
1080
|
-
${
|
|
1081
|
-
${
|
|
1082
|
-
${FILTER_SCRIPT}
|
|
1151
|
+
${verdictBar(model, feed.length)}
|
|
1152
|
+
${renderNeedsYouFeed(feed)}
|
|
1153
|
+
${renderFleetBrowsePanel(model)}
|
|
1154
|
+
${renderInboxLane(model)}
|
|
1155
|
+
${AUDIT_SCRIPT}
|
|
1156
|
+
${FLEET_BROWSE_SCRIPT}
|
|
1083
1157
|
</body>
|
|
1084
1158
|
</html>`;
|
|
1085
1159
|
}
|