@reddoorla/maintenance 0.47.0 → 0.49.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
@@ -2646,6 +2646,10 @@ var ACTIVE_STATUSES = /* @__PURE__ */ new Set([
2646
2646
  "maintenance",
2647
2647
  "launch period"
2648
2648
  ]);
2649
+ var FREQUENCIES = ["None", "Monthly", "Quarterly", "Yearly"];
2650
+ function toFrequency(raw) {
2651
+ return typeof raw === "string" && FREQUENCIES.includes(raw) ? raw : "None";
2652
+ }
2649
2653
  function mapRow(rec) {
2650
2654
  const f = rec.fields;
2651
2655
  const attachments = f["Header image"] ?? [];
@@ -2656,8 +2660,8 @@ function mapRow(rec) {
2656
2660
  url: String(f["url"] ?? ""),
2657
2661
  status: f["Status"] ?? null,
2658
2662
  pointOfContact: f["point of contact"] ?? null,
2659
- maintenanceFreq: f["maintenence freq"] ?? "None",
2660
- testingFreq: f["testing freq"] ?? "None",
2663
+ maintenanceFreq: toFrequency(f["maintenence freq"]),
2664
+ testingFreq: toFrequency(f["testing freq"]),
2661
2665
  maintenanceDay: f["maintenance day"] ?? null,
2662
2666
  testingDay: f["testing day"] ?? null,
2663
2667
  ga4PropertyId: f["GA4 property ID"] ?? null,
@@ -2785,15 +2789,10 @@ var DEFAULT_COPY = {
2785
2789
  ],
2786
2790
  announceHeading: "YOUR ONGOING SITE CARE",
2787
2791
  announceBody: "We've completed a full test of your site and set it up for ongoing care to keep it fast, secure, and healthy. Here's what you can expect from us going forward:",
2788
- announceCadenceHeading: "WHAT TO EXPECT",
2789
- announceTestingLabel: "Full site testing",
2790
- announceMaintenanceLabel: "Routine maintenance",
2791
- announcePreviewLabel: "From your latest full site test:",
2792
- announceScoreNote: "These are independent Google Lighthouse scores, each out of 100 \u2014 higher is better.",
2793
2792
  announceImprovementResend: "Your contact forms now deliver straight to your inbox through reliable infrastructure, so no inquiry slips through the cracks.",
2794
2793
  announceImprovementSvelte5: "We've modernized your site to the latest framework \u2014 it's faster, more secure, and built to last.",
2795
2794
  announceCadence: "After each one we'll send you a short report like this \u2014 there's nothing you need to do.",
2796
- announceOpenDoor: "And if you'd ever like to expand the scope, add features, or freshen anything up, just reply \u2014 we'd love to help."
2795
+ announceOpenDoor: "And if you'd ever like to expand the scope, add features, or freshen anything up, just let us know."
2797
2796
  };
2798
2797
  function override(v) {
2799
2798
  if (typeof v !== "string") return null;
@@ -2883,70 +2882,118 @@ function safeUrl(raw) {
2883
2882
  return "#";
2884
2883
  }
2885
2884
 
2886
- // src/reports/maintenance-email/template.ts
2885
+ // src/reports/email-sections.ts
2887
2886
  var escapeXml = escapeHtml;
2887
+ var RED = "#C00";
2888
+ var GREY = "#757575";
2889
+ var BORDER = "#CCCCCC";
2890
+ var TREND_UP = "#2E7D32";
2891
+ var TREND_NEUTRAL = GREY;
2888
2892
  var CHECK_PNG = `cid:${CHECK_CID}`;
2889
- var BLURRED_TESTS = `cid:${BLURRED_CID}`;
2890
- function fmtDate(d) {
2891
- if (!d || Number.isNaN(d.getTime())) return "";
2892
- const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
2893
- const dd = String(d.getUTCDate()).padStart(2, "0");
2894
- const yyyy = d.getUTCFullYear();
2895
- return `${mm}.${dd}.${yyyy}`;
2896
- }
2897
2893
  function fmtUsers(n) {
2898
2894
  return n.toLocaleString("en-US");
2899
2895
  }
2900
- var TREND_UP = "#2E7D32";
2901
- var TREND_NEUTRAL = "#757575";
2902
- function trendText(color, text) {
2903
- return `<mj-text color="${color}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${text}</mj-text>`;
2896
+ function checklistRowsSection(rows, opts) {
2897
+ return rows.map((label, i) => {
2898
+ const isLast = i === rows.length - 1;
2899
+ const border = isLast ? "" : ` border-bottom="solid ${BORDER} 1px"`;
2900
+ const lastPad = isLast ? ` padding-bottom="${opts.lastPaddingBottom}"` : "";
2901
+ return `
2902
+ <mj-section background-color="${opts.background}" padding="0px"${lastPad}>
2903
+ <mj-group>
2904
+ <mj-column padding-left="0px" width="90%"${border}>
2905
+ <mj-text height="25px" padding-left="0px" color="${GREY}" padding-top="20px" padding-bottom="7.5px" font-size="16px">${escapeXml(label)}</mj-text>
2906
+ </mj-column>
2907
+ <mj-column width="10%"${border} padding-top="15px">
2908
+ <mj-image align="right" padding-right="0px" width="20px" height="20px" padding-top="2.5px" padding-bottom="15px" src="${CHECK_PNG}" />
2909
+ </mj-column>
2910
+ </mj-group>
2911
+ </mj-section>`;
2912
+ }).join("");
2913
+ }
2914
+ var LIGHTHOUSE_ROWS = [
2915
+ { label: "Performance", key: "performance", range: "Acceptable 50\u201389 // Ideal 90\u2013100" },
2916
+ { label: "Readability (A11y)", key: "accessibility", range: "Acceptable 80\u201399 // Ideal 100" },
2917
+ { label: "Best Practices", key: "bestPractices", range: "Acceptable 60\u201379 // Ideal 80\u2013100" },
2918
+ { label: "Site Structure", key: "seo", range: "Acceptable 50\u201389 // Ideal 90\u2013100" }
2919
+ ];
2920
+ function lighthouseScoresSection(lighthouse2, opts = {}) {
2921
+ const background = opts.background ?? "#F4F4F4";
2922
+ const sectionPad = opts.pad ? ` padding-top="${opts.pad}" padding-bottom="${opts.pad}"` : "";
2923
+ const labelTop = opts.pad ?? "55px";
2924
+ const footnoteBottom = opts.pad ? "0px" : "36px";
2925
+ const rows = LIGHTHOUSE_ROWS.map(
2926
+ ({ label, key, range }, i) => `
2927
+ <mj-text color="${RED}" font-size="20px" font-weight="300" padding-top="25px">${label}</mj-text>
2928
+ <mj-text color="${RED}" font-size="44px" font-weight="400" padding-top="0px">${lighthouse2[key]}</mj-text>
2929
+ <mj-text color="${GREY}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="0px" padding-bottom="36px">${range}</mj-text>${i < LIGHTHOUSE_ROWS.length - 1 ? `
2930
+ <mj-divider border-width="1px" border-style="solid" border-color="${BORDER}" padding="0" />` : ""}`
2931
+ ).join("");
2932
+ return `
2933
+ <mj-section background-color="${background}"${sectionPad}>
2934
+ <mj-column>
2935
+ <mj-text color="${RED}" font-size="20px" font-weight="700" padding-top="${labelTop}">LIGHTHOUSE SCORES*</mj-text>${rows}
2936
+ <mj-text color="${GREY}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" padding-bottom="${footnoteBottom}" line-height="20px">*A Lighthouse score is a numerical measure provided by Google's Lighthouse tool, which evaluates various aspects of a web page's quality.</mj-text>
2937
+ </mj-column>
2938
+ </mj-section>`;
2904
2939
  }
2905
2940
  function analyticsTrendLine(cur, prev) {
2906
2941
  if (cur === void 0 || prev === void 0) {
2907
- return trendText(TREND_NEUTRAL, `Last Period: ${prev !== void 0 ? fmtUsers(prev) : "\u2014"}`);
2942
+ return trendLine(TREND_NEUTRAL, `Last Period: ${prev !== void 0 ? fmtUsers(prev) : "\u2014"}`);
2908
2943
  }
2909
2944
  if (prev === 0) {
2910
- return cur > 0 ? trendText(TREND_UP, "\u25B2 New this period (0 last period)") : trendText(TREND_NEUTRAL, "Last Period: 0");
2945
+ return cur > 0 ? trendLine(TREND_UP, "\u25B2 New this period (0 last period)") : trendLine(TREND_NEUTRAL, "Last Period: 0");
2911
2946
  }
2912
2947
  const pct = Math.round((cur - prev) / prev * 100);
2913
2948
  const range = `(${fmtUsers(prev)} \u2192 ${fmtUsers(cur)})`;
2914
- if (pct > 0) return trendText(TREND_UP, `\u25B2 ${pct}% vs last period ${range}`);
2915
- if (pct < 0) return trendText(TREND_NEUTRAL, `\u25BC ${Math.abs(pct)}% vs last period ${range}`);
2916
- return trendText(TREND_NEUTRAL, `No change vs last period (${fmtUsers(prev)})`);
2949
+ if (pct > 0) return trendLine(TREND_UP, `\u25B2 ${pct}% vs last period ${range}`);
2950
+ if (pct < 0) return trendLine(TREND_NEUTRAL, `\u25BC ${Math.abs(pct)}% vs last period ${range}`);
2951
+ return trendLine(TREND_NEUTRAL, `No change vs last period (${fmtUsers(prev)})`);
2952
+ }
2953
+ function trendLine(color, text) {
2954
+ return `<mj-text color="${color}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${text}</mj-text>`;
2955
+ }
2956
+ function footnoteLine(text) {
2957
+ return `<mj-text color="${GREY}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" padding-bottom="36px" line-height="20px">${text}</mj-text>`;
2958
+ }
2959
+ function analyticsSection(opts) {
2960
+ const users = opts.current !== void 0 ? fmtUsers(opts.current) : "\u2014";
2961
+ const body = (opts.bodyLines ?? []).map((l) => trendLine(TREND_NEUTRAL, l)).join("\n ");
2962
+ const footnotes = (opts.footnoteLines ?? []).map(footnoteLine).join("\n ");
2963
+ const sectionPad = opts.pad ? ` padding-top="${opts.pad}" padding-bottom="${opts.pad}"` : "";
2964
+ const labelTop = opts.pad ?? "75px";
2965
+ return `
2966
+ <mj-section background-color="${opts.background}"${sectionPad}>
2967
+ <mj-column>
2968
+ <mj-text color="${RED}" font-size="20px" font-weight="700" padding-top="${labelTop}">ANALYTICS</mj-text>
2969
+ <mj-text color="${RED}" font-size="44px" font-weight="400">${users} Users</mj-text>
2970
+ ${analyticsTrendLine(opts.current, opts.previous)}
2971
+ ${body}
2972
+ ${footnotes}
2973
+ </mj-column>
2974
+ </mj-section>`;
2975
+ }
2976
+
2977
+ // src/reports/maintenance-email/template.ts
2978
+ var escapeXml2 = escapeHtml;
2979
+ var BLURRED_TESTS = `cid:${BLURRED_CID}`;
2980
+ function fmtDate(d) {
2981
+ if (!d || Number.isNaN(d.getTime())) return "";
2982
+ const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
2983
+ const dd = String(d.getUTCDate()).padStart(2, "0");
2984
+ const yyyy = d.getUTCFullYear();
2985
+ return `${mm}.${dd}.${yyyy}`;
2917
2986
  }
2918
2987
  function maintenanceChecksSection(copy, searchPosition) {
2919
2988
  const googleLabel = searchPosition !== void 0 ? `Page 1 Google Result (#${searchPosition})` : copy.maintenanceChecks[3] ?? "";
2920
2989
  const rows = copy.maintenanceChecks.map((label, i) => i === 3 ? googleLabel : label);
2921
- return rows.map(
2922
- (label, i) => `
2923
- <mj-section background-color="white" padding="0px"${i === rows.length - 1 ? ' padding-bottom="36px"' : ""}>
2924
- <mj-group>
2925
- <mj-column padding-left="0px" width="90%"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""}>
2926
- <mj-text height="25px" padding-left="0px" color="#757575" padding-top="20px" padding-bottom="7.5px" font-size="16px">${escapeXml(label)}</mj-text>
2927
- </mj-column>
2928
- <mj-column width="10%"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""} padding-top="15px">
2929
- <mj-image align="right" padding-right="0px" width="20px" height="20px" padding-top="2.5px" padding-bottom="15px" src="${CHECK_PNG}" />
2930
- </mj-column>
2931
- </mj-group>
2932
- </mj-section>`
2933
- ).join("");
2990
+ return checklistRowsSection(rows, { background: "white", lastPaddingBottom: "36px" });
2934
2991
  }
2935
2992
  function testingChecklistSection(copy) {
2936
- const rows = copy.testingChecklist;
2937
- return rows.map(
2938
- (label, i) => `
2939
- <mj-section background-color="#F4F4F4" padding="0px"${i === rows.length - 1 ? ' padding-bottom="60px"' : ""}>
2940
- <mj-group>
2941
- <mj-column width="90%" padding-left="0px"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""}>
2942
- <mj-text height="25px" padding-left="0px" color="#757575" padding-top="20px" padding-bottom="7.5px" font-size="16px">${escapeXml(label)}</mj-text>
2943
- </mj-column>
2944
- <mj-column width="10%"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""} padding-top="15px">
2945
- <mj-image align="right" padding-right="0px" width="20px" height="20px" padding-top="2.5px" padding-bottom="15px" src="${CHECK_PNG}" />
2946
- </mj-column>
2947
- </mj-group>
2948
- </mj-section>`
2949
- ).join("");
2993
+ return checklistRowsSection(copy.testingChecklist, {
2994
+ background: "#F4F4F4",
2995
+ lastPaddingBottom: "60px"
2996
+ });
2950
2997
  }
2951
2998
  function maintenanceTestingPlaceholder(lastTested) {
2952
2999
  return `
@@ -2966,7 +3013,7 @@ function testingIntroSection(copy) {
2966
3013
  <mj-section background-color="#F4F4F4">
2967
3014
  <mj-column>
2968
3015
  <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">TESTING</mj-text>
2969
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml(copy.testingIntro)}</mj-text>
3016
+ <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml2(copy.testingIntro)}</mj-text>
2970
3017
  </mj-column>
2971
3018
  </mj-section>`;
2972
3019
  }
@@ -2974,8 +3021,8 @@ function commentarySection(text, copy) {
2974
3021
  return `
2975
3022
  <mj-section background-color="white">
2976
3023
  <mj-column>
2977
- <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="55px">${escapeXml(copy.notesHeader)}</mj-text>
2978
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml(text).replace(/\r\n?|\n/g, "<br/>")}</mj-text>
3024
+ <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="55px">${escapeXml2(copy.notesHeader)}</mj-text>
3025
+ <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml2(text).replace(/\r\n?|\n/g, "<br/>")}</mj-text>
2979
3026
  </mj-column>
2980
3027
  </mj-section>`;
2981
3028
  }
@@ -2984,8 +3031,8 @@ function hasHeaderDims(data) {
2984
3031
  }
2985
3032
  function headerImageTag(data) {
2986
3033
  const src = `cid:${data.headerImageCid}`;
2987
- const alt = `${escapeXml(data.siteName)} maintenance report`;
2988
- const href = isHttpUrl(data.siteUrl) ? escapeXml(data.siteUrl) : "#";
3034
+ const alt = `${escapeXml2(data.siteName)} maintenance report`;
3035
+ const href = isHttpUrl(data.siteUrl) ? escapeXml2(data.siteUrl) : "#";
2989
3036
  if (hasHeaderDims(data)) {
2990
3037
  return `<mj-image href="${href}" src="${src}" alt="${alt}" width="${data.headerWidth}px" css-class="rd-header" container-background-color="${data.headerBgColor}" />`;
2991
3038
  }
@@ -2998,7 +3045,7 @@ function headerStyleBlock(data) {
2998
3045
  function buildMjml(data) {
2999
3046
  const copy = data.copy ?? DEFAULT_COPY;
3000
3047
  const isTesting = data.reportType === "Testing";
3001
- const previewText = `Checked up on ${escapeXml(data.siteName)}`;
3048
+ const previewText = `Checked up on ${escapeXml2(data.siteName)}`;
3002
3049
  return `<mjml>
3003
3050
  <mj-head>
3004
3051
  <mj-attributes>
@@ -3020,52 +3067,30 @@ function buildMjml(data) {
3020
3067
  <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">COMPLETED ON</mj-text>
3021
3068
  <mj-text color="#C00" font-size="44px" font-weight="400">${fmtDate(data.completedOn)}</mj-text>
3022
3069
  <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">MAINTENANCE CHECKS</mj-text>
3023
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml(copy.maintenanceIntro)}</mj-text>
3070
+ <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml2(copy.maintenanceIntro)}</mj-text>
3024
3071
  </mj-column>
3025
3072
  </mj-section>
3026
3073
  ${maintenanceChecksSection(copy, data.searchPosition)}
3027
- <mj-section background-color="#F4F4F4">
3028
- <mj-column>
3029
- <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="55px">LIGHTHOUSE SCORES*</mj-text>
3030
- <mj-text color="#C00" font-size="20px" font-weight="300" padding-top="25px">Performance</mj-text>
3031
- <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.performance}</mj-text>
3032
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="0px" padding-bottom="36px">Acceptable 50\u201389 // Ideal 90\u2013100</mj-text>
3033
- <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
3034
- <mj-text color="#C00" font-size="20px" font-weight="300" padding-top="25px">Readability (A11y)</mj-text>
3035
- <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.accessibility}</mj-text>
3036
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="0px" padding-bottom="36px">Acceptable 80\u201399 // Ideal 100</mj-text>
3037
- <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
3038
- <mj-text color="#C00" font-size="20px" font-weight="300" padding-top="25px">Best Practices</mj-text>
3039
- <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.bestPractices}</mj-text>
3040
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="0px" padding-bottom="36px">Acceptable 60\u201379 // Ideal 80\u201392</mj-text>
3041
- <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
3042
- <mj-text color="#C00" font-size="20px" font-weight="300" padding-top="25px">Site Structure</mj-text>
3043
- <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.seo}</mj-text>
3044
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="0px" padding-bottom="36px">Acceptable 50\u201389 // Ideal 90\u2013100</mj-text>
3045
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" padding-bottom="36px" line-height="20px">*A Lighthouse score is a numerical measure provided by Google's Lighthouse tool, which evaluates various aspects of a web page's quality.</mj-text>
3046
- </mj-column>
3047
- </mj-section>
3048
- <mj-section background-color="white">
3049
- <mj-column>
3050
- <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">ANALYTICS</mj-text>
3051
- <mj-text color="#C00" font-size="44px" font-weight="400">${data.gaUsersCurrent !== void 0 ? fmtUsers(data.gaUsersCurrent) : "\u2014"} Users</mj-text>
3052
- ${analyticsTrendLine(data.gaUsersCurrent, data.gaUsersPrevious)}
3053
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" padding-bottom="36px" line-height="20px">${escapeXml(copy.seoCta)}</mj-text>
3054
- </mj-column>
3055
- </mj-section>
3074
+ ${lighthouseScoresSection(data.lighthouse)}
3075
+ ${analyticsSection({
3076
+ current: data.gaUsersCurrent,
3077
+ previous: data.gaUsersPrevious,
3078
+ background: "white",
3079
+ footnoteLines: [escapeXml2(copy.seoCta)]
3080
+ })}
3056
3081
  ${isTesting ? testingIntroSection(copy) + testingChecklistSection(copy) : maintenanceTestingPlaceholder(data.lastTestedDate)}
3057
3082
  ${data.commentary ? commentarySection(data.commentary, copy) : ""}
3058
3083
  <mj-section background-color="white">
3059
3084
  <mj-column padding-top="36px">
3060
3085
  <mj-text color="#C00" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" padding-top="36px" line-height="36px">Any questions, concerns or requests?</mj-text>
3061
3086
  ${copy.contact.map(
3062
- (line, i) => i === copy.contact.length - 1 ? `<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" padding-top="0px" line-height="30px" padding-bottom="36px">${escapeXml(line)}</mj-text>` : `<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
3087
+ (line, i) => i === copy.contact.length - 1 ? `<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" padding-top="0px" line-height="30px" padding-bottom="36px">${escapeXml2(line)}</mj-text>` : `<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
3063
3088
  ).join("\n ")}
3064
3089
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
3065
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>
3090
+ <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()} ${escapeXml2(copy.footerOrg)}. All rights reserved.</mj-text>
3066
3091
  <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="700" line-height="16px" padding-top="0" padding-bottom="0px">Our mailing address is:</mj-text>
3067
3092
  ${[copy.footerOrg, ...copy.footerAddress].map(
3068
- (line) => `<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml(line)}</mj-text>`
3093
+ (line) => `<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml2(line)}</mj-text>`
3069
3094
  ).join("\n ")}
3070
3095
  </mj-column>
3071
3096
  </mj-section>
@@ -3074,22 +3099,22 @@ function buildMjml(data) {
3074
3099
  }
3075
3100
 
3076
3101
  // src/reports/launch-email/template.ts
3077
- var RED = "#C00";
3078
- var GREY = "#757575";
3102
+ var RED2 = "#C00";
3103
+ var GREY2 = "#757575";
3079
3104
  function buildLaunchMjml(data) {
3080
3105
  const copy = data.copy ?? DEFAULT_COPY;
3081
- const previewText = `${escapeXml(data.siteName)} is live`;
3106
+ const previewText = `${escapeXml2(data.siteName)} is live`;
3082
3107
  const setupRows = copy.launchSetupItems.map(
3083
3108
  (item) => `
3084
- <mj-text color="${GREY}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px" padding-bottom="4px">\u2022 ${escapeXml(item)}</mj-text>`
3109
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px" padding-bottom="4px">\u2022 ${escapeXml2(item)}</mj-text>`
3085
3110
  ).join("");
3086
3111
  const contactRows = copy.contact.map(
3087
3112
  (line) => `
3088
- <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
3113
+ <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
3089
3114
  ).join("");
3090
3115
  const footerAddressRows = copy.footerAddress.map(
3091
3116
  (line) => `
3092
- <mj-text color="${GREY}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml(line)}</mj-text>`
3117
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml2(line)}</mj-text>`
3093
3118
  ).join("");
3094
3119
  return `<mjml>
3095
3120
  <mj-head>
@@ -3107,20 +3132,20 @@ function buildLaunchMjml(data) {
3107
3132
  </mj-section>
3108
3133
  <mj-section background-color="white">
3109
3134
  <mj-column>
3110
- <mj-text color="${RED}" font-size="20px" font-weight="700" padding-top="75px">${escapeXml(copy.launchHeading)}</mj-text>
3111
- <mj-text color="${RED}" font-size="44px" font-weight="400">${fmtDate(data.completedOn)}</mj-text>
3112
- <mj-text color="${GREY}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="20px">${escapeXml(copy.launchBody)}</mj-text>
3135
+ <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="75px">${escapeXml2(copy.launchHeading)}</mj-text>
3136
+ <mj-text color="${RED2}" font-size="44px" font-weight="400">${fmtDate(data.completedOn)}</mj-text>
3137
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="20px">${escapeXml2(copy.launchBody)}</mj-text>
3113
3138
  ${setupRows}
3114
3139
  </mj-column>
3115
3140
  </mj-section>
3116
3141
  <mj-section background-color="white">
3117
3142
  <mj-column padding-top="36px">
3118
- <mj-text color="${RED}" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" padding-top="36px" line-height="36px">Any questions, concerns or requests?</mj-text>
3143
+ <mj-text color="${RED2}" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" padding-top="36px" line-height="36px">Any questions, concerns or requests?</mj-text>
3119
3144
  ${contactRows}
3120
3145
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
3121
- <mj-text color="${GREY}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>
3122
- <mj-text color="${GREY}" font-family="helvetica, sans-serif" font-size="12px" font-weight="700" line-height="16px" padding-top="0" padding-bottom="0px">Our mailing address is:</mj-text>
3123
- <mj-text color="${GREY}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml(copy.footerOrg)}</mj-text>
3146
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()} ${escapeXml2(copy.footerOrg)}. All rights reserved.</mj-text>
3147
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="700" line-height="16px" padding-top="0" padding-bottom="0px">Our mailing address is:</mj-text>
3148
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml2(copy.footerOrg)}</mj-text>
3124
3149
  ${footerAddressRows}
3125
3150
  </mj-column>
3126
3151
  </mj-section>
@@ -3134,18 +3159,14 @@ var FREQ_PHRASE = {
3134
3159
  Quarterly: "every quarter",
3135
3160
  Yearly: "every year"
3136
3161
  };
3137
- var RED2 = "#C00";
3138
- var GREY2 = "#757575";
3139
- var CHECK_PNG2 = `cid:${CHECK_CID}`;
3140
- function fmtVisitors(n) {
3141
- return n.toLocaleString("en-US");
3162
+ var RED3 = "#C00";
3163
+ var GREY3 = "#757575";
3164
+ var SECTION_PAD = "40px";
3165
+ function sectionLabel(text) {
3166
+ return `<mj-text color="${RED3}" font-size="20px" font-weight="700" padding-top="0px">${escapeXml2(text)}</mj-text>`;
3142
3167
  }
3143
- function visitorTrend(cur, prev) {
3144
- if (cur === void 0 || prev === void 0 || prev === 0) return null;
3145
- const pct = Math.round((cur - prev) / prev * 100);
3146
- if (pct > 0) return `\u25B2 ${pct}% vs the previous month`;
3147
- if (pct < 0) return `\u25BC ${Math.abs(pct)}% vs the previous month`;
3148
- return "No change vs the previous month";
3168
+ function bodyLine(text, paddingTop = "8px") {
3169
+ return `<mj-text color="${GREY3}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="${paddingTop}">${escapeXml2(text)}</mj-text>`;
3149
3170
  }
3150
3171
  function announcementSiteExtras(site) {
3151
3172
  return {
@@ -3153,83 +3174,68 @@ function announcementSiteExtras(site) {
3153
3174
  improvements: { resendForms: true, svelte5: true }
3154
3175
  };
3155
3176
  }
3156
- var SCORE_PREVIEW = [
3157
- { label: "Performance", key: "performance" },
3158
- { label: "Readability (A11y)", key: "accessibility" },
3159
- { label: "Best Practices", key: "bestPractices" },
3160
- { label: "Site Structure", key: "seo" }
3161
- ];
3162
3177
  function buildAnnouncementMjml(data) {
3163
3178
  const copy = data.copy ?? DEFAULT_COPY;
3164
3179
  const previewText = "Your monthly report from Reddoor";
3180
+ const cad = data.cadence;
3181
+ const hasMaint = Boolean(cad && cad.maintenance !== "None");
3182
+ const hasTesting = Boolean(cad && cad.testing !== "None");
3165
3183
  const improvementItems = [];
3166
3184
  if (data.improvements?.resendForms) improvementItems.push(copy.announceImprovementResend);
3167
3185
  if (data.improvements?.svelte5) improvementItems.push(copy.announceImprovementSvelte5);
3168
- const improvementsSection = improvementItems.length > 0 ? `
3169
- <mj-section background-color="white">
3186
+ const hasImpr = improvementItems.length > 0;
3187
+ const BANDS = ["white", "#F4F4F4"];
3188
+ let bandN = 0;
3189
+ const nextBg = () => BANDS[bandN++ % 2];
3190
+ const introBg = nextBg();
3191
+ const maintBg = hasMaint ? nextBg() : "";
3192
+ const testBg = hasTesting ? nextBg() : "";
3193
+ const lighthouseBg = nextBg();
3194
+ const analyticsBg = nextBg();
3195
+ const improvementsBg = hasImpr ? nextBg() : "";
3196
+ const contactBg = nextBg();
3197
+ const maintenanceSection = cad && cad.maintenance !== "None" ? `
3198
+ <mj-section background-color="${maintBg}" padding-top="${SECTION_PAD}" padding-bottom="0px">
3170
3199
  <mj-column>
3171
- <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">RECENT IMPROVEMENTS</mj-text>
3172
- ${improvementItems.map(
3173
- (item) => `
3174
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px" padding-bottom="4px">\u2022 ${escapeXml(item)}</mj-text>`
3175
- ).join("")}
3200
+ ${sectionLabel("MAINTENANCE CHECKS")}
3201
+ ${bodyLine(`${copy.maintenanceIntro} We do this ${FREQ_PHRASE[cad.maintenance]}.${cad.testing === "None" ? ` ${copy.announceCadence}` : ""}`)}
3176
3202
  </mj-column>
3177
- </mj-section>` : "";
3178
- const cad = data.cadence;
3179
- const cadenceBlocks = [];
3180
- if (cad && cad.testing !== "None")
3181
- cadenceBlocks.push({
3182
- line: `${copy.announceTestingLabel} \u2014 ${FREQ_PHRASE[cad.testing]}`,
3183
- checks: copy.testingChecklist
3184
- });
3185
- if (cad && cad.maintenance !== "None")
3186
- cadenceBlocks.push({
3187
- line: `${copy.announceMaintenanceLabel} \u2014 ${FREQ_PHRASE[cad.maintenance]}`,
3188
- checks: copy.maintenanceChecks
3189
- });
3190
- const cadenceSection = cadenceBlocks.length > 0 ? `
3191
- <mj-section background-color="white">
3203
+ </mj-section>${checklistRowsSection(copy.maintenanceChecks, {
3204
+ background: maintBg,
3205
+ lastPaddingBottom: SECTION_PAD
3206
+ })}` : "";
3207
+ const testingSection = cad && cad.testing !== "None" ? `
3208
+ <mj-section background-color="${testBg}" padding-top="${SECTION_PAD}" padding-bottom="0px">
3192
3209
  <mj-column>
3193
- <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">${escapeXml(copy.announceCadenceHeading)}</mj-text>
3194
- ${cadenceBlocks.map(
3195
- (b) => `
3196
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="400" line-height="24px" padding-top="12px" padding-bottom="2px">\u2022 ${escapeXml(b.line)}</mj-text>${b.checks.map(
3197
- (c) => `
3198
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="14px" font-weight="300" line-height="22px" padding-top="1px" padding-bottom="0px" padding-left="16px">${escapeXml(c)} <img src="${CHECK_PNG2}" alt="\u2713" width="14" height="14" style="vertical-align:middle;display:inline-block;margin-left:2px;" /></mj-text>`
3199
- ).join("")}`
3200
- ).join("")}
3201
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="14px">${escapeXml(copy.announceCadence)}</mj-text>
3210
+ ${sectionLabel("TESTING")}
3211
+ ${bodyLine(`${copy.testingIntro} We run a full test ${FREQ_PHRASE[cad.testing]}. ${copy.announceCadence}`)}
3202
3212
  </mj-column>
3203
- </mj-section>` : "";
3204
- const scoreRows = SCORE_PREVIEW.map(
3205
- ({ label, key }) => `
3206
- <mj-text color="${RED2}" font-size="20px" font-weight="300" padding-top="25px">${label}</mj-text>
3207
- <mj-text color="${RED2}" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse[key]}</mj-text>`
3208
- ).join("");
3209
- const scoreNote = copy.announceScoreNote ? `
3210
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" font-style="italic" line-height="18px" padding-top="16px">${escapeXml(copy.announceScoreNote)}</mj-text>` : "";
3211
- const trend = visitorTrend(data.gaUsersCurrent, data.gaUsersPrevious);
3212
- const trafficRows = [];
3213
- if (data.gaUsersCurrent !== void 0)
3214
- trafficRows.push(`
3215
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="8px"><span style="color:${RED2};font-size:22px;font-weight:400;">${escapeXml(fmtVisitors(data.gaUsersCurrent))}</span> visitors in the last month${trend ? ` \u2014 ${escapeXml(trend)}` : ""}</mj-text>`);
3216
- if (data.searchPosition !== void 0)
3217
- trafficRows.push(`
3218
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px">Page 1 Google result (#${data.searchPosition}) for your brand search</mj-text>`);
3219
- const trafficSection = trafficRows.length > 0 ? `
3220
- <mj-section background-color="white">
3213
+ </mj-section>${checklistRowsSection(copy.testingChecklist, {
3214
+ background: testBg,
3215
+ lastPaddingBottom: SECTION_PAD
3216
+ })}` : "";
3217
+ const analytics = analyticsSection({
3218
+ current: data.gaUsersCurrent,
3219
+ previous: data.gaUsersPrevious,
3220
+ background: analyticsBg,
3221
+ pad: SECTION_PAD,
3222
+ bodyLines: data.searchPosition !== void 0 ? [`Page 1 Google result (#${data.searchPosition}) for your brand search`] : []
3223
+ });
3224
+ const improvementsSection = hasImpr ? `
3225
+ <mj-section background-color="${improvementsBg}" padding-top="${SECTION_PAD}" padding-bottom="${SECTION_PAD}">
3221
3226
  <mj-column>
3222
- <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">TRAFFIC &amp; SEARCH</mj-text>
3223
- ${trafficRows.join("")}
3227
+ ${sectionLabel("RECENT IMPROVEMENTS")}
3228
+ ${improvementItems.map((item) => bodyLine(item)).join("\n ")}
3229
+ ${bodyLine(copy.announceOpenDoor, "16px")}
3224
3230
  </mj-column>
3225
3231
  </mj-section>` : "";
3226
3232
  const contactRows = copy.contact.map(
3227
3233
  (line) => `
3228
- <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
3234
+ <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
3229
3235
  ).join("");
3230
3236
  const footerAddressRows = copy.footerAddress.map(
3231
3237
  (line) => `
3232
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml(line)}</mj-text>`
3238
+ <mj-text color="${GREY3}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml2(line)}</mj-text>`
3233
3239
  ).join("");
3234
3240
  return `<mjml>
3235
3241
  <mj-head>
@@ -3238,42 +3244,33 @@ function buildAnnouncementMjml(data) {
3238
3244
  <mj-section padding-left="11%" padding-right="11%"/>
3239
3245
  <mj-image padding="0px" />
3240
3246
  </mj-attributes>
3241
- <mj-preview>${escapeXml(previewText)}</mj-preview>
3247
+ <mj-preview>${escapeXml2(previewText)}</mj-preview>
3242
3248
  ${headerStyleBlock(data)}
3243
3249
  </mj-head>
3244
3250
  <mj-body background-color="white">
3245
3251
  <mj-section background-color="#F4F4F4" padding-top="0px" padding-bottom="0px" padding-left="0px" padding-right="0px">
3246
3252
  <mj-column>${headerImageTag(data)}</mj-column>
3247
3253
  </mj-section>
3248
- <mj-section background-color="white">
3254
+ <mj-section background-color="${introBg}" padding-top="${SECTION_PAD}" padding-bottom="${SECTION_PAD}">
3249
3255
  <mj-column>
3250
- <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="75px">${escapeXml(copy.announceHeading)}</mj-text>
3251
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="20px">Prepared for ${escapeXml(data.siteName)}</mj-text>
3252
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="8px">${escapeXml(copy.announceBody)}</mj-text>
3256
+ ${sectionLabel(copy.announceHeading)}
3257
+ <mj-text color="${GREY3}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="20px">Prepared for ${escapeXml2(data.siteName)}</mj-text>
3258
+ ${bodyLine(copy.announceBody)}
3253
3259
  </mj-column>
3254
3260
  </mj-section>
3255
- ${cadenceSection}
3261
+ ${maintenanceSection}
3262
+ ${testingSection}
3263
+ ${lighthouseScoresSection(data.lighthouse, { background: lighthouseBg, pad: SECTION_PAD })}
3264
+ ${analytics}
3256
3265
  ${improvementsSection}
3257
- <mj-section background-color="#F4F4F4">
3266
+ <mj-section background-color="${contactBg}" padding-top="${SECTION_PAD}" padding-bottom="${SECTION_PAD}">
3258
3267
  <mj-column>
3259
- <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="55px">${escapeXml(copy.announcePreviewLabel)}</mj-text>
3260
- ${scoreRows}${scoreNote}
3261
- </mj-column>
3262
- </mj-section>
3263
- ${trafficSection}
3264
- <mj-section background-color="white">
3265
- <mj-column>
3266
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="36px">${escapeXml(copy.announceOpenDoor)}</mj-text>
3267
- </mj-column>
3268
- </mj-section>
3269
- <mj-section background-color="white">
3270
- <mj-column padding-top="36px">
3271
- <mj-text color="${RED2}" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" padding-top="36px" line-height="36px">Any questions, concerns or requests?</mj-text>
3268
+ <mj-text color="${RED3}" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" padding-top="0px" line-height="36px">Any questions, concerns or requests?</mj-text>
3272
3269
  ${contactRows}
3273
3270
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
3274
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>
3275
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="700" line-height="16px" padding-top="0" padding-bottom="0px">Our mailing address is:</mj-text>
3276
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml(copy.footerOrg)}</mj-text>
3271
+ <mj-text color="${GREY3}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()} ${escapeXml2(copy.footerOrg)}. All rights reserved.</mj-text>
3272
+ <mj-text color="${GREY3}" font-family="helvetica, sans-serif" font-size="12px" font-weight="700" line-height="16px" padding-top="0" padding-bottom="0px">Our mailing address is:</mj-text>
3273
+ <mj-text color="${GREY3}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml2(copy.footerOrg)}</mj-text>
3277
3274
  ${footerAddressRows}
3278
3275
  </mj-column>
3279
3276
  </mj-section>
@@ -3443,6 +3440,33 @@ async function stampSent(base, recordId, sentAt, messageId) {
3443
3440
  ]);
3444
3441
  }
3445
3442
 
3443
+ // src/reports/queue.ts
3444
+ var REPORT_TIER = {
3445
+ Maintenance: 1,
3446
+ Testing: 2,
3447
+ Announcement: 3,
3448
+ Launch: 3
3449
+ };
3450
+ function reportTier(type) {
3451
+ return REPORT_TIER[type];
3452
+ }
3453
+ async function queueDraft(base, report) {
3454
+ const newTier = reportTier(report.reportType);
3455
+ const others = (await listReportsForSite(base, report.siteId)).filter(isPendingApproval).filter((r) => r.id !== report.id);
3456
+ const blocker = others.find((r) => reportTier(r.reportType) >= newTier);
3457
+ if (blocker) {
3458
+ await setDraftReady(base, report.id, false);
3459
+ return { queued: false, blockedBy: blocker.reportType, supersededIds: [] };
3460
+ }
3461
+ const supersededIds = [];
3462
+ for (const r of others) {
3463
+ await setDraftReady(base, r.id, false);
3464
+ supersededIds.push(r.id);
3465
+ }
3466
+ await setDraftReady(base, report.id, true);
3467
+ return { queued: true, supersededIds };
3468
+ }
3469
+
3446
3470
  // src/reports/airtable/attachments.ts
3447
3471
  function looksLikeHtml(bytes) {
3448
3472
  const start = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
@@ -3681,12 +3705,24 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
3681
3705
  const path = options.previewPath ?? `reports/${slug}/draft.html`;
3682
3706
  await mkdir3(dirname6(path), { recursive: true });
3683
3707
  await writeFile10(path, html, "utf-8");
3684
- return { reportRow: null, htmlPath: path, html, softFailures };
3708
+ return { reportRow: null, htmlPath: path, html, softFailures, queued: null, supersededIds: [] };
3685
3709
  }
3686
3710
  if (base === null) throw new Error("base required when previewOnly=false");
3687
3711
  if (options.completeRowId) {
3688
- await finishDraftRow(base, options.completeRowId, slug, periodEnd, html);
3689
- return { reportRow: options.existingRow ?? null, htmlPath: null, html, softFailures };
3712
+ await uploadDraftHtml(options.completeRowId, slug, periodEnd, html);
3713
+ const outcome2 = await queueDraft(base, {
3714
+ id: options.completeRowId,
3715
+ siteId: siteRow.id,
3716
+ reportType
3717
+ });
3718
+ return {
3719
+ reportRow: options.existingRow ?? null,
3720
+ htmlPath: null,
3721
+ html,
3722
+ softFailures,
3723
+ queued: outcome2.queued,
3724
+ supersededIds: outcome2.supersededIds
3725
+ };
3690
3726
  }
3691
3727
  const reportId = `${siteRow.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
3692
3728
  const created = await createDraft(base, {
@@ -3703,13 +3739,24 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
3703
3739
  ...search ? { searchFoundPage1: search.foundOnPage1 } : {},
3704
3740
  ...search?.foundOnPage1 && search.position !== null ? { searchPosition: search.position } : {}
3705
3741
  });
3706
- await finishDraftRow(base, created.id, slug, periodEnd, html);
3707
- return { reportRow: created, htmlPath: null, html, softFailures };
3742
+ await uploadDraftHtml(created.id, slug, periodEnd, html);
3743
+ const outcome = await queueDraft(base, {
3744
+ id: created.id,
3745
+ siteId: siteRow.id,
3746
+ reportType
3747
+ });
3748
+ return {
3749
+ reportRow: created,
3750
+ htmlPath: null,
3751
+ html,
3752
+ softFailures,
3753
+ queued: outcome.queued,
3754
+ supersededIds: outcome.supersededIds
3755
+ };
3708
3756
  }
3709
- async function finishDraftRow(base, rowId, slug, periodEnd, html) {
3757
+ async function uploadDraftHtml(rowId, slug, periodEnd, html) {
3710
3758
  const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
3711
3759
  await uploadAttachment(rowId, "Rendered HTML", html, htmlFilename, "text/html");
3712
- await setDraftReady(base, rowId, true);
3713
3760
  }
3714
3761
  var NO_ENRICHMENT = { value: null, softFailed: false };
3715
3762
  async function fetchGaUsers(siteRow, periodStart, periodEnd) {
@@ -3973,9 +4020,8 @@ async function sendOne(client, base, site, report) {
3973
4020
  headerHeight: header.displayHeight,
3974
4021
  headerBgColor: header.placeholderColor,
3975
4022
  // Announcement-only: re-derive cadence + improvements from the site row so the SENT email
3976
- // keeps its WHAT TO EXPECT section + improvement callouts. Without this the send-time
3977
- // re-render drops them entirely (they're not stored on the Reports row). Ignored by the
3978
- // other report templates.
4023
+ // keeps its cadence copy + improvement callouts. Without this the send-time re-render drops
4024
+ // them entirely (they're not stored on the Reports row). Ignored by the other templates.
3979
4025
  ...report.reportType === "Announcement" ? announcementSiteExtras(site) : {}
3980
4026
  });
3981
4027
  const reportDate = report.completedOn ? new Date(report.completedOn) : /* @__PURE__ */ new Date();