@reddoorla/maintenance 0.15.0 → 0.17.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
@@ -2703,11 +2703,30 @@ async function derivePeriodStart(base, siteRow, reportType, today) {
2703
2703
 
2704
2704
  // src/reports/airtable/client.ts
2705
2705
  import Airtable from "airtable";
2706
+
2707
+ // src/util/credentials.ts
2708
+ import { readFileSync as readFileSync2 } from "fs";
2709
+ import { homedir } from "os";
2710
+ import { join as join20 } from "path";
2711
+ function defaultCredentialsPath() {
2712
+ const base = process.env.XDG_CONFIG_HOME ?? join20(homedir(), ".config");
2713
+ return join20(base, "reddoor-maint", "credentials.env");
2714
+ }
2715
+
2716
+ // src/reports/airtable/client.ts
2717
+ function missing(name) {
2718
+ return Object.assign(
2719
+ new Error(
2720
+ `${name} not set. Export it in your shell or put it in ${defaultCredentialsPath()} as ${name}=...`
2721
+ ),
2722
+ { exitCode: 2 }
2723
+ );
2724
+ }
2706
2725
  function readAirtableConfig() {
2707
2726
  const apiKey = process.env.AIRTABLE_PAT;
2708
2727
  const baseId = process.env.AIRTABLE_BASE_ID;
2709
- if (!apiKey) throw Object.assign(new Error("AIRTABLE_PAT not set"), { exitCode: 2 });
2710
- if (!baseId) throw Object.assign(new Error("AIRTABLE_BASE_ID not set"), { exitCode: 2 });
2728
+ if (!apiKey) throw missing("AIRTABLE_PAT");
2729
+ if (!baseId) throw missing("AIRTABLE_BASE_ID");
2711
2730
  return { apiKey, baseId };
2712
2731
  }
2713
2732
  function openBase(cfg) {
@@ -2938,6 +2957,25 @@ function findDueReports(websites, reports, today) {
2938
2957
  return out;
2939
2958
  }
2940
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
+
2941
2979
  // src/dashboard/render.ts
2942
2980
  function escapeHtml(s) {
2943
2981
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -2954,6 +2992,34 @@ function scoreTile(label, value) {
2954
2992
  const display = value === null ? "\u2014" : String(value);
2955
2993
  return `<div class="tile"><div class="tile-value">${escapeHtml(display)}</div><div class="tile-label">${escapeHtml(label)}</div></div>`;
2956
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
+ }
2957
3023
  function reportRow(r) {
2958
3024
  const date = r.completedOn ? escapeHtml(r.completedOn) : "\u2014";
2959
3025
  const type = escapeHtml(r.reportType);
@@ -2968,6 +3034,7 @@ body { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 860px; ma
2968
3034
  h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
2969
3035
  .meta { color: #666; margin-bottom: 2rem; }
2970
3036
  .meta a { color: inherit; }
3037
+ .audited { color: #999; font-size: 0.85rem; margin-bottom: 1.5rem; }
2971
3038
  .section { margin: 2rem 0; }
2972
3039
  .section h2 { font-size: 1.1rem; margin: 0 0 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #666; }
2973
3040
  .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.75rem; }
@@ -2975,6 +3042,7 @@ h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
2975
3042
  @media (prefers-color-scheme: dark) { .tile { border-color: #333; } }
2976
3043
  .tile-value { font-size: 2rem; font-weight: 600; }
2977
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; }
2978
3046
  table { width: 100%; border-collapse: collapse; }
2979
3047
  th, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid #eee; }
2980
3048
  @media (prefers-color-scheme: dark) { th, td { border-color: #2a2a2a; } }
@@ -2991,6 +3059,14 @@ function renderSiteDashboardHtml(site, reports) {
2991
3059
  ${scoreTile("Best Practices", site.bpScore)}
2992
3060
  ${scoreTile("SEO", site.seoScore)}
2993
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>` : "";
2994
3070
  const reportsSection = reports.length === 0 ? `<div class="empty">No reports yet.</div>` : `<table>
2995
3071
  <thead><tr><th>Completed</th><th>Type</th><th>ID</th><th>Report</th></tr></thead>
2996
3072
  <tbody>${reports.map(reportRow).join("")}</tbody>
@@ -3006,12 +3082,18 @@ function renderSiteDashboardHtml(site, reports) {
3006
3082
  <body>
3007
3083
  <h1>${name}</h1>
3008
3084
  <div class="meta"><a href="${escapeHtml(urlSafe)}">${escapeHtml(site.url)}</a></div>
3085
+ ${auditedLine}
3009
3086
 
3010
3087
  <div class="section">
3011
3088
  <h2>Lighthouse</h2>
3012
3089
  ${scoresSection}
3013
3090
  </div>
3014
3091
 
3092
+ <div class="section">
3093
+ <h2>Site Health</h2>
3094
+ ${healthSection}
3095
+ </div>
3096
+
3015
3097
  <div class="section">
3016
3098
  <h2>Reports</h2>
3017
3099
  ${reportsSection}
@@ -3043,25 +3125,6 @@ function onboardingStatus(row) {
3043
3125
  return { score, total: 4, checks };
3044
3126
  }
3045
3127
 
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
-
3065
3128
  // src/dashboard/fleet-render.ts
3066
3129
  function escapeHtml2(s) {
3067
3130
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");