@reddoorla/maintenance 0.16.0 → 0.17.1

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
@@ -2647,7 +2647,7 @@ function scoresFromWebsite(siteRow) {
2647
2647
  }
2648
2648
  function daysAgo(today, n) {
2649
2649
  const out = new Date(today);
2650
- out.setDate(out.getDate() - n);
2650
+ out.setUTCDate(out.getUTCDate() - n);
2651
2651
  return out;
2652
2652
  }
2653
2653
  async function draftReportForSite(base, siteRow, reportType, options = {}) {
@@ -2957,6 +2957,25 @@ function findDueReports(websites, reports, today) {
2957
2957
  return out;
2958
2958
  }
2959
2959
 
2960
+ // src/dashboard/relative-time.ts
2961
+ function relativeTimeFromNow(iso, now = /* @__PURE__ */ new Date()) {
2962
+ if (!iso) return "\u2014";
2963
+ const t = Date.parse(iso);
2964
+ if (Number.isNaN(t)) return "\u2014";
2965
+ const seconds = Math.max(0, Math.floor((now.getTime() - t) / 1e3));
2966
+ if (seconds < 60) return "just now";
2967
+ const minutes = Math.floor(seconds / 60);
2968
+ if (minutes < 60) return `${minutes}m ago`;
2969
+ const hours = Math.floor(minutes / 60);
2970
+ if (hours < 24) return `${hours}h ago`;
2971
+ const days = Math.floor(hours / 24);
2972
+ if (days < 7) return `${days}d ago`;
2973
+ const weeks = Math.floor(days / 7);
2974
+ if (weeks < 4) return `${weeks}w ago`;
2975
+ const months = Math.floor(days / 30);
2976
+ return `${months}mo ago`;
2977
+ }
2978
+
2960
2979
  // src/dashboard/render.ts
2961
2980
  function escapeHtml(s) {
2962
2981
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -2973,6 +2992,34 @@ function scoreTile(label, value) {
2973
2992
  const display = value === null ? "\u2014" : String(value);
2974
2993
  return `<div class="tile"><div class="tile-value">${escapeHtml(display)}</div><div class="tile-label">${escapeHtml(label)}</div></div>`;
2975
2994
  }
2995
+ function healthTile(label, value, sub) {
2996
+ const display = value === null ? "\u2014" : String(value);
2997
+ const subLine = sub ? `<div class="tile-sub">${escapeHtml(sub)}</div>` : "";
2998
+ return `<div class="tile"><div class="tile-value">${escapeHtml(display)}</div><div class="tile-label">${escapeHtml(label)}</div>${subLine}</div>`;
2999
+ }
3000
+ function depsSub(majorBehind) {
3001
+ if (majorBehind === null || majorBehind === 0) return null;
3002
+ return `${majorBehind} major behind`;
3003
+ }
3004
+ function securityTotal(site) {
3005
+ const parts = [
3006
+ site.securityVulnsCritical,
3007
+ site.securityVulnsHigh,
3008
+ site.securityVulnsModerate,
3009
+ site.securityVulnsLow
3010
+ ];
3011
+ if (parts.every((p) => p === null)) return null;
3012
+ return parts.reduce((sum, p) => sum + (p ?? 0), 0);
3013
+ }
3014
+ function securitySub(site) {
3015
+ const total = securityTotal(site);
3016
+ if (total === null || total === 0) return null;
3017
+ const c = site.securityVulnsCritical ?? 0;
3018
+ const h = site.securityVulnsHigh ?? 0;
3019
+ const m = site.securityVulnsModerate ?? 0;
3020
+ const l = site.securityVulnsLow ?? 0;
3021
+ return `${c}C / ${h}H / ${m}M / ${l}L`;
3022
+ }
2976
3023
  function reportRow(r) {
2977
3024
  const date = r.completedOn ? escapeHtml(r.completedOn) : "\u2014";
2978
3025
  const type = escapeHtml(r.reportType);
@@ -2987,6 +3034,7 @@ body { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 860px; ma
2987
3034
  h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
2988
3035
  .meta { color: #666; margin-bottom: 2rem; }
2989
3036
  .meta a { color: inherit; }
3037
+ .audited { color: #999; font-size: 0.85rem; margin-bottom: 1.5rem; }
2990
3038
  .section { margin: 2rem 0; }
2991
3039
  .section h2 { font-size: 1.1rem; margin: 0 0 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #666; }
2992
3040
  .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.75rem; }
@@ -2994,6 +3042,7 @@ h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
2994
3042
  @media (prefers-color-scheme: dark) { .tile { border-color: #333; } }
2995
3043
  .tile-value { font-size: 2rem; font-weight: 600; }
2996
3044
  .tile-label { font-size: 0.85rem; color: #666; margin-top: 0.25rem; }
3045
+ .tile-sub { font-size: 0.75rem; color: #999; margin-top: 0.15rem; }
2997
3046
  table { width: 100%; border-collapse: collapse; }
2998
3047
  th, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid #eee; }
2999
3048
  @media (prefers-color-scheme: dark) { th, td { border-color: #2a2a2a; } }
@@ -3010,6 +3059,14 @@ function renderSiteDashboardHtml(site, reports) {
3010
3059
  ${scoreTile("Best Practices", site.bpScore)}
3011
3060
  ${scoreTile("SEO", site.seoScore)}
3012
3061
  </div>`;
3062
+ const secTotal = securityTotal(site);
3063
+ const allHealthNull = site.a11yViolations === null && site.depsDrifted === null && secTotal === null;
3064
+ const healthSection = allHealthNull ? `<div class="empty">No health data yet \u2014 run <code>reddoor-maint audit --write-airtable</code> from the site checkout.</div>` : `<div class="tiles">
3065
+ ${healthTile("Accessibility issues", site.a11yViolations, null)}
3066
+ ${healthTile("Dependency updates", site.depsDrifted, depsSub(site.depsMajorBehind))}
3067
+ ${healthTile("Security alerts", secTotal, securitySub(site))}
3068
+ </div>`;
3069
+ const auditedLine = site.lastLighthouseAuditAt ? `<div class="audited">Last audited ${escapeHtml(relativeTimeFromNow(site.lastLighthouseAuditAt))}</div>` : "";
3013
3070
  const reportsSection = reports.length === 0 ? `<div class="empty">No reports yet.</div>` : `<table>
3014
3071
  <thead><tr><th>Completed</th><th>Type</th><th>ID</th><th>Report</th></tr></thead>
3015
3072
  <tbody>${reports.map(reportRow).join("")}</tbody>
@@ -3025,12 +3082,18 @@ function renderSiteDashboardHtml(site, reports) {
3025
3082
  <body>
3026
3083
  <h1>${name}</h1>
3027
3084
  <div class="meta"><a href="${escapeHtml(urlSafe)}">${escapeHtml(site.url)}</a></div>
3085
+ ${auditedLine}
3028
3086
 
3029
3087
  <div class="section">
3030
3088
  <h2>Lighthouse</h2>
3031
3089
  ${scoresSection}
3032
3090
  </div>
3033
3091
 
3092
+ <div class="section">
3093
+ <h2>Site Health</h2>
3094
+ ${healthSection}
3095
+ </div>
3096
+
3034
3097
  <div class="section">
3035
3098
  <h2>Reports</h2>
3036
3099
  ${reportsSection}
@@ -3062,25 +3125,6 @@ function onboardingStatus(row) {
3062
3125
  return { score, total: 4, checks };
3063
3126
  }
3064
3127
 
3065
- // src/dashboard/relative-time.ts
3066
- function relativeTimeFromNow(iso, now = /* @__PURE__ */ new Date()) {
3067
- if (!iso) return "\u2014";
3068
- const t = Date.parse(iso);
3069
- if (Number.isNaN(t)) return "\u2014";
3070
- const seconds = Math.max(0, Math.floor((now.getTime() - t) / 1e3));
3071
- if (seconds < 60) return "just now";
3072
- const minutes = Math.floor(seconds / 60);
3073
- if (minutes < 60) return `${minutes}m ago`;
3074
- const hours = Math.floor(minutes / 60);
3075
- if (hours < 24) return `${hours}h ago`;
3076
- const days = Math.floor(hours / 24);
3077
- if (days < 7) return `${days}d ago`;
3078
- const weeks = Math.floor(days / 7);
3079
- if (weeks < 4) return `${weeks}w ago`;
3080
- const months = Math.floor(days / 30);
3081
- return `${months}mo ago`;
3082
- }
3083
-
3084
3128
  // src/dashboard/fleet-render.ts
3085
3129
  function escapeHtml2(s) {
3086
3130
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");