@reddoorla/maintenance 0.11.2 → 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.d.ts CHANGED
@@ -420,4 +420,34 @@ declare function renderSiteDashboardHtml(site: WebsiteRow, reports: ReportRow[])
420
420
  */
421
421
  declare function verifyDashboardToken(provided: string | null | undefined, expected: string | null): boolean;
422
422
 
423
- export { ALL_AUDIT_NAMES, ALL_RECIPE_NAMES, type AirtableInventoryOptions, AuditName, AuditResult, BLURRED_CID, type BumpDepsOptions, type BundledImage, CHECK_CID, type ConvertToPnpmOptions, DEFAULT_INIT_STEPS, type DraftOptions, type DraftResult, type DueItem, type HeaderImage, type InitOptions, type InitResult, type InitStep, type InitStepResult, InventoryProvider, type LighthouseScores, type LocalPathOptions, type OnboardAudit, type OnboardOptions, type OrchestrateOptions, RecipeName, RecipeResult, type RenderResult, type ReportData, type ReportType, Site, type UpgradeSvelte4to5Options, a11yAudit, a11yFixturesPage, bumpDeps, convertToPnpm, depsAudit, draftReportForSite, findDueReports, fromAirtableBase, fromJsonFile, init, isRecipeName, lighthouseAudit, lintAudit, loadBundledImages, localPath, onboard, renderReportHtml, renderSiteDashboardHtml, runAudits, runAuditsAcross, securityAudit, selfCaretRange, selfPackageVersion, sendApprovedReports, svelteCodemods, upgradeSvelte4to5, verifyDashboardToken };
423
+ /**
424
+ * Render the fleet homepage as a single HTML document. Pure function:
425
+ * no Airtable access, no env reads, no I/O. The Netlify function handler
426
+ * fetches and gates, then hands here. Same style vocabulary as
427
+ * renderSiteDashboardHtml so the two pages feel like one product.
428
+ *
429
+ * Sites without a dashboardToken render as plain text plus a "no token"
430
+ * badge — visible-but-inactive, so the homepage doubles as a per-site
431
+ * setup-progress view.
432
+ */
433
+ declare function renderFleetHomeHtml(sites: WebsiteRow[]): string;
434
+
435
+ /**
436
+ * Verify an `Authorization: Basic <base64>` header against the configured
437
+ * dashboard password. Username is intentionally ignored — operators may
438
+ * type anything when the browser prompts; only the password gates entry.
439
+ *
440
+ * Returns false for any of:
441
+ * - missing/empty Authorization header
442
+ * - non-Basic auth scheme
443
+ * - malformed base64 or payload (no colon to split user:password)
444
+ * - wrong password
445
+ * - expected password missing (DASHBOARD_PASSWORD not configured)
446
+ *
447
+ * Wrong-password compare is constant-time; lengths are checked first
448
+ * (timingSafeEqual throws on mismatch, and the length itself doesn't
449
+ * leak — operator's password length is fixed per deploy).
450
+ */
451
+ declare function verifyBasicAuth(authHeader: string | null | undefined, expectedPassword: string | null): boolean;
452
+
453
+ export { ALL_AUDIT_NAMES, ALL_RECIPE_NAMES, type AirtableInventoryOptions, AuditName, AuditResult, BLURRED_CID, type BumpDepsOptions, type BundledImage, CHECK_CID, type ConvertToPnpmOptions, DEFAULT_INIT_STEPS, type DraftOptions, type DraftResult, type DueItem, type HeaderImage, type InitOptions, type InitResult, type InitStep, type InitStepResult, InventoryProvider, type LighthouseScores, type LocalPathOptions, type OnboardAudit, type OnboardOptions, type OrchestrateOptions, RecipeName, RecipeResult, type RenderResult, type ReportData, type ReportType, Site, type UpgradeSvelte4to5Options, a11yAudit, a11yFixturesPage, bumpDeps, convertToPnpm, depsAudit, draftReportForSite, findDueReports, fromAirtableBase, fromJsonFile, init, isRecipeName, lighthouseAudit, lintAudit, loadBundledImages, localPath, onboard, renderFleetHomeHtml, renderReportHtml, renderSiteDashboardHtml, runAudits, runAuditsAcross, securityAudit, selfCaretRange, selfPackageVersion, sendApprovedReports, svelteCodemods, upgradeSvelte4to5, verifyBasicAuth, verifyDashboardToken };
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