@reddoorla/maintenance 0.13.0 → 0.15.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/cli/bin.js +277 -50
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +273 -53
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.js +140 -63
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -753,25 +753,27 @@ var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
|
|
|
753
753
|
function timedSpawn(timeoutMs) {
|
|
754
754
|
return (cmd, args, opts = {}) => defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });
|
|
755
755
|
}
|
|
756
|
+
async function runOneAudit(site, name) {
|
|
757
|
+
if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);
|
|
758
|
+
const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
|
|
759
|
+
const label = site.name ?? site.path;
|
|
760
|
+
try {
|
|
761
|
+
return await REGISTRY[name]({ site, spawn: spawn2 });
|
|
762
|
+
} catch (err) {
|
|
763
|
+
return {
|
|
764
|
+
audit: name,
|
|
765
|
+
site: label,
|
|
766
|
+
status: "fail",
|
|
767
|
+
summary: `${name}: unexpected error \u2014 ${String(err)}`
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
}
|
|
756
771
|
async function runAudits(site, which) {
|
|
757
772
|
const names = which ?? ALL_AUDIT_NAMES;
|
|
758
773
|
for (const n of names) {
|
|
759
774
|
if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);
|
|
760
775
|
}
|
|
761
|
-
|
|
762
|
-
const label = site.name ?? site.path;
|
|
763
|
-
return Promise.all(
|
|
764
|
-
names.map(
|
|
765
|
-
(n) => REGISTRY[n]({ site, spawn: spawn2 }).catch(
|
|
766
|
-
(err) => ({
|
|
767
|
-
audit: n,
|
|
768
|
-
site: label,
|
|
769
|
-
status: "fail",
|
|
770
|
-
summary: `${n}: unexpected error \u2014 ${String(err)}`
|
|
771
|
-
})
|
|
772
|
-
)
|
|
773
|
-
)
|
|
774
|
-
);
|
|
776
|
+
return Promise.all(names.map((n) => runOneAudit(site, n)));
|
|
775
777
|
}
|
|
776
778
|
async function runAuditsAcross(sites, which) {
|
|
777
779
|
const all = await Promise.all(sites.map((s) => runAudits(s, which)));
|
|
@@ -2218,6 +2220,13 @@ function mapRow(rec) {
|
|
|
2218
2220
|
bpScore: f["bpScore"] ?? null,
|
|
2219
2221
|
seoScore: f["seoScore"] ?? null,
|
|
2220
2222
|
lastLighthouseAuditAt: f["Last lighthouse audit at"] ?? null,
|
|
2223
|
+
a11yViolations: f["A11y Violations"] ?? null,
|
|
2224
|
+
depsDrifted: f["Deps Drifted"] ?? null,
|
|
2225
|
+
depsMajorBehind: f["Deps Major Behind"] ?? null,
|
|
2226
|
+
securityVulnsCritical: f["Security Vulns Critical"] ?? null,
|
|
2227
|
+
securityVulnsHigh: f["Security Vulns High"] ?? null,
|
|
2228
|
+
securityVulnsModerate: f["Security Vulns Moderate"] ?? null,
|
|
2229
|
+
securityVulnsLow: f["Security Vulns Low"] ?? null,
|
|
2221
2230
|
dashboardToken: (() => {
|
|
2222
2231
|
const raw = f["Dashboard Token"];
|
|
2223
2232
|
if (typeof raw !== "string") return null;
|
|
@@ -2627,11 +2636,11 @@ async function uploadAttachment(recordId, fieldName, body, filename, contentType
|
|
|
2627
2636
|
}
|
|
2628
2637
|
|
|
2629
2638
|
// src/reports/draft.ts
|
|
2630
|
-
function scoresFromWebsite(
|
|
2631
|
-
const { pScore, rScore, bpScore, seoScore } =
|
|
2639
|
+
function scoresFromWebsite(siteRow) {
|
|
2640
|
+
const { pScore, rScore, bpScore, seoScore } = siteRow;
|
|
2632
2641
|
if (pScore === null || rScore === null || bpScore === null || seoScore === null) {
|
|
2633
2642
|
throw new Error(
|
|
2634
|
-
`Site '${
|
|
2643
|
+
`Site '${siteRow.name}' is missing one or more Lighthouse scores on the Websites row (pScore, rScore, bpScore, seoScore). Run 'reddoor-maint audit lighthouse' from the site's checkout and paste the four numbers into Airtable, then retry.`
|
|
2635
2644
|
);
|
|
2636
2645
|
}
|
|
2637
2646
|
return { performance: pScore, accessibility: rScore, bestPractices: bpScore, seo: seoScore };
|
|
@@ -2641,18 +2650,18 @@ function daysAgo(today, n) {
|
|
|
2641
2650
|
out.setDate(out.getDate() - n);
|
|
2642
2651
|
return out;
|
|
2643
2652
|
}
|
|
2644
|
-
async function draftReportForSite(base,
|
|
2645
|
-
const scores = scoresFromWebsite(
|
|
2653
|
+
async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
2654
|
+
const scores = scoresFromWebsite(siteRow);
|
|
2646
2655
|
const today = /* @__PURE__ */ new Date();
|
|
2647
|
-
const slug = siteSlug(
|
|
2648
|
-
const periodStart = base !== null ? await derivePeriodStart(base,
|
|
2656
|
+
const slug = siteSlug(siteRow.name);
|
|
2657
|
+
const periodStart = base !== null ? await derivePeriodStart(base, siteRow, reportType, today) : daysAgo(today, 30);
|
|
2649
2658
|
const periodEnd = today;
|
|
2650
2659
|
const completedOn = today;
|
|
2651
|
-
const lastTestedDate = reportType === "Maintenance" &&
|
|
2660
|
+
const lastTestedDate = reportType === "Maintenance" && siteRow.testingDay ? new Date(siteRow.testingDay) : null;
|
|
2652
2661
|
const cidName = `${slug}-header`;
|
|
2653
2662
|
const { html } = await renderReportHtml({
|
|
2654
|
-
siteName:
|
|
2655
|
-
siteUrl:
|
|
2663
|
+
siteName: siteRow.name,
|
|
2664
|
+
siteUrl: siteRow.url,
|
|
2656
2665
|
reportType,
|
|
2657
2666
|
completedOn,
|
|
2658
2667
|
lighthouse: scores,
|
|
@@ -2669,10 +2678,10 @@ async function draftReportForSite(base, siteRow2, reportType, options = {}) {
|
|
|
2669
2678
|
return { reportRow: null, htmlPath: path, html };
|
|
2670
2679
|
}
|
|
2671
2680
|
if (base === null) throw new Error("base required when previewOnly=false");
|
|
2672
|
-
const reportId = `${
|
|
2681
|
+
const reportId = `${siteRow.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
|
|
2673
2682
|
const created = await createDraft(base, {
|
|
2674
2683
|
reportId,
|
|
2675
|
-
siteId:
|
|
2684
|
+
siteId: siteRow.id,
|
|
2676
2685
|
reportType,
|
|
2677
2686
|
periodStart,
|
|
2678
2687
|
periodEnd,
|
|
@@ -2685,8 +2694,8 @@ async function draftReportForSite(base, siteRow2, reportType, options = {}) {
|
|
|
2685
2694
|
await setDraftReady(base, created.id, true);
|
|
2686
2695
|
return { reportRow: created, htmlPath: null, html };
|
|
2687
2696
|
}
|
|
2688
|
-
async function derivePeriodStart(base,
|
|
2689
|
-
const prior = await listReportsForSite(base,
|
|
2697
|
+
async function derivePeriodStart(base, siteRow, reportType, today) {
|
|
2698
|
+
const prior = await listReportsForSite(base, siteRow.id);
|
|
2690
2699
|
const sameType = prior.filter((r) => r.reportType === reportType && r.periodEnd).map((r) => r.periodEnd).sort();
|
|
2691
2700
|
const latest = sameType[sameType.length - 1];
|
|
2692
2701
|
return latest ? new Date(latest) : daysAgo(today, 30);
|
|
@@ -3019,6 +3028,40 @@ function verifyDashboardToken(provided, expected) {
|
|
|
3019
3028
|
return timingSafeEqual(Buffer.from(provided, "utf-8"), Buffer.from(expected, "utf-8"));
|
|
3020
3029
|
}
|
|
3021
3030
|
|
|
3031
|
+
// src/dashboard/onboarding.ts
|
|
3032
|
+
function isNonEmpty(s) {
|
|
3033
|
+
return typeof s === "string" && s.trim().length > 0;
|
|
3034
|
+
}
|
|
3035
|
+
function onboardingStatus(row) {
|
|
3036
|
+
const checks = {
|
|
3037
|
+
firstAudit: isNonEmpty(row.lastLighthouseAuditAt),
|
|
3038
|
+
recipients: isNonEmpty(row.reportRecipientsTo),
|
|
3039
|
+
schedule: row.maintenanceFreq !== "None",
|
|
3040
|
+
poc: isNonEmpty(row.pointOfContact)
|
|
3041
|
+
};
|
|
3042
|
+
const score = Object.values(checks).filter(Boolean).length;
|
|
3043
|
+
return { score, total: 4, checks };
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// src/dashboard/relative-time.ts
|
|
3047
|
+
function relativeTimeFromNow(iso, now = /* @__PURE__ */ new Date()) {
|
|
3048
|
+
if (!iso) return "\u2014";
|
|
3049
|
+
const t = Date.parse(iso);
|
|
3050
|
+
if (Number.isNaN(t)) return "\u2014";
|
|
3051
|
+
const seconds = Math.max(0, Math.floor((now.getTime() - t) / 1e3));
|
|
3052
|
+
if (seconds < 60) return "just now";
|
|
3053
|
+
const minutes = Math.floor(seconds / 60);
|
|
3054
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
3055
|
+
const hours = Math.floor(minutes / 60);
|
|
3056
|
+
if (hours < 24) return `${hours}h ago`;
|
|
3057
|
+
const days = Math.floor(hours / 24);
|
|
3058
|
+
if (days < 7) return `${days}d ago`;
|
|
3059
|
+
const weeks = Math.floor(days / 7);
|
|
3060
|
+
if (weeks < 4) return `${weeks}w ago`;
|
|
3061
|
+
const months = Math.floor(days / 30);
|
|
3062
|
+
return `${months}mo ago`;
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3022
3065
|
// src/dashboard/fleet-render.ts
|
|
3023
3066
|
function escapeHtml2(s) {
|
|
3024
3067
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -3031,54 +3074,88 @@ function safeUrl2(raw) {
|
|
|
3031
3074
|
}
|
|
3032
3075
|
return "#";
|
|
3033
3076
|
}
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3077
|
+
var DASH = "\u2014";
|
|
3078
|
+
function scoreSpan(category, value) {
|
|
3079
|
+
const display = value === null ? DASH : String(value);
|
|
3080
|
+
return `<span class="score ${category}">${escapeHtml2(display)}</span>`;
|
|
3081
|
+
}
|
|
3082
|
+
function a11ySpan(value) {
|
|
3083
|
+
const display = value === null ? DASH : String(value);
|
|
3084
|
+
return `<span class="metric a11y">${escapeHtml2(display)}</span>`;
|
|
3037
3085
|
}
|
|
3038
|
-
function
|
|
3086
|
+
function depsSpan(drifted, majorBehind) {
|
|
3087
|
+
if (drifted === null || majorBehind === null) {
|
|
3088
|
+
return `<span class="metric deps">${DASH}</span>`;
|
|
3089
|
+
}
|
|
3090
|
+
const display = drifted === 0 ? "0" : `${drifted} drifted (${majorBehind} major)`;
|
|
3091
|
+
return `<span class="metric deps">${escapeHtml2(display)}</span>`;
|
|
3092
|
+
}
|
|
3093
|
+
function securitySpan(critical, high, moderate, low) {
|
|
3094
|
+
if (critical === null || high === null || moderate === null || low === null) {
|
|
3095
|
+
return `<span class="metric sec">${DASH}</span>`;
|
|
3096
|
+
}
|
|
3097
|
+
const total = critical + high + moderate + low;
|
|
3098
|
+
const display = total === 0 ? "0" : `${critical}C/${high}H/${moderate}M/${low}L`;
|
|
3099
|
+
return `<span class="metric sec">${escapeHtml2(display)}</span>`;
|
|
3100
|
+
}
|
|
3101
|
+
function card(site) {
|
|
3039
3102
|
const name = escapeHtml2(site.name);
|
|
3040
3103
|
const token = site.dashboardToken ?? "";
|
|
3041
3104
|
const href = `/s/${escapeHtml2(siteSlug(site.name))}?t=${escapeHtml2(token)}`;
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
<
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3105
|
+
const onboarding = onboardingStatus(site);
|
|
3106
|
+
const audited = relativeTimeFromNow(site.lastLighthouseAuditAt);
|
|
3107
|
+
const safeSiteUrl = escapeHtml2(safeUrl2(site.url));
|
|
3108
|
+
const visibleUrl = escapeHtml2(site.url);
|
|
3109
|
+
return `<article class="card">
|
|
3110
|
+
<header class="card-head">
|
|
3111
|
+
<a class="site" href="${href}">${name}</a>
|
|
3112
|
+
<a class="url" href="${safeSiteUrl}" target="_blank" rel="noopener">${visibleUrl}</a>
|
|
3113
|
+
<span class="setup">Setup: <strong>${onboarding.score}/${onboarding.total}</strong></span>
|
|
3114
|
+
<span class="audited">Audited: <strong>${escapeHtml2(audited)}</strong></span>
|
|
3115
|
+
</header>
|
|
3116
|
+
<div class="card-metrics">
|
|
3117
|
+
<span class="cluster lighthouse">
|
|
3118
|
+
${scoreSpan("perf", site.pScore)}
|
|
3119
|
+
${scoreSpan("a11y-lh", site.rScore)}
|
|
3120
|
+
${scoreSpan("bp", site.bpScore)}
|
|
3121
|
+
${scoreSpan("seo", site.seoScore)}
|
|
3122
|
+
</span>
|
|
3123
|
+
<span class="cluster health">
|
|
3124
|
+
<span class="metric-label">a11y</span> ${a11ySpan(site.a11yViolations)}
|
|
3125
|
+
<span class="metric-label">deps</span> ${depsSpan(site.depsDrifted, site.depsMajorBehind)}
|
|
3126
|
+
<span class="metric-label">sec</span> ${securitySpan(
|
|
3127
|
+
site.securityVulnsCritical,
|
|
3128
|
+
site.securityVulnsHigh,
|
|
3129
|
+
site.securityVulnsModerate,
|
|
3130
|
+
site.securityVulnsLow
|
|
3131
|
+
)}
|
|
3132
|
+
</span>
|
|
3133
|
+
</div>
|
|
3134
|
+
</article>`;
|
|
3053
3135
|
}
|
|
3054
3136
|
var STYLES2 = `
|
|
3055
3137
|
:root { color-scheme: light dark; }
|
|
3056
3138
|
body { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 1100px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }
|
|
3057
3139
|
@media (prefers-color-scheme: dark) { body { color: #e8e8e8; background: #111; } a { color: #6cb6ff; } }
|
|
3058
3140
|
h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
|
|
3059
|
-
.meta { color: #666; margin-bottom:
|
|
3060
|
-
table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
|
|
3061
|
-
th { text-align: left; padding: 0.5rem; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: #666; border-bottom: 2px solid #ddd; }
|
|
3062
|
-
@media (prefers-color-scheme: dark) { th { border-color: #333; } }
|
|
3063
|
-
td { padding: 0.65rem 0.5rem; border-bottom: 1px solid #eee; }
|
|
3064
|
-
@media (prefers-color-scheme: dark) { td { border-color: #2a2a2a; } }
|
|
3065
|
-
td.site a { font-weight: 500; }
|
|
3066
|
-
td.url { color: #666; font-size: 0.85rem; }
|
|
3067
|
-
td.score { text-align: right; font-variant-numeric: tabular-nums; min-width: 3.5rem; }
|
|
3141
|
+
.meta { color: #666; margin-bottom: 1.5rem; }
|
|
3068
3142
|
.empty { color: #999; padding: 2rem; text-align: center; border: 1px dashed #ccc; border-radius: 6px; }
|
|
3143
|
+
.cards { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
3144
|
+
.card { border: 1px solid #e5e5e5; border-radius: 8px; padding: 0.9rem 1.1rem; }
|
|
3145
|
+
@media (prefers-color-scheme: dark) { .card { border-color: #2a2a2a; background: #181818; } }
|
|
3146
|
+
.card-head { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; align-items: baseline; }
|
|
3147
|
+
.card-head .site { font-weight: 600; font-size: 1.05rem; }
|
|
3148
|
+
.card-head .url { color: #666; font-size: 0.85rem; }
|
|
3149
|
+
.card-head .setup, .card-head .audited { color: #666; font-size: 0.85rem; }
|
|
3150
|
+
.card-head .setup { margin-left: auto; }
|
|
3151
|
+
.card-metrics { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; margin-top: 0.5rem; font-variant-numeric: tabular-nums; }
|
|
3152
|
+
.cluster { display: inline-flex; gap: 0.5rem; align-items: baseline; }
|
|
3153
|
+
.cluster.lighthouse .score { display: inline-block; min-width: 2.25rem; text-align: right; }
|
|
3154
|
+
.metric-label { color: #999; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
3155
|
+
.metric { font-feature-settings: "tnum"; }
|
|
3069
3156
|
`;
|
|
3070
3157
|
function renderFleetHomeHtml(sites) {
|
|
3071
|
-
const body = sites.length === 0 ? `<div class="empty">No sites to display.</div>` : `<
|
|
3072
|
-
<thead><tr>
|
|
3073
|
-
<th>Site</th>
|
|
3074
|
-
<th>URL</th>
|
|
3075
|
-
<th>Perf</th>
|
|
3076
|
-
<th>A11y</th>
|
|
3077
|
-
<th>BP</th>
|
|
3078
|
-
<th>SEO</th>
|
|
3079
|
-
</tr></thead>
|
|
3080
|
-
<tbody>${sites.map(siteRow).join("")}</tbody>
|
|
3081
|
-
</table>`;
|
|
3158
|
+
const body = sites.length === 0 ? `<div class="empty">No sites to display.</div>` : `<div class="cards">${sites.map(card).join("")}</div>`;
|
|
3082
3159
|
return `<!doctype html>
|
|
3083
3160
|
<html lang="en">
|
|
3084
3161
|
<head>
|