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