@reddoorla/maintenance 0.11.1 → 0.12.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
@@ -2626,11 +2626,11 @@ async function uploadAttachment(recordId, fieldName, body, filename, contentType
2626
2626
  }
2627
2627
 
2628
2628
  // src/reports/draft.ts
2629
- function scoresFromWebsite(siteRow) {
2630
- const { pScore, rScore, bpScore, seoScore } = siteRow;
2629
+ function scoresFromWebsite(siteRow2) {
2630
+ const { pScore, rScore, bpScore, seoScore } = siteRow2;
2631
2631
  if (pScore === null || rScore === null || bpScore === null || seoScore === null) {
2632
2632
  throw new Error(
2633
- `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.`
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.`
2634
2634
  );
2635
2635
  }
2636
2636
  return { performance: pScore, accessibility: rScore, bestPractices: bpScore, seo: seoScore };
@@ -2640,18 +2640,18 @@ function daysAgo(today, n) {
2640
2640
  out.setDate(out.getDate() - n);
2641
2641
  return out;
2642
2642
  }
2643
- async function draftReportForSite(base, siteRow, reportType, options = {}) {
2644
- const scores = scoresFromWebsite(siteRow);
2643
+ async function draftReportForSite(base, siteRow2, reportType, options = {}) {
2644
+ const scores = scoresFromWebsite(siteRow2);
2645
2645
  const today = /* @__PURE__ */ new Date();
2646
- const slug = siteSlug(siteRow.name);
2647
- const periodStart = base !== null ? await derivePeriodStart(base, siteRow, reportType, today) : daysAgo(today, 30);
2646
+ const slug = siteSlug(siteRow2.name);
2647
+ const periodStart = base !== null ? await derivePeriodStart(base, siteRow2, reportType, today) : daysAgo(today, 30);
2648
2648
  const periodEnd = today;
2649
2649
  const completedOn = today;
2650
- const lastTestedDate = reportType === "Maintenance" && siteRow.testingDay ? new Date(siteRow.testingDay) : null;
2650
+ const lastTestedDate = reportType === "Maintenance" && siteRow2.testingDay ? new Date(siteRow2.testingDay) : null;
2651
2651
  const cidName = `${slug}-header`;
2652
2652
  const { html } = await renderReportHtml({
2653
- siteName: siteRow.name,
2654
- siteUrl: siteRow.url,
2653
+ siteName: siteRow2.name,
2654
+ siteUrl: siteRow2.url,
2655
2655
  reportType,
2656
2656
  completedOn,
2657
2657
  lighthouse: scores,
@@ -2668,10 +2668,10 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
2668
2668
  return { reportRow: null, htmlPath: path, html };
2669
2669
  }
2670
2670
  if (base === null) throw new Error("base required when previewOnly=false");
2671
- const reportId = `${siteRow.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
2671
+ const reportId = `${siteRow2.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
2672
2672
  const created = await createDraft(base, {
2673
2673
  reportId,
2674
- siteId: siteRow.id,
2674
+ siteId: siteRow2.id,
2675
2675
  reportType,
2676
2676
  periodStart,
2677
2677
  periodEnd,
@@ -2684,8 +2684,8 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
2684
2684
  await setDraftReady(base, created.id, true);
2685
2685
  return { reportRow: created, htmlPath: null, html };
2686
2686
  }
2687
- async function derivePeriodStart(base, siteRow, reportType, today) {
2688
- const prior = await listReportsForSite(base, siteRow.id);
2687
+ async function derivePeriodStart(base, siteRow2, reportType, today) {
2688
+ const prior = await listReportsForSite(base, siteRow2.id);
2689
2689
  const sameType = prior.filter((r) => r.reportType === reportType && r.periodEnd).map((r) => r.periodEnd).sort();
2690
2690
  const latest = sameType[sameType.length - 1];
2691
2691
  return latest ? new Date(latest) : daysAgo(today, 30);
@@ -3017,6 +3017,105 @@ function verifyDashboardToken(provided, expected) {
3017
3017
  if (provided.length !== expected.length) return false;
3018
3018
  return timingSafeEqual(Buffer.from(provided, "utf-8"), Buffer.from(expected, "utf-8"));
3019
3019
  }
3020
+
3021
+ // src/dashboard/fleet-render.ts
3022
+ function escapeHtml2(s) {
3023
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3024
+ }
3025
+ function safeUrl2(raw) {
3026
+ try {
3027
+ const u = new URL(raw);
3028
+ if (u.protocol === "http:" || u.protocol === "https:") return raw;
3029
+ } catch {
3030
+ }
3031
+ return "#";
3032
+ }
3033
+ function scoreCell(value) {
3034
+ const display = value === null ? "\u2014" : String(value);
3035
+ return `<td class="score">${escapeHtml2(display)}</td>`;
3036
+ }
3037
+ function siteHrefCell(site) {
3038
+ const name = escapeHtml2(site.name);
3039
+ if (!site.dashboardToken) {
3040
+ return `<td class="site"><span class="name">${name}</span> <span class="badge">no token</span></td>`;
3041
+ }
3042
+ const href = `/s/${escapeHtml2(siteSlug(site.name))}?t=${escapeHtml2(site.dashboardToken)}`;
3043
+ return `<td class="site"><a href="${href}">${name}</a></td>`;
3044
+ }
3045
+ function siteRow(site) {
3046
+ return `<tr>
3047
+ ${siteHrefCell(site)}
3048
+ <td><a href="${escapeHtml2(safeUrl2(site.url))}" class="url" target="_blank" rel="noopener">${escapeHtml2(site.url)}</a></td>
3049
+ ${scoreCell(site.pScore)}
3050
+ ${scoreCell(site.rScore)}
3051
+ ${scoreCell(site.bpScore)}
3052
+ ${scoreCell(site.seoScore)}
3053
+ </tr>`;
3054
+ }
3055
+ var STYLES2 = `
3056
+ :root { color-scheme: light dark; }
3057
+ body { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 1100px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }
3058
+ @media (prefers-color-scheme: dark) { body { color: #e8e8e8; background: #111; } a { color: #6cb6ff; } }
3059
+ h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
3060
+ .meta { color: #666; margin-bottom: 2rem; }
3061
+ table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
3062
+ 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; }
3063
+ @media (prefers-color-scheme: dark) { th { border-color: #333; } }
3064
+ td { padding: 0.65rem 0.5rem; border-bottom: 1px solid #eee; }
3065
+ @media (prefers-color-scheme: dark) { td { border-color: #2a2a2a; } }
3066
+ td.site a { font-weight: 500; }
3067
+ td.url { color: #666; font-size: 0.85rem; }
3068
+ td.score { text-align: right; font-variant-numeric: tabular-nums; min-width: 3.5rem; }
3069
+ .badge { display: inline-block; margin-left: 0.5rem; padding: 0.1rem 0.5rem; font-size: 0.75rem; border-radius: 3px; background: #f0f0f0; color: #999; }
3070
+ @media (prefers-color-scheme: dark) { .badge { background: #2a2a2a; color: #777; } }
3071
+ .empty { color: #999; padding: 2rem; text-align: center; border: 1px dashed #ccc; border-radius: 6px; }
3072
+ `;
3073
+ function renderFleetHomeHtml(sites) {
3074
+ const body = sites.length === 0 ? `<div class="empty">No sites in the Websites table yet.</div>` : `<table>
3075
+ <thead><tr>
3076
+ <th>Site</th>
3077
+ <th>URL</th>
3078
+ <th>Perf</th>
3079
+ <th>A11y</th>
3080
+ <th>BP</th>
3081
+ <th>SEO</th>
3082
+ </tr></thead>
3083
+ <tbody>${sites.map(siteRow).join("")}</tbody>
3084
+ </table>`;
3085
+ return `<!doctype html>
3086
+ <html lang="en">
3087
+ <head>
3088
+ <meta charset="utf-8" />
3089
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
3090
+ <title>Reddoor maintenance \u2014 fleet</title>
3091
+ <style>${STYLES2}</style>
3092
+ </head>
3093
+ <body>
3094
+ <h1>Reddoor fleet</h1>
3095
+ <div class="meta">${sites.length} site${sites.length === 1 ? "" : "s"} in the Websites table.</div>
3096
+ ${body}
3097
+ </body>
3098
+ </html>`;
3099
+ }
3100
+
3101
+ // src/dashboard/basic-auth.ts
3102
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
3103
+ function verifyBasicAuth(authHeader, expectedPassword) {
3104
+ if (!authHeader || !expectedPassword) return false;
3105
+ const match = /^basic\s+(.+)$/i.exec(authHeader.trim());
3106
+ if (!match) return false;
3107
+ let decoded;
3108
+ try {
3109
+ decoded = Buffer.from(match[1], "base64").toString("utf-8");
3110
+ } catch {
3111
+ return false;
3112
+ }
3113
+ const colonIdx = decoded.indexOf(":");
3114
+ if (colonIdx === -1) return false;
3115
+ const provided = decoded.slice(colonIdx + 1);
3116
+ if (provided.length !== expectedPassword.length) return false;
3117
+ return timingSafeEqual2(Buffer.from(provided, "utf-8"), Buffer.from(expectedPassword, "utf-8"));
3118
+ }
3020
3119
  export {
3021
3120
  ALL_AUDIT_NAMES,
3022
3121
  ALL_RECIPE_NAMES,
@@ -3039,6 +3138,7 @@ export {
3039
3138
  loadBundledImages,
3040
3139
  localPath,
3041
3140
  onboard,
3141
+ renderFleetHomeHtml,
3042
3142
  renderReportHtml,
3043
3143
  renderSiteDashboardHtml,
3044
3144
  runAudits,
@@ -3050,6 +3150,7 @@ export {
3050
3150
  svelteCodemods,
3051
3151
  syncConfigs,
3052
3152
  upgradeSvelte4to5,
3153
+ verifyBasicAuth,
3053
3154
  verifyDashboardToken
3054
3155
  };
3055
3156
  //# sourceMappingURL=index.js.map