@reddoorla/maintenance 0.12.1 → 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 +198 -48
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +196 -47
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.js +146 -70
- package/dist/index.js.map +1 -1
- package/dist/recipes/sync-configs.js +2 -1
- package/dist/recipes/sync-configs.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,39 +39,39 @@ function siteLabel(site) {
|
|
|
39
39
|
// src/configs/baseline-versions.ts
|
|
40
40
|
var baselineVersions = {
|
|
41
41
|
// SvelteKit core
|
|
42
|
-
svelte: "^5.55.
|
|
43
|
-
"@sveltejs/kit": "^2.
|
|
42
|
+
svelte: "^5.55.10",
|
|
43
|
+
"@sveltejs/kit": "^2.61.1",
|
|
44
44
|
"@sveltejs/adapter-netlify": "^6.0.4",
|
|
45
|
-
"@sveltejs/adapter-auto": "^7.0.
|
|
46
|
-
"@sveltejs/vite-plugin-svelte": "^7.
|
|
47
|
-
"svelte-check": "^4.4.
|
|
45
|
+
"@sveltejs/adapter-auto": "^7.0.1",
|
|
46
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
47
|
+
"svelte-check": "^4.4.8",
|
|
48
48
|
// Build tooling
|
|
49
|
-
vite: "^8.0.
|
|
50
|
-
vitest: "^4.1.
|
|
49
|
+
vite: "^8.0.14",
|
|
50
|
+
vitest: "^4.1.7",
|
|
51
51
|
typescript: "^6.0.3",
|
|
52
52
|
// Tailwind 4
|
|
53
|
-
tailwindcss: "^4.0
|
|
53
|
+
tailwindcss: "^4.3.0",
|
|
54
54
|
"@tailwindcss/vite": "^4.3.0",
|
|
55
55
|
// Prismic
|
|
56
|
-
"@prismicio/client": "^7.
|
|
57
|
-
"@prismicio/svelte": "^2.
|
|
58
|
-
"@slicemachine/adapter-sveltekit": "^0.3.
|
|
59
|
-
"slice-machine-ui": "^2.
|
|
56
|
+
"@prismicio/client": "^7.21.8",
|
|
57
|
+
"@prismicio/svelte": "^2.2.1",
|
|
58
|
+
"@slicemachine/adapter-sveltekit": "^0.3.96",
|
|
59
|
+
"slice-machine-ui": "^2.21.3",
|
|
60
60
|
// Test tooling
|
|
61
|
-
"@playwright/test": "^1.
|
|
61
|
+
"@playwright/test": "^1.60.0",
|
|
62
62
|
"@axe-core/playwright": "^4.11.3",
|
|
63
63
|
"@lhci/cli": "^0.15.1",
|
|
64
64
|
// Lint
|
|
65
|
-
eslint: "^10.
|
|
66
|
-
"eslint-plugin-svelte": "^3.
|
|
67
|
-
"eslint-config-prettier": "^10.1.
|
|
68
|
-
prettier: "^3.
|
|
69
|
-
"prettier-plugin-svelte": "^
|
|
70
|
-
"typescript-eslint": "^8.
|
|
65
|
+
eslint: "^10.4.0",
|
|
66
|
+
"eslint-plugin-svelte": "^3.18.0",
|
|
67
|
+
"eslint-config-prettier": "^10.1.8",
|
|
68
|
+
prettier: "^3.8.3",
|
|
69
|
+
"prettier-plugin-svelte": "^4.0.1",
|
|
70
|
+
"typescript-eslint": "^8.60.0",
|
|
71
71
|
"@eslint/js": "^10.0.1",
|
|
72
72
|
globals: "^17.6.0",
|
|
73
73
|
// Misc
|
|
74
|
-
"@lucide/svelte": "^1.
|
|
74
|
+
"@lucide/svelte": "^1.17.0",
|
|
75
75
|
"@zerodevx/svelte-img": "^2.1.2"
|
|
76
76
|
};
|
|
77
77
|
|
|
@@ -864,7 +864,8 @@ var CANONICAL_GITIGNORE_ENTRIES = [
|
|
|
864
864
|
".DS_Store",
|
|
865
865
|
"*.log",
|
|
866
866
|
".vercel/",
|
|
867
|
-
".netlify/"
|
|
867
|
+
".netlify/",
|
|
868
|
+
".reddoor-a11y/"
|
|
868
869
|
];
|
|
869
870
|
function stripLeadingSlash(s) {
|
|
870
871
|
return s.startsWith("/") ? s.slice(1) : s;
|
|
@@ -2217,6 +2218,13 @@ function mapRow(rec) {
|
|
|
2217
2218
|
bpScore: f["bpScore"] ?? null,
|
|
2218
2219
|
seoScore: f["seoScore"] ?? null,
|
|
2219
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,
|
|
2220
2228
|
dashboardToken: (() => {
|
|
2221
2229
|
const raw = f["Dashboard Token"];
|
|
2222
2230
|
if (typeof raw !== "string") return null;
|
|
@@ -2626,11 +2634,11 @@ async function uploadAttachment(recordId, fieldName, body, filename, contentType
|
|
|
2626
2634
|
}
|
|
2627
2635
|
|
|
2628
2636
|
// src/reports/draft.ts
|
|
2629
|
-
function scoresFromWebsite(
|
|
2630
|
-
const { pScore, rScore, bpScore, seoScore } =
|
|
2637
|
+
function scoresFromWebsite(siteRow) {
|
|
2638
|
+
const { pScore, rScore, bpScore, seoScore } = siteRow;
|
|
2631
2639
|
if (pScore === null || rScore === null || bpScore === null || seoScore === null) {
|
|
2632
2640
|
throw new Error(
|
|
2633
|
-
`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.`
|
|
2634
2642
|
);
|
|
2635
2643
|
}
|
|
2636
2644
|
return { performance: pScore, accessibility: rScore, bestPractices: bpScore, seo: seoScore };
|
|
@@ -2640,18 +2648,18 @@ function daysAgo(today, n) {
|
|
|
2640
2648
|
out.setDate(out.getDate() - n);
|
|
2641
2649
|
return out;
|
|
2642
2650
|
}
|
|
2643
|
-
async function draftReportForSite(base,
|
|
2644
|
-
const scores = scoresFromWebsite(
|
|
2651
|
+
async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
2652
|
+
const scores = scoresFromWebsite(siteRow);
|
|
2645
2653
|
const today = /* @__PURE__ */ new Date();
|
|
2646
|
-
const slug = siteSlug(
|
|
2647
|
-
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);
|
|
2648
2656
|
const periodEnd = today;
|
|
2649
2657
|
const completedOn = today;
|
|
2650
|
-
const lastTestedDate = reportType === "Maintenance" &&
|
|
2658
|
+
const lastTestedDate = reportType === "Maintenance" && siteRow.testingDay ? new Date(siteRow.testingDay) : null;
|
|
2651
2659
|
const cidName = `${slug}-header`;
|
|
2652
2660
|
const { html } = await renderReportHtml({
|
|
2653
|
-
siteName:
|
|
2654
|
-
siteUrl:
|
|
2661
|
+
siteName: siteRow.name,
|
|
2662
|
+
siteUrl: siteRow.url,
|
|
2655
2663
|
reportType,
|
|
2656
2664
|
completedOn,
|
|
2657
2665
|
lighthouse: scores,
|
|
@@ -2668,10 +2676,10 @@ async function draftReportForSite(base, siteRow2, reportType, options = {}) {
|
|
|
2668
2676
|
return { reportRow: null, htmlPath: path, html };
|
|
2669
2677
|
}
|
|
2670
2678
|
if (base === null) throw new Error("base required when previewOnly=false");
|
|
2671
|
-
const reportId = `${
|
|
2679
|
+
const reportId = `${siteRow.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
|
|
2672
2680
|
const created = await createDraft(base, {
|
|
2673
2681
|
reportId,
|
|
2674
|
-
siteId:
|
|
2682
|
+
siteId: siteRow.id,
|
|
2675
2683
|
reportType,
|
|
2676
2684
|
periodStart,
|
|
2677
2685
|
periodEnd,
|
|
@@ -2684,8 +2692,8 @@ async function draftReportForSite(base, siteRow2, reportType, options = {}) {
|
|
|
2684
2692
|
await setDraftReady(base, created.id, true);
|
|
2685
2693
|
return { reportRow: created, htmlPath: null, html };
|
|
2686
2694
|
}
|
|
2687
|
-
async function derivePeriodStart(base,
|
|
2688
|
-
const prior = await listReportsForSite(base,
|
|
2695
|
+
async function derivePeriodStart(base, siteRow, reportType, today) {
|
|
2696
|
+
const prior = await listReportsForSite(base, siteRow.id);
|
|
2689
2697
|
const sameType = prior.filter((r) => r.reportType === reportType && r.periodEnd).map((r) => r.periodEnd).sort();
|
|
2690
2698
|
const latest = sameType[sameType.length - 1];
|
|
2691
2699
|
return latest ? new Date(latest) : daysAgo(today, 30);
|
|
@@ -3018,6 +3026,40 @@ function verifyDashboardToken(provided, expected) {
|
|
|
3018
3026
|
return timingSafeEqual(Buffer.from(provided, "utf-8"), Buffer.from(expected, "utf-8"));
|
|
3019
3027
|
}
|
|
3020
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
|
+
|
|
3021
3063
|
// src/dashboard/fleet-render.ts
|
|
3022
3064
|
function escapeHtml2(s) {
|
|
3023
3065
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -3030,54 +3072,88 @@ function safeUrl2(raw) {
|
|
|
3030
3072
|
}
|
|
3031
3073
|
return "#";
|
|
3032
3074
|
}
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
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>`;
|
|
3036
3083
|
}
|
|
3037
|
-
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) {
|
|
3038
3100
|
const name = escapeHtml2(site.name);
|
|
3039
3101
|
const token = site.dashboardToken ?? "";
|
|
3040
3102
|
const href = `/s/${escapeHtml2(siteSlug(site.name))}?t=${escapeHtml2(token)}`;
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
<
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
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>`;
|
|
3052
3133
|
}
|
|
3053
3134
|
var STYLES2 = `
|
|
3054
3135
|
:root { color-scheme: light dark; }
|
|
3055
3136
|
body { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 1100px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }
|
|
3056
3137
|
@media (prefers-color-scheme: dark) { body { color: #e8e8e8; background: #111; } a { color: #6cb6ff; } }
|
|
3057
3138
|
h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
|
|
3058
|
-
.meta { color: #666; margin-bottom:
|
|
3059
|
-
table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
|
|
3060
|
-
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; }
|
|
3061
|
-
@media (prefers-color-scheme: dark) { th { border-color: #333; } }
|
|
3062
|
-
td { padding: 0.65rem 0.5rem; border-bottom: 1px solid #eee; }
|
|
3063
|
-
@media (prefers-color-scheme: dark) { td { border-color: #2a2a2a; } }
|
|
3064
|
-
td.site a { font-weight: 500; }
|
|
3065
|
-
td.url { color: #666; font-size: 0.85rem; }
|
|
3066
|
-
td.score { text-align: right; font-variant-numeric: tabular-nums; min-width: 3.5rem; }
|
|
3139
|
+
.meta { color: #666; margin-bottom: 1.5rem; }
|
|
3067
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"; }
|
|
3068
3154
|
`;
|
|
3069
3155
|
function renderFleetHomeHtml(sites) {
|
|
3070
|
-
const body = sites.length === 0 ? `<div class="empty">No sites to display.</div>` : `<
|
|
3071
|
-
<thead><tr>
|
|
3072
|
-
<th>Site</th>
|
|
3073
|
-
<th>URL</th>
|
|
3074
|
-
<th>Perf</th>
|
|
3075
|
-
<th>A11y</th>
|
|
3076
|
-
<th>BP</th>
|
|
3077
|
-
<th>SEO</th>
|
|
3078
|
-
</tr></thead>
|
|
3079
|
-
<tbody>${sites.map(siteRow).join("")}</tbody>
|
|
3080
|
-
</table>`;
|
|
3156
|
+
const body = sites.length === 0 ? `<div class="empty">No sites to display.</div>` : `<div class="cards">${sites.map(card).join("")}</div>`;
|
|
3081
3157
|
return `<!doctype html>
|
|
3082
3158
|
<html lang="en">
|
|
3083
3159
|
<head>
|