@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/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.5",
43
- "@sveltejs/kit": "^2.59.0",
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.0",
46
- "@sveltejs/vite-plugin-svelte": "^7.0.0",
47
- "svelte-check": "^4.4.7",
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.10",
50
- vitest: "^4.1.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.14",
53
+ tailwindcss: "^4.3.0",
54
54
  "@tailwindcss/vite": "^4.3.0",
55
55
  // Prismic
56
- "@prismicio/client": "^7.3.1",
57
- "@prismicio/svelte": "^2.0.0",
58
- "@slicemachine/adapter-sveltekit": "^0.3.36",
59
- "slice-machine-ui": "^2.11.1",
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.59.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.3.0",
66
- "eslint-plugin-svelte": "^3.1.0",
67
- "eslint-config-prettier": "^10.1.1",
68
- prettier: "^3.1.1",
69
- "prettier-plugin-svelte": "^3.2.6",
70
- "typescript-eslint": "^8.59.1",
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.14.0",
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(siteRow2) {
2630
- const { pScore, rScore, bpScore, seoScore } = siteRow2;
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 '${siteRow2.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.`
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, siteRow2, reportType, options = {}) {
2644
- const scores = scoresFromWebsite(siteRow2);
2651
+ async function draftReportForSite(base, siteRow, reportType, options = {}) {
2652
+ const scores = scoresFromWebsite(siteRow);
2645
2653
  const today = /* @__PURE__ */ new Date();
2646
- const slug = siteSlug(siteRow2.name);
2647
- const periodStart = base !== null ? await derivePeriodStart(base, siteRow2, reportType, today) : daysAgo(today, 30);
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" && siteRow2.testingDay ? new Date(siteRow2.testingDay) : null;
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: siteRow2.name,
2654
- siteUrl: siteRow2.url,
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 = `${siteRow2.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
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: siteRow2.id,
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, siteRow2, reportType, today) {
2688
- const prior = await listReportsForSite(base, siteRow2.id);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -3030,54 +3072,88 @@ function safeUrl2(raw) {
3030
3072
  }
3031
3073
  return "#";
3032
3074
  }
3033
- function scoreCell(value) {
3034
- const display = value === null ? "\u2014" : String(value);
3035
- return `<td class="score">${escapeHtml2(display)}</td>`;
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 siteHrefCell(site) {
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
- return `<td class="site"><a href="${href}">${name}</a></td>`;
3042
- }
3043
- function siteRow(site) {
3044
- return `<tr>
3045
- ${siteHrefCell(site)}
3046
- <td><a href="${escapeHtml2(safeUrl2(site.url))}" class="url" target="_blank" rel="noopener">${escapeHtml2(site.url)}</a></td>
3047
- ${scoreCell(site.pScore)}
3048
- ${scoreCell(site.rScore)}
3049
- ${scoreCell(site.bpScore)}
3050
- ${scoreCell(site.seoScore)}
3051
- </tr>`;
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: 2rem; }
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>` : `<table>
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>