@reddoorla/maintenance 0.46.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/cli/bin.js CHANGED
@@ -161,6 +161,9 @@ function parseNotifyRouting(raw) {
161
161
  function isDashboardVisible(site) {
162
162
  return site.status !== null && ACTIVE_STATUSES.has(site.status);
163
163
  }
164
+ function toFrequency(raw) {
165
+ return typeof raw === "string" && FREQUENCIES.includes(raw) ? raw : "None";
166
+ }
164
167
  function mapRow(rec) {
165
168
  const f = rec.fields;
166
169
  const attachments = f["Header image"] ?? [];
@@ -171,8 +174,8 @@ function mapRow(rec) {
171
174
  url: String(f["url"] ?? ""),
172
175
  status: f["Status"] ?? null,
173
176
  pointOfContact: f["point of contact"] ?? null,
174
- maintenanceFreq: f["maintenence freq"] ?? "None",
175
- testingFreq: f["testing freq"] ?? "None",
177
+ maintenanceFreq: toFrequency(f["maintenence freq"]),
178
+ testingFreq: toFrequency(f["testing freq"]),
176
179
  maintenanceDay: f["maintenance day"] ?? null,
177
180
  testingDay: f["testing day"] ?? null,
178
181
  ga4PropertyId: f["GA4 property ID"] ?? null,
@@ -295,7 +298,7 @@ async function updateLaunched(base, recordId, at) {
295
298
  const fields = { Status: "maintenance", "Launched at": at };
296
299
  await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
297
300
  }
298
- var WEBSITES_TABLE, ACTIVE_STATUSES;
301
+ var WEBSITES_TABLE, ACTIVE_STATUSES, FREQUENCIES;
299
302
  var init_websites = __esm({
300
303
  "src/reports/airtable/websites.ts"() {
301
304
  "use strict";
@@ -304,6 +307,7 @@ var init_websites = __esm({
304
307
  "maintenance",
305
308
  "launch period"
306
309
  ]);
310
+ FREQUENCIES = ["None", "Monthly", "Quarterly", "Yearly"];
307
311
  }
308
312
  });
309
313
 
@@ -607,7 +611,10 @@ var init_checklist = __esm({
607
611
  label: "Interactions & Animations",
608
612
  field: "Test: Interactions & Animations"
609
613
  },
610
- { key: "updates", label: "Verified After Updates", field: "Test: Verified After Updates" }
614
+ // `field` keeps its original Airtable column name ("Verified After Updates") even though
615
+ // the client-facing label is now "Tested After Updates" — the column holds operator data,
616
+ // so renaming the label is display-only and avoids a live-base column migration.
617
+ { key: "updates", label: "Tested After Updates", field: "Test: Verified After Updates" }
611
618
  ];
612
619
  ALL_CHECKLIST_FIELDS = [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map(
613
620
  (i) => i.field
@@ -701,7 +708,7 @@ async function createDraft(base, input) {
701
708
  async function setDraftReady(base, recordId, ready) {
702
709
  await base(REPORTS_TABLE).update([{ id: recordId, fields: { "Draft ready": ready } }]);
703
710
  }
704
- async function updateReportScores(base, recordId, scores, completedOn) {
711
+ async function updateReportScores(base, recordId, scores, completedOn, enrichment) {
705
712
  const fields = {
706
713
  "Lighthouse \u2014 Performance": scores.performance,
707
714
  "Lighthouse \u2014 Accessibility": scores.accessibility,
@@ -709,6 +716,14 @@ async function updateReportScores(base, recordId, scores, completedOn) {
709
716
  "Lighthouse \u2014 SEO": scores.seo
710
717
  };
711
718
  if (completedOn) fields["Completed on"] = ymd(completedOn);
719
+ if (enrichment?.gaUsersCurrent !== void 0)
720
+ fields["GA users (period)"] = enrichment.gaUsersCurrent;
721
+ if (enrichment?.gaUsersPrevious !== void 0)
722
+ fields["GA users (prev period)"] = enrichment.gaUsersPrevious;
723
+ if (enrichment?.searchFoundPage1 !== void 0)
724
+ fields["Search found page 1"] = enrichment.searchFoundPage1;
725
+ if (enrichment?.searchPosition !== void 0)
726
+ fields["Search position"] = enrichment.searchPosition;
712
727
  await base(REPORTS_TABLE).update([{ id: recordId, fields }]);
713
728
  }
714
729
  async function listSendableReports(base) {
@@ -808,7 +823,7 @@ var init_copy = __esm({
808
823
  "Links & Navigation",
809
824
  "Form Functionality",
810
825
  "Interactions & Animations",
811
- "Verified After Updates"
826
+ "Tested After Updates"
812
827
  ],
813
828
  notesHeader: "NOTES",
814
829
  seoCta: "Contact us if you are interested in more in-depth data or have questions about SEO.",
@@ -824,14 +839,10 @@ var init_copy = __esm({
824
839
  ],
825
840
  announceHeading: "YOUR ONGOING SITE CARE",
826
841
  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:",
827
- announceCadenceHeading: "WHAT TO EXPECT",
828
- announceTestingLabel: "Full site testing",
829
- announceMaintenanceLabel: "Routine maintenance",
830
- announcePreviewLabel: "From your latest full site test:",
831
842
  announceImprovementResend: "Your contact forms now deliver straight to your inbox through reliable infrastructure, so no inquiry slips through the cracks.",
832
843
  announceImprovementSvelte5: "We've modernized your site to the latest framework \u2014 it's faster, more secure, and built to last.",
833
844
  announceCadence: "After each one we'll send you a short report like this \u2014 there's nothing you need to do.",
834
- 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."
845
+ announceOpenDoor: "And if you'd ever like to expand the scope, add features, or freshen anything up, just let us know."
835
846
  };
836
847
  }
837
848
  });
@@ -905,65 +916,124 @@ var init_html = __esm({
905
916
  }
906
917
  });
907
918
 
908
- // src/reports/maintenance-email/template.ts
909
- function fmtDate(d) {
910
- if (!d || Number.isNaN(d.getTime())) return "";
911
- const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
912
- const dd = String(d.getUTCDate()).padStart(2, "0");
913
- const yyyy = d.getUTCFullYear();
914
- return `${mm}.${dd}.${yyyy}`;
915
- }
919
+ // src/reports/email-sections.ts
916
920
  function fmtUsers(n) {
917
921
  return n.toLocaleString("en-US");
918
922
  }
919
- function trendText(color, text) {
920
- return `<mj-text color="${color}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${text}</mj-text>`;
923
+ function checklistRowsSection(rows, opts) {
924
+ return rows.map((label, i) => {
925
+ const isLast = i === rows.length - 1;
926
+ const border = isLast ? "" : ` border-bottom="solid ${BORDER} 1px"`;
927
+ const lastPad = isLast ? ` padding-bottom="${opts.lastPaddingBottom}"` : "";
928
+ return `
929
+ <mj-section background-color="${opts.background}" padding="0px"${lastPad}>
930
+ <mj-group>
931
+ <mj-column padding-left="0px" width="90%"${border}>
932
+ <mj-text height="25px" padding-left="0px" color="${GREY}" padding-top="20px" padding-bottom="7.5px" font-size="16px">${escapeXml(label)}</mj-text>
933
+ </mj-column>
934
+ <mj-column width="10%"${border} padding-top="15px">
935
+ <mj-image align="right" padding-right="0px" width="20px" height="20px" padding-top="2.5px" padding-bottom="15px" src="${CHECK_PNG}" />
936
+ </mj-column>
937
+ </mj-group>
938
+ </mj-section>`;
939
+ }).join("");
940
+ }
941
+ function lighthouseScoresSection(lighthouse2, opts = {}) {
942
+ const background = opts.background ?? "#F4F4F4";
943
+ const sectionPad = opts.pad ? ` padding-top="${opts.pad}" padding-bottom="${opts.pad}"` : "";
944
+ const labelTop = opts.pad ?? "55px";
945
+ const footnoteBottom = opts.pad ? "0px" : "36px";
946
+ const rows = LIGHTHOUSE_ROWS.map(
947
+ ({ label, key, range }, i) => `
948
+ <mj-text color="${RED}" font-size="20px" font-weight="300" padding-top="25px">${label}</mj-text>
949
+ <mj-text color="${RED}" font-size="44px" font-weight="400" padding-top="0px">${lighthouse2[key]}</mj-text>
950
+ <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 ? `
951
+ <mj-divider border-width="1px" border-style="solid" border-color="${BORDER}" padding="0" />` : ""}`
952
+ ).join("");
953
+ return `
954
+ <mj-section background-color="${background}"${sectionPad}>
955
+ <mj-column>
956
+ <mj-text color="${RED}" font-size="20px" font-weight="700" padding-top="${labelTop}">LIGHTHOUSE SCORES*</mj-text>${rows}
957
+ <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>
958
+ </mj-column>
959
+ </mj-section>`;
921
960
  }
922
961
  function analyticsTrendLine(cur, prev) {
923
962
  if (cur === void 0 || prev === void 0) {
924
- return trendText(TREND_NEUTRAL, `Last Period: ${prev !== void 0 ? fmtUsers(prev) : "\u2014"}`);
963
+ return trendLine(TREND_NEUTRAL, `Last Period: ${prev !== void 0 ? fmtUsers(prev) : "\u2014"}`);
925
964
  }
926
965
  if (prev === 0) {
927
- return cur > 0 ? trendText(TREND_UP, "\u25B2 New this period (0 last period)") : trendText(TREND_NEUTRAL, "Last Period: 0");
966
+ return cur > 0 ? trendLine(TREND_UP, "\u25B2 New this period (0 last period)") : trendLine(TREND_NEUTRAL, "Last Period: 0");
928
967
  }
929
968
  const pct = Math.round((cur - prev) / prev * 100);
930
969
  const range = `(${fmtUsers(prev)} \u2192 ${fmtUsers(cur)})`;
931
- if (pct > 0) return trendText(TREND_UP, `\u25B2 ${pct}% vs last period ${range}`);
932
- if (pct < 0) return trendText(TREND_NEUTRAL, `\u25BC ${Math.abs(pct)}% vs last period ${range}`);
933
- return trendText(TREND_NEUTRAL, `No change vs last period (${fmtUsers(prev)})`);
970
+ if (pct > 0) return trendLine(TREND_UP, `\u25B2 ${pct}% vs last period ${range}`);
971
+ if (pct < 0) return trendLine(TREND_NEUTRAL, `\u25BC ${Math.abs(pct)}% vs last period ${range}`);
972
+ return trendLine(TREND_NEUTRAL, `No change vs last period (${fmtUsers(prev)})`);
973
+ }
974
+ function trendLine(color, text) {
975
+ return `<mj-text color="${color}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${text}</mj-text>`;
976
+ }
977
+ function footnoteLine(text) {
978
+ 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>`;
979
+ }
980
+ function analyticsSection(opts) {
981
+ const users = opts.current !== void 0 ? fmtUsers(opts.current) : "\u2014";
982
+ const body = (opts.bodyLines ?? []).map((l) => trendLine(TREND_NEUTRAL, l)).join("\n ");
983
+ const footnotes = (opts.footnoteLines ?? []).map(footnoteLine).join("\n ");
984
+ const sectionPad = opts.pad ? ` padding-top="${opts.pad}" padding-bottom="${opts.pad}"` : "";
985
+ const labelTop = opts.pad ?? "75px";
986
+ return `
987
+ <mj-section background-color="${opts.background}"${sectionPad}>
988
+ <mj-column>
989
+ <mj-text color="${RED}" font-size="20px" font-weight="700" padding-top="${labelTop}">ANALYTICS</mj-text>
990
+ <mj-text color="${RED}" font-size="44px" font-weight="400">${users} Users</mj-text>
991
+ ${analyticsTrendLine(opts.current, opts.previous)}
992
+ ${body}
993
+ ${footnotes}
994
+ </mj-column>
995
+ </mj-section>`;
996
+ }
997
+ var escapeXml, RED, GREY, BORDER, TREND_UP, TREND_NEUTRAL, CHECK_PNG, LIGHTHOUSE_ROWS;
998
+ var init_email_sections = __esm({
999
+ "src/reports/email-sections.ts"() {
1000
+ "use strict";
1001
+ init_html();
1002
+ init_assets();
1003
+ escapeXml = escapeHtml;
1004
+ RED = "#C00";
1005
+ GREY = "#757575";
1006
+ BORDER = "#CCCCCC";
1007
+ TREND_UP = "#2E7D32";
1008
+ TREND_NEUTRAL = GREY;
1009
+ CHECK_PNG = `cid:${CHECK_CID}`;
1010
+ LIGHTHOUSE_ROWS = [
1011
+ { label: "Performance", key: "performance", range: "Acceptable 50\u201389 // Ideal 90\u2013100" },
1012
+ { label: "Readability (A11y)", key: "accessibility", range: "Acceptable 80\u201399 // Ideal 100" },
1013
+ { label: "Best Practices", key: "bestPractices", range: "Acceptable 60\u201379 // Ideal 80\u2013100" },
1014
+ { label: "Site Structure", key: "seo", range: "Acceptable 50\u201389 // Ideal 90\u2013100" }
1015
+ ];
1016
+ }
1017
+ });
1018
+
1019
+ // src/reports/maintenance-email/template.ts
1020
+ function fmtDate(d) {
1021
+ if (!d || Number.isNaN(d.getTime())) return "";
1022
+ const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
1023
+ const dd = String(d.getUTCDate()).padStart(2, "0");
1024
+ const yyyy = d.getUTCFullYear();
1025
+ return `${mm}.${dd}.${yyyy}`;
934
1026
  }
935
1027
  function maintenanceChecksSection(copy, searchPosition) {
936
1028
  const googleLabel = searchPosition !== void 0 ? `Page 1 Google Result (#${searchPosition})` : copy.maintenanceChecks[3] ?? "";
937
1029
  const rows = copy.maintenanceChecks.map((label, i) => i === 3 ? googleLabel : label);
938
- return rows.map(
939
- (label, i) => `
940
- <mj-section background-color="white" padding="0px"${i === rows.length - 1 ? ' padding-bottom="36px"' : ""}>
941
- <mj-group>
942
- <mj-column padding-left="0px" width="90%"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""}>
943
- <mj-text height="25px" padding-left="0px" color="#757575" padding-top="20px" padding-bottom="7.5px" font-size="16px">${escapeXml(label)}</mj-text>
944
- </mj-column>
945
- <mj-column width="10%"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""} padding-top="15px">
946
- <mj-image align="right" padding-right="0px" width="20px" height="20px" padding-top="2.5px" padding-bottom="15px" src="${CHECK_PNG}" />
947
- </mj-column>
948
- </mj-group>
949
- </mj-section>`
950
- ).join("");
1030
+ return checklistRowsSection(rows, { background: "white", lastPaddingBottom: "36px" });
951
1031
  }
952
1032
  function testingChecklistSection(copy) {
953
- const rows = copy.testingChecklist;
954
- return rows.map(
955
- (label, i) => `
956
- <mj-section background-color="#F4F4F4" padding="0px"${i === rows.length - 1 ? ' padding-bottom="60px"' : ""}>
957
- <mj-group>
958
- <mj-column width="90%" padding-left="0px"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""}>
959
- <mj-text height="25px" padding-left="0px" color="#757575" padding-top="20px" padding-bottom="7.5px" font-size="16px">${escapeXml(label)}</mj-text>
960
- </mj-column>
961
- <mj-column width="10%"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""} padding-top="15px">
962
- <mj-image align="right" padding-right="0px" width="20px" height="20px" padding-top="2.5px" padding-bottom="15px" src="${CHECK_PNG}" />
963
- </mj-column>
964
- </mj-group>
965
- </mj-section>`
966
- ).join("");
1033
+ return checklistRowsSection(copy.testingChecklist, {
1034
+ background: "#F4F4F4",
1035
+ lastPaddingBottom: "60px"
1036
+ });
967
1037
  }
968
1038
  function maintenanceTestingPlaceholder(lastTested) {
969
1039
  return `
@@ -983,7 +1053,7 @@ function testingIntroSection(copy) {
983
1053
  <mj-section background-color="#F4F4F4">
984
1054
  <mj-column>
985
1055
  <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">TESTING</mj-text>
986
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml(copy.testingIntro)}</mj-text>
1056
+ <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml2(copy.testingIntro)}</mj-text>
987
1057
  </mj-column>
988
1058
  </mj-section>`;
989
1059
  }
@@ -991,8 +1061,8 @@ function commentarySection(text, copy) {
991
1061
  return `
992
1062
  <mj-section background-color="white">
993
1063
  <mj-column>
994
- <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="55px">${escapeXml(copy.notesHeader)}</mj-text>
995
- <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>
1064
+ <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="55px">${escapeXml2(copy.notesHeader)}</mj-text>
1065
+ <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>
996
1066
  </mj-column>
997
1067
  </mj-section>`;
998
1068
  }
@@ -1001,8 +1071,8 @@ function hasHeaderDims(data) {
1001
1071
  }
1002
1072
  function headerImageTag(data) {
1003
1073
  const src = `cid:${data.headerImageCid}`;
1004
- const alt = `${escapeXml(data.siteName)} maintenance report`;
1005
- const href = isHttpUrl(data.siteUrl) ? escapeXml(data.siteUrl) : "#";
1074
+ const alt = `${escapeXml2(data.siteName)} maintenance report`;
1075
+ const href = isHttpUrl(data.siteUrl) ? escapeXml2(data.siteUrl) : "#";
1006
1076
  if (hasHeaderDims(data)) {
1007
1077
  return `<mj-image href="${href}" src="${src}" alt="${alt}" width="${data.headerWidth}px" css-class="rd-header" container-background-color="${data.headerBgColor}" />`;
1008
1078
  }
@@ -1015,7 +1085,7 @@ function headerStyleBlock(data) {
1015
1085
  function buildMjml(data) {
1016
1086
  const copy = data.copy ?? DEFAULT_COPY;
1017
1087
  const isTesting = data.reportType === "Testing";
1018
- const previewText = `Checked up on ${escapeXml(data.siteName)}`;
1088
+ const previewText = `Checked up on ${escapeXml2(data.siteName)}`;
1019
1089
  return `<mjml>
1020
1090
  <mj-head>
1021
1091
  <mj-attributes>
@@ -1037,89 +1107,65 @@ function buildMjml(data) {
1037
1107
  <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">COMPLETED ON</mj-text>
1038
1108
  <mj-text color="#C00" font-size="44px" font-weight="400">${fmtDate(data.completedOn)}</mj-text>
1039
1109
  <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">MAINTENANCE CHECKS</mj-text>
1040
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml(copy.maintenanceIntro)}</mj-text>
1110
+ <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml2(copy.maintenanceIntro)}</mj-text>
1041
1111
  </mj-column>
1042
1112
  </mj-section>
1043
1113
  ${maintenanceChecksSection(copy, data.searchPosition)}
1044
- <mj-section background-color="#F4F4F4">
1045
- <mj-column>
1046
- <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="55px">LIGHTHOUSE SCORES*</mj-text>
1047
- <mj-text color="#C00" font-size="20px" font-weight="300" padding-top="25px">Performance</mj-text>
1048
- <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.performance}</mj-text>
1049
- <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>
1050
- <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
1051
- <mj-text color="#C00" font-size="20px" font-weight="300" padding-top="25px">Readability</mj-text>
1052
- <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.accessibility}</mj-text>
1053
- <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>
1054
- <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
1055
- <mj-text color="#C00" font-size="20px" font-weight="300" padding-top="25px">Best Practices</mj-text>
1056
- <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.bestPractices}</mj-text>
1057
- <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>
1058
- <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
1059
- <mj-text color="#C00" font-size="20px" font-weight="300" padding-top="25px">Site Structure</mj-text>
1060
- <mj-text color="#C00" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse.seo}</mj-text>
1061
- <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>
1062
- <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>
1063
- </mj-column>
1064
- </mj-section>
1065
- <mj-section background-color="white">
1066
- <mj-column>
1067
- <mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">ANALYTICS</mj-text>
1068
- <mj-text color="#C00" font-size="44px" font-weight="400">${data.gaUsersCurrent !== void 0 ? fmtUsers(data.gaUsersCurrent) : "\u2014"} Users</mj-text>
1069
- ${analyticsTrendLine(data.gaUsersCurrent, data.gaUsersPrevious)}
1070
- <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>
1071
- </mj-column>
1072
- </mj-section>
1114
+ ${lighthouseScoresSection(data.lighthouse)}
1115
+ ${analyticsSection({
1116
+ current: data.gaUsersCurrent,
1117
+ previous: data.gaUsersPrevious,
1118
+ background: "white",
1119
+ footnoteLines: [escapeXml2(copy.seoCta)]
1120
+ })}
1073
1121
  ${isTesting ? testingIntroSection(copy) + testingChecklistSection(copy) : maintenanceTestingPlaceholder(data.lastTestedDate)}
1074
1122
  ${data.commentary ? commentarySection(data.commentary, copy) : ""}
1075
1123
  <mj-section background-color="white">
1076
1124
  <mj-column padding-top="36px">
1077
1125
  <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>
1078
1126
  ${copy.contact.map(
1079
- (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>`
1127
+ (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>`
1080
1128
  ).join("\n ")}
1081
1129
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
1082
- <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>
1130
+ <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>
1083
1131
  <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>
1084
1132
  ${[copy.footerOrg, ...copy.footerAddress].map(
1085
- (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>`
1133
+ (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>`
1086
1134
  ).join("\n ")}
1087
1135
  </mj-column>
1088
1136
  </mj-section>
1089
1137
  </mj-body>
1090
1138
  </mjml>`;
1091
1139
  }
1092
- var escapeXml, CHECK_PNG, BLURRED_TESTS, TREND_UP, TREND_NEUTRAL;
1140
+ var escapeXml2, BLURRED_TESTS;
1093
1141
  var init_template = __esm({
1094
1142
  "src/reports/maintenance-email/template.ts"() {
1095
1143
  "use strict";
1096
1144
  init_copy();
1097
1145
  init_assets();
1146
+ init_email_sections();
1098
1147
  init_html();
1099
1148
  init_url();
1100
- escapeXml = escapeHtml;
1101
- CHECK_PNG = `cid:${CHECK_CID}`;
1149
+ escapeXml2 = escapeHtml;
1102
1150
  BLURRED_TESTS = `cid:${BLURRED_CID}`;
1103
- TREND_UP = "#2E7D32";
1104
- TREND_NEUTRAL = "#757575";
1105
1151
  }
1106
1152
  });
1107
1153
 
1108
1154
  // src/reports/launch-email/template.ts
1109
1155
  function buildLaunchMjml(data) {
1110
1156
  const copy = data.copy ?? DEFAULT_COPY;
1111
- const previewText = `${escapeXml(data.siteName)} is live`;
1157
+ const previewText = `${escapeXml2(data.siteName)} is live`;
1112
1158
  const setupRows = copy.launchSetupItems.map(
1113
1159
  (item) => `
1114
- <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>`
1160
+ <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>`
1115
1161
  ).join("");
1116
1162
  const contactRows = copy.contact.map(
1117
1163
  (line) => `
1118
- <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
1164
+ <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
1119
1165
  ).join("");
1120
1166
  const footerAddressRows = copy.footerAddress.map(
1121
1167
  (line) => `
1122
- <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>`
1168
+ <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>`
1123
1169
  ).join("");
1124
1170
  return `<mjml>
1125
1171
  <mj-head>
@@ -1137,90 +1183,112 @@ function buildLaunchMjml(data) {
1137
1183
  </mj-section>
1138
1184
  <mj-section background-color="white">
1139
1185
  <mj-column>
1140
- <mj-text color="${RED}" font-size="20px" font-weight="700" padding-top="75px">${escapeXml(copy.launchHeading)}</mj-text>
1141
- <mj-text color="${RED}" font-size="44px" font-weight="400">${fmtDate(data.completedOn)}</mj-text>
1142
- <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>
1186
+ <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="75px">${escapeXml2(copy.launchHeading)}</mj-text>
1187
+ <mj-text color="${RED2}" font-size="44px" font-weight="400">${fmtDate(data.completedOn)}</mj-text>
1188
+ <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>
1143
1189
  ${setupRows}
1144
1190
  </mj-column>
1145
1191
  </mj-section>
1146
1192
  <mj-section background-color="white">
1147
1193
  <mj-column padding-top="36px">
1148
- <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>
1194
+ <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>
1149
1195
  ${contactRows}
1150
1196
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
1151
- <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>
1152
- <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>
1153
- <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>
1197
+ <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>
1198
+ <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>
1199
+ <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>
1154
1200
  ${footerAddressRows}
1155
1201
  </mj-column>
1156
1202
  </mj-section>
1157
1203
  </mj-body>
1158
1204
  </mjml>`;
1159
1205
  }
1160
- var RED, GREY;
1206
+ var RED2, GREY2;
1161
1207
  var init_template2 = __esm({
1162
1208
  "src/reports/launch-email/template.ts"() {
1163
1209
  "use strict";
1164
1210
  init_copy();
1165
1211
  init_template();
1166
- RED = "#C00";
1167
- GREY = "#757575";
1212
+ RED2 = "#C00";
1213
+ GREY2 = "#757575";
1168
1214
  }
1169
1215
  });
1170
1216
 
1171
1217
  // src/reports/announcement-email/template.ts
1218
+ function sectionLabel(text) {
1219
+ return `<mj-text color="${RED3}" font-size="20px" font-weight="700" padding-top="0px">${escapeXml2(text)}</mj-text>`;
1220
+ }
1221
+ function bodyLine(text, paddingTop = "8px") {
1222
+ 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>`;
1223
+ }
1224
+ function announcementSiteExtras(site) {
1225
+ return {
1226
+ cadence: { maintenance: site.maintenanceFreq, testing: site.testingFreq },
1227
+ improvements: { resendForms: true, svelte5: true }
1228
+ };
1229
+ }
1172
1230
  function buildAnnouncementMjml(data) {
1173
1231
  const copy = data.copy ?? DEFAULT_COPY;
1174
1232
  const previewText = "Your monthly report from Reddoor";
1233
+ const cad = data.cadence;
1234
+ const hasMaint = Boolean(cad && cad.maintenance !== "None");
1235
+ const hasTesting = Boolean(cad && cad.testing !== "None");
1175
1236
  const improvementItems = [];
1176
1237
  if (data.improvements?.resendForms) improvementItems.push(copy.announceImprovementResend);
1177
1238
  if (data.improvements?.svelte5) improvementItems.push(copy.announceImprovementSvelte5);
1178
- const improvementsSection = improvementItems.length > 0 ? `
1179
- <mj-section background-color="white">
1239
+ const hasImpr = improvementItems.length > 0;
1240
+ const BANDS = ["white", "#F4F4F4"];
1241
+ let bandN = 0;
1242
+ const nextBg = () => BANDS[bandN++ % 2];
1243
+ const introBg = nextBg();
1244
+ const maintBg = hasMaint ? nextBg() : "";
1245
+ const testBg = hasTesting ? nextBg() : "";
1246
+ const lighthouseBg = nextBg();
1247
+ const analyticsBg = nextBg();
1248
+ const improvementsBg = hasImpr ? nextBg() : "";
1249
+ const contactBg = nextBg();
1250
+ const maintenanceSection = cad && cad.maintenance !== "None" ? `
1251
+ <mj-section background-color="${maintBg}" padding-top="${SECTION_PAD}" padding-bottom="0px">
1180
1252
  <mj-column>
1181
- <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">RECENT IMPROVEMENTS</mj-text>
1182
- ${improvementItems.map(
1183
- (item) => `
1184
- <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>`
1185
- ).join("")}
1253
+ ${sectionLabel("MAINTENANCE CHECKS")}
1254
+ ${bodyLine(`${copy.maintenanceIntro} We do this ${FREQ_PHRASE[cad.maintenance]}.${cad.testing === "None" ? ` ${copy.announceCadence}` : ""}`)}
1186
1255
  </mj-column>
1187
- </mj-section>` : "";
1188
- const cad = data.cadence;
1189
- const cadenceBlocks = [];
1190
- if (cad && cad.testing !== "None")
1191
- cadenceBlocks.push({
1192
- line: `${copy.announceTestingLabel} \u2014 ${FREQ_PHRASE[cad.testing]}`,
1193
- checks: copy.testingChecklist
1194
- });
1195
- if (cad && cad.maintenance !== "None")
1196
- cadenceBlocks.push({
1197
- line: `${copy.announceMaintenanceLabel} \u2014 ${FREQ_PHRASE[cad.maintenance]}`,
1198
- checks: copy.maintenanceChecks
1199
- });
1200
- const cadenceSection = cadenceBlocks.length > 0 ? `
1201
- <mj-section background-color="white">
1256
+ </mj-section>${checklistRowsSection(copy.maintenanceChecks, {
1257
+ background: maintBg,
1258
+ lastPaddingBottom: SECTION_PAD
1259
+ })}` : "";
1260
+ const testingSection = cad && cad.testing !== "None" ? `
1261
+ <mj-section background-color="${testBg}" padding-top="${SECTION_PAD}" padding-bottom="0px">
1202
1262
  <mj-column>
1203
- <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">${escapeXml(copy.announceCadenceHeading)}</mj-text>
1204
- ${cadenceBlocks.map(
1205
- (b) => `
1206
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="400" line-height="24px" padding-top="12px" padding-bottom="0px">\u2022 ${escapeXml(b.line)}</mj-text>
1207
- <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="13px" font-weight="300" line-height="20px" padding-top="2px" padding-bottom="0px" padding-left="16px">${escapeXml(b.checks.join(" \xB7 "))}</mj-text>`
1208
- ).join("")}
1209
- <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>
1263
+ ${sectionLabel("TESTING")}
1264
+ ${bodyLine(`${copy.testingIntro} We run a full test ${FREQ_PHRASE[cad.testing]}. ${copy.announceCadence}`)}
1265
+ </mj-column>
1266
+ </mj-section>${checklistRowsSection(copy.testingChecklist, {
1267
+ background: testBg,
1268
+ lastPaddingBottom: SECTION_PAD
1269
+ })}` : "";
1270
+ const analytics = analyticsSection({
1271
+ current: data.gaUsersCurrent,
1272
+ previous: data.gaUsersPrevious,
1273
+ background: analyticsBg,
1274
+ pad: SECTION_PAD,
1275
+ bodyLines: data.searchPosition !== void 0 ? [`Page 1 Google result (#${data.searchPosition}) for your brand search`] : []
1276
+ });
1277
+ const improvementsSection = hasImpr ? `
1278
+ <mj-section background-color="${improvementsBg}" padding-top="${SECTION_PAD}" padding-bottom="${SECTION_PAD}">
1279
+ <mj-column>
1280
+ ${sectionLabel("RECENT IMPROVEMENTS")}
1281
+ ${improvementItems.map((item) => bodyLine(item)).join("\n ")}
1282
+ ${bodyLine(copy.announceOpenDoor, "16px")}
1210
1283
  </mj-column>
1211
1284
  </mj-section>` : "";
1212
- const scoreRows = SCORE_PREVIEW.map(
1213
- ({ label, key }) => `
1214
- <mj-text color="${RED2}" font-size="20px" font-weight="300" padding-top="25px">${label}</mj-text>
1215
- <mj-text color="${RED2}" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse[key]}</mj-text>`
1216
- ).join("");
1217
1285
  const contactRows = copy.contact.map(
1218
1286
  (line) => `
1219
- <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
1287
+ <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
1220
1288
  ).join("");
1221
1289
  const footerAddressRows = copy.footerAddress.map(
1222
1290
  (line) => `
1223
- <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>`
1291
+ <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>`
1224
1292
  ).join("");
1225
1293
  return `<mjml>
1226
1294
  <mj-head>
@@ -1229,66 +1297,54 @@ function buildAnnouncementMjml(data) {
1229
1297
  <mj-section padding-left="11%" padding-right="11%"/>
1230
1298
  <mj-image padding="0px" />
1231
1299
  </mj-attributes>
1232
- <mj-preview>${escapeXml(previewText)}</mj-preview>
1300
+ <mj-preview>${escapeXml2(previewText)}</mj-preview>
1233
1301
  ${headerStyleBlock(data)}
1234
1302
  </mj-head>
1235
1303
  <mj-body background-color="white">
1236
1304
  <mj-section background-color="#F4F4F4" padding-top="0px" padding-bottom="0px" padding-left="0px" padding-right="0px">
1237
1305
  <mj-column>${headerImageTag(data)}</mj-column>
1238
1306
  </mj-section>
1239
- <mj-section background-color="white">
1307
+ <mj-section background-color="${introBg}" padding-top="${SECTION_PAD}" padding-bottom="${SECTION_PAD}">
1240
1308
  <mj-column>
1241
- <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="75px">${escapeXml(copy.announceHeading)}</mj-text>
1242
- <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>
1243
- <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>
1309
+ ${sectionLabel(copy.announceHeading)}
1310
+ <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>
1311
+ ${bodyLine(copy.announceBody)}
1244
1312
  </mj-column>
1245
1313
  </mj-section>
1246
- ${cadenceSection}
1314
+ ${maintenanceSection}
1315
+ ${testingSection}
1316
+ ${lighthouseScoresSection(data.lighthouse, { background: lighthouseBg, pad: SECTION_PAD })}
1317
+ ${analytics}
1247
1318
  ${improvementsSection}
1248
- <mj-section background-color="#F4F4F4">
1249
- <mj-column>
1250
- <mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="55px">${escapeXml(copy.announcePreviewLabel)}</mj-text>
1251
- ${scoreRows}
1252
- </mj-column>
1253
- </mj-section>
1254
- <mj-section background-color="white">
1319
+ <mj-section background-color="${contactBg}" padding-top="${SECTION_PAD}" padding-bottom="${SECTION_PAD}">
1255
1320
  <mj-column>
1256
- <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>
1257
- </mj-column>
1258
- </mj-section>
1259
- <mj-section background-color="white">
1260
- <mj-column padding-top="36px">
1261
- <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>
1321
+ <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>
1262
1322
  ${contactRows}
1263
1323
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
1264
- <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>
1265
- <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>
1266
- <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>
1324
+ <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>
1325
+ <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>
1326
+ <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>
1267
1327
  ${footerAddressRows}
1268
1328
  </mj-column>
1269
1329
  </mj-section>
1270
1330
  </mj-body>
1271
1331
  </mjml>`;
1272
1332
  }
1273
- var FREQ_PHRASE, RED2, GREY2, SCORE_PREVIEW;
1333
+ var FREQ_PHRASE, RED3, GREY3, SECTION_PAD;
1274
1334
  var init_template3 = __esm({
1275
1335
  "src/reports/announcement-email/template.ts"() {
1276
1336
  "use strict";
1277
1337
  init_copy();
1278
1338
  init_template();
1339
+ init_email_sections();
1279
1340
  FREQ_PHRASE = {
1280
1341
  Monthly: "every month",
1281
1342
  Quarterly: "every quarter",
1282
1343
  Yearly: "every year"
1283
1344
  };
1284
- RED2 = "#C00";
1285
- GREY2 = "#757575";
1286
- SCORE_PREVIEW = [
1287
- { label: "Performance", key: "performance" },
1288
- { label: "Readability", key: "accessibility" },
1289
- { label: "Best Practices", key: "bestPractices" },
1290
- { label: "Site Structure", key: "seo" }
1291
- ];
1345
+ RED3 = "#C00";
1346
+ GREY3 = "#757575";
1347
+ SECTION_PAD = "40px";
1292
1348
  }
1293
1349
  });
1294
1350
 
@@ -1598,16 +1654,16 @@ __export(digest_exports, {
1598
1654
  runDigest: () => runDigest
1599
1655
  });
1600
1656
  function readySection(items) {
1601
- const heading = `<h2 style="color:${RED3};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px">Ready for your yes</h2>`;
1657
+ const heading = `<h2 style="color:${RED4};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px">Ready for your yes</h2>`;
1602
1658
  if (items.length === 0) {
1603
- return `${heading}<p style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;margin:0">Nothing waiting on you.</p>`;
1659
+ return `${heading}<p style="color:${GREY4};font-family:helvetica,sans-serif;font-size:16px;margin:0">Nothing waiting on you.</p>`;
1604
1660
  }
1605
1661
  const rows = items.map((it) => {
1606
1662
  const safeUrl = it.dashboardUrl.startsWith("https://") ? it.dashboardUrl : void 0;
1607
1663
  const link = safeUrl ? `<a href="${escapeHtml(safeUrl)}" style="${ANCHOR_STYLE}">review &amp; approve</a>` : `review &amp; approve`;
1608
1664
  return `
1609
1665
  <tr>
1610
- <td style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px">
1666
+ <td style="color:${GREY4};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px">
1611
1667
  <strong style="color:#222">${escapeHtml(it.siteName)}</strong> \u2014 ${escapeHtml(it.reportType)} (${escapeHtml(it.period)})
1612
1668
  \u2014 ${link}
1613
1669
  </td>
@@ -1617,15 +1673,15 @@ function readySection(items) {
1617
1673
  }
1618
1674
  function attentionBadge(status) {
1619
1675
  if (status === "new")
1620
- return `<strong style="color:${RED3};font-family:helvetica,sans-serif">NEW</strong> `;
1676
+ return `<strong style="color:${RED4};font-family:helvetica,sans-serif">NEW</strong> `;
1621
1677
  if (status === "worse")
1622
- return `<strong style="color:${RED3};font-family:helvetica,sans-serif">WORSE</strong> `;
1678
+ return `<strong style="color:${RED4};font-family:helvetica,sans-serif">WORSE</strong> `;
1623
1679
  return "";
1624
1680
  }
1625
1681
  function attentionSection(items) {
1626
- const heading = `<h2 style="color:${RED3};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px">Needs attention</h2>`;
1682
+ const heading = `<h2 style="color:${RED4};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px">Needs attention</h2>`;
1627
1683
  if (items.length === 0) {
1628
- return `${heading}<p style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;margin:0">All clear \u2014 nothing needs attention.</p>`;
1684
+ return `${heading}<p style="color:${GREY4};font-family:helvetica,sans-serif;font-size:16px;margin:0">All clear \u2014 nothing needs attention.</p>`;
1629
1685
  }
1630
1686
  const bySite = /* @__PURE__ */ new Map();
1631
1687
  for (const it of items) {
@@ -1642,7 +1698,7 @@ function attentionSection(items) {
1642
1698
  const titleHtml = safeUrl ? `<a href="${escapeHtml(safeUrl)}" style="${ANCHOR_STYLE}">${escapeHtml(it.title)}</a>` : escapeHtml(it.title);
1643
1699
  return `
1644
1700
  <tr>
1645
- <td style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px">${attentionBadge(it.status)}${titleHtml}</td>
1701
+ <td style="color:${GREY4};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px">${attentionBadge(it.status)}${titleHtml}</td>
1646
1702
  </tr>`;
1647
1703
  }).join("");
1648
1704
  return `
@@ -1767,7 +1823,7 @@ function renderDigestHtml(sections) {
1767
1823
  <table width="600" style="border-collapse:collapse">
1768
1824
  <tr>
1769
1825
  <td>
1770
- <h1 style="color:${RED3};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px">Your fleet today</h1>
1826
+ <h1 style="color:${RED4};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px">Your fleet today</h1>
1771
1827
  ${readySection(sections.readyForYourYes)}
1772
1828
  ${attentionSection(sections.needsAttention)}
1773
1829
  </td>
@@ -1779,7 +1835,7 @@ function renderDigestHtml(sections) {
1779
1835
  </body>
1780
1836
  </html>`;
1781
1837
  }
1782
- var GREY3, RED3, ANCHOR_STYLE, SEVERITY_ORDER, FROM_ADDRESS, DIGEST_OPERATOR_FALLBACK;
1838
+ var GREY4, RED4, ANCHOR_STYLE, SEVERITY_ORDER, FROM_ADDRESS, DIGEST_OPERATOR_FALLBACK;
1783
1839
  var init_digest = __esm({
1784
1840
  "src/reports/digest.ts"() {
1785
1841
  "use strict";
@@ -1791,9 +1847,9 @@ var init_digest = __esm({
1791
1847
  init_digest_collectors();
1792
1848
  init_digest_state();
1793
1849
  init_html();
1794
- GREY3 = "#757575";
1795
- RED3 = "#C00";
1796
- ANCHOR_STYLE = `color:${RED3};font-family:helvetica,sans-serif`;
1850
+ GREY4 = "#757575";
1851
+ RED4 = "#C00";
1852
+ ANCHOR_STYLE = `color:${RED4};font-family:helvetica,sans-serif`;
1797
1853
  SEVERITY_ORDER = { critical: 0, warning: 1 };
1798
1854
  FROM_ADDRESS = "Reddoor Reports <reports@reddoorla.com>";
1799
1855
  DIGEST_OPERATOR_FALLBACK = "info@reddoorla.com";
@@ -1951,7 +2007,11 @@ async function sendOne(client, base, site, report) {
1951
2007
  headerImageCid: cidName,
1952
2008
  headerWidth: header.displayWidth,
1953
2009
  headerHeight: header.displayHeight,
1954
- headerBgColor: header.placeholderColor
2010
+ headerBgColor: header.placeholderColor,
2011
+ // Announcement-only: re-derive cadence + improvements from the site row so the SENT email
2012
+ // keeps its cadence copy + improvement callouts. Without this the send-time re-render drops
2013
+ // them entirely (they're not stored on the Reports row). Ignored by the other templates.
2014
+ ...report.reportType === "Announcement" ? announcementSiteExtras(site) : {}
1955
2015
  });
1956
2016
  const reportDate = report.completedOn ? new Date(report.completedOn) : /* @__PURE__ */ new Date();
1957
2017
  const subject = report.subjectOverride ?? `${site.name} \u2014 ${monthYear(reportDate)} ${report.reportType} Report`;
@@ -2037,6 +2097,7 @@ var init_orchestrate = __esm({
2037
2097
  init_attachments();
2038
2098
  init_render();
2039
2099
  init_copy();
2100
+ init_template3();
2040
2101
  init_assets();
2041
2102
  init_header_image();
2042
2103
  init_resend();
@@ -5719,10 +5780,40 @@ init_render();
5719
5780
  init_websites();
5720
5781
  init_copy();
5721
5782
  init_reports();
5722
- init_attachments();
5723
5783
  import { mkdir as mkdir4, writeFile as writeFile10 } from "fs/promises";
5724
5784
  import { dirname as dirname6 } from "path";
5725
5785
 
5786
+ // src/reports/queue.ts
5787
+ init_reports();
5788
+ var REPORT_TIER = {
5789
+ Maintenance: 1,
5790
+ Testing: 2,
5791
+ Announcement: 3,
5792
+ Launch: 3
5793
+ };
5794
+ function reportTier(type) {
5795
+ return REPORT_TIER[type];
5796
+ }
5797
+ async function queueDraft(base, report) {
5798
+ const newTier = reportTier(report.reportType);
5799
+ const others = (await listReportsForSite(base, report.siteId)).filter(isPendingApproval).filter((r) => r.id !== report.id);
5800
+ const blocker = others.find((r) => reportTier(r.reportType) >= newTier);
5801
+ if (blocker) {
5802
+ await setDraftReady(base, report.id, false);
5803
+ return { queued: false, blockedBy: blocker.reportType, supersededIds: [] };
5804
+ }
5805
+ const supersededIds = [];
5806
+ for (const r of others) {
5807
+ await setDraftReady(base, r.id, false);
5808
+ supersededIds.push(r.id);
5809
+ }
5810
+ await setDraftReady(base, report.id, true);
5811
+ return { queued: true, supersededIds };
5812
+ }
5813
+
5814
+ // src/reports/draft.ts
5815
+ init_attachments();
5816
+
5726
5817
  // src/reports/ga/config.ts
5727
5818
  init_credentials();
5728
5819
  import { dirname as dirname5, join as join26 } from "path";
@@ -5776,6 +5867,7 @@ import { JWT as JWT2 } from "google-auth-library";
5776
5867
  var WEBMASTERS_READONLY = "https://www.googleapis.com/auth/webmasters.readonly";
5777
5868
  var SC_BASE = "https://searchconsole.googleapis.com/webmasters/v3";
5778
5869
  var PAGE_1_MAX_POSITION = 10;
5870
+ var BRAND_QUERY_ROW_LIMIT = 100;
5779
5871
  function bareHost(s) {
5780
5872
  return s.trim().replace(/^sc-domain:/i, "").replace(/^https?:\/\//i, "").split("/")[0].replace(/^www\./i, "").toLowerCase();
5781
5873
  }
@@ -5789,6 +5881,25 @@ function resolvePropertyCandidates(entries, host) {
5789
5881
  function ymd3(d) {
5790
5882
  return d.toISOString().slice(0, 10);
5791
5883
  }
5884
+ function pickBrandQuery(rows) {
5885
+ let best;
5886
+ for (const r of rows) {
5887
+ if (typeof r.position !== "number") continue;
5888
+ const impressions = r.impressions ?? 0;
5889
+ if (best === void 0 || impressions > best.impressions || impressions === best.impressions && r.position < best.position) {
5890
+ best = { position: r.position, impressions };
5891
+ }
5892
+ }
5893
+ return best?.position;
5894
+ }
5895
+ function selectBrandPosition(rows, exactQuery) {
5896
+ const want = exactQuery.toLowerCase();
5897
+ const exact = rows.find(
5898
+ (r) => typeof r.position === "number" && (r.keys?.[0] ?? "").toLowerCase() === want
5899
+ );
5900
+ if (exact) return exact.position;
5901
+ return pickBrandQuery(rows);
5902
+ }
5792
5903
  async function fetchSearchPresence(q, periodStart, periodEnd) {
5793
5904
  const key = JSON.parse(readFileSync4(q.keyPath, "utf8"));
5794
5905
  const jwt = new JWT2({
@@ -5820,14 +5931,14 @@ async function fetchSearchPresence(q, periodStart, periodEnd) {
5820
5931
  dimensionFilterGroups: [
5821
5932
  {
5822
5933
  filters: [
5823
- { dimension: "query", operator: "equals", expression: q.query.toLowerCase() }
5934
+ { dimension: "query", operator: "contains", expression: q.query.toLowerCase() }
5824
5935
  ]
5825
5936
  }
5826
5937
  ],
5827
- rowLimit: 1
5938
+ rowLimit: BRAND_QUERY_ROW_LIMIT
5828
5939
  }
5829
5940
  });
5830
- const pos = res.data.rows?.[0]?.position;
5941
+ const pos = selectBrandPosition(res.data.rows ?? [], q.query.toLowerCase());
5831
5942
  if (typeof pos === "number") {
5832
5943
  return { foundOnPage1: pos <= PAGE_1_MAX_POSITION, position: Math.max(1, Math.round(pos)) };
5833
5944
  }
@@ -5885,12 +5996,24 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
5885
5996
  const path = options.previewPath ?? `reports/${slug}/draft.html`;
5886
5997
  await mkdir4(dirname6(path), { recursive: true });
5887
5998
  await writeFile10(path, html, "utf-8");
5888
- return { reportRow: null, htmlPath: path, html, softFailures };
5999
+ return { reportRow: null, htmlPath: path, html, softFailures, queued: null, supersededIds: [] };
5889
6000
  }
5890
6001
  if (base === null) throw new Error("base required when previewOnly=false");
5891
6002
  if (options.completeRowId) {
5892
- await finishDraftRow(base, options.completeRowId, slug, periodEnd, html);
5893
- return { reportRow: options.existingRow ?? null, htmlPath: null, html, softFailures };
6003
+ await uploadDraftHtml(options.completeRowId, slug, periodEnd, html);
6004
+ const outcome2 = await queueDraft(base, {
6005
+ id: options.completeRowId,
6006
+ siteId: siteRow.id,
6007
+ reportType
6008
+ });
6009
+ return {
6010
+ reportRow: options.existingRow ?? null,
6011
+ htmlPath: null,
6012
+ html,
6013
+ softFailures,
6014
+ queued: outcome2.queued,
6015
+ supersededIds: outcome2.supersededIds
6016
+ };
5894
6017
  }
5895
6018
  const reportId = `${siteRow.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
5896
6019
  const created = await createDraft(base, {
@@ -5907,13 +6030,24 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
5907
6030
  ...search ? { searchFoundPage1: search.foundOnPage1 } : {},
5908
6031
  ...search?.foundOnPage1 && search.position !== null ? { searchPosition: search.position } : {}
5909
6032
  });
5910
- await finishDraftRow(base, created.id, slug, periodEnd, html);
5911
- return { reportRow: created, htmlPath: null, html, softFailures };
6033
+ await uploadDraftHtml(created.id, slug, periodEnd, html);
6034
+ const outcome = await queueDraft(base, {
6035
+ id: created.id,
6036
+ siteId: siteRow.id,
6037
+ reportType
6038
+ });
6039
+ return {
6040
+ reportRow: created,
6041
+ htmlPath: null,
6042
+ html,
6043
+ softFailures,
6044
+ queued: outcome.queued,
6045
+ supersededIds: outcome.supersededIds
6046
+ };
5912
6047
  }
5913
- async function finishDraftRow(base, rowId, slug, periodEnd, html) {
6048
+ async function uploadDraftHtml(rowId, slug, periodEnd, html) {
5914
6049
  const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
5915
6050
  await uploadAttachment(rowId, "Rendered HTML", html, htmlFilename, "text/html");
5916
- await setDraftReady(base, rowId, true);
5917
6051
  }
5918
6052
  var NO_ENRICHMENT = { value: null, softFailed: false };
5919
6053
  async function fetchGaUsers(siteRow, periodStart, periodEnd) {
@@ -5963,6 +6097,14 @@ async function derivePeriodStart(base, siteRow, reportType, today) {
5963
6097
  }
5964
6098
 
5965
6099
  // src/cli/commands/report.ts
6100
+ function draftLine(reportId, queued, supersededIds, verb = "drafted") {
6101
+ const id = reportId ?? "(unknown)";
6102
+ if (queued === false) {
6103
+ return `\u2022 ${verb} but NOT queued: ${id} \u2014 a higher-or-equal-tier report is already pending approval`;
6104
+ }
6105
+ const sup = supersededIds.length > 0 ? ` (superseded ${supersededIds.length} lower-tier draft${supersededIds.length > 1 ? "s" : ""})` : "";
6106
+ return `\u2713 ${verb}: ${id}${sup}`;
6107
+ }
5966
6108
  function parseSingleSiteReportType(raw) {
5967
6109
  if (raw === void 0 || raw.trim() === "") return "Maintenance";
5968
6110
  const norm = raw.trim().toLowerCase();
@@ -6025,15 +6167,30 @@ async function draftDueReports(base, today) {
6025
6167
  lines.push(`\u2022 skipped (already drafted ${period}): ${item.site.name} ${item.reportType}`);
6026
6168
  continue;
6027
6169
  }
6170
+ const blockedByPending = reports.some(
6171
+ (r) => r.siteId === item.site.id && r.id !== existing.id && r.sentAt === null && r.draftReady && reportTier(r.reportType) >= reportTier(item.reportType)
6172
+ );
6173
+ if (blockedByPending) {
6174
+ skipped++;
6175
+ lines.push(
6176
+ `\u2022 skipped (superseded \u2014 a higher-or-equal-tier report is pending): ${item.site.name} ${item.reportType}`
6177
+ );
6178
+ continue;
6179
+ }
6028
6180
  try {
6029
6181
  const result = await draftReportForSite(base, item.site, item.reportType, {
6030
6182
  period,
6031
6183
  completeRowId: existing.id,
6032
6184
  existingRow: existing
6033
6185
  });
6034
- existing.draftReady = true;
6186
+ existing.draftReady = result.queued === true;
6035
6187
  lines.push(
6036
- `\u2713 completed half-made draft: ${result.reportRow?.reportId ?? existing.reportId}`
6188
+ draftLine(
6189
+ result.reportRow?.reportId ?? existing.reportId,
6190
+ result.queued,
6191
+ result.supersededIds,
6192
+ "completed half-made draft"
6193
+ )
6037
6194
  );
6038
6195
  if (result.softFailures.length > 0) softFailedSites++;
6039
6196
  } catch (e) {
@@ -6053,7 +6210,7 @@ async function draftDueReports(base, today) {
6053
6210
  }
6054
6211
  try {
6055
6212
  const result = await draftReportForSite(base, item.site, item.reportType, { period });
6056
- lines.push(`\u2713 drafted: ${result.reportRow?.reportId}`);
6213
+ lines.push(draftLine(result.reportRow?.reportId, result.queued, result.supersededIds));
6057
6214
  if (result.reportRow) reports.push(result.reportRow);
6058
6215
  if (result.softFailures.length > 0) softFailedSites++;
6059
6216
  } catch (e) {
@@ -6083,7 +6240,15 @@ async function runSingleSiteDraft(slug, opts) {
6083
6240
  if (opts.previewOnly) {
6084
6241
  return { output: `Preview written to ${result.htmlPath}`, code: 0 };
6085
6242
  }
6086
- return { output: `Draft created: ${result.reportRow?.reportId}`, code: 0 };
6243
+ return {
6244
+ output: draftLine(
6245
+ result.reportRow?.reportId,
6246
+ result.queued,
6247
+ result.supersededIds,
6248
+ "Draft created"
6249
+ ),
6250
+ code: 0
6251
+ };
6087
6252
  }
6088
6253
 
6089
6254
  // src/cli/commands/init.ts
@@ -6352,7 +6517,7 @@ async function launch(site, deps = {}) {
6352
6517
  `\u26A0 Launch preview upload skipped for ${target.name}: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
6353
6518
  );
6354
6519
  }
6355
- await setDraftReady(base, report.id, true);
6520
+ await queueDraft(base, { id: report.id, siteId: target.id, reportType: "Launch" });
6356
6521
  } catch (err) {
6357
6522
  steps.push({ name: "draft", result: errorOf(err) });
6358
6523
  return stop();
@@ -6419,6 +6584,7 @@ init_reports();
6419
6584
  init_attachments();
6420
6585
  init_render();
6421
6586
  init_copy();
6587
+ init_template3();
6422
6588
  async function announce(deps) {
6423
6589
  const base = deps?.base ?? openBase(readAirtableConfig());
6424
6590
  const now = deps?.now ?? /* @__PURE__ */ new Date();
@@ -6437,15 +6603,24 @@ async function announce(deps) {
6437
6603
  results.push({ site: w.name, status: "skipped-no-scores" });
6438
6604
  continue;
6439
6605
  }
6606
+ const periodEnd = now;
6607
+ const periodStart = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
6608
+ const gaUsers = (await fetchGaUsers(w, periodStart, periodEnd)).value;
6609
+ const search = (await fetchSearch(w, periodStart, periodEnd)).value;
6610
+ const enrichment = {
6611
+ ...gaUsers ? { gaUsersCurrent: gaUsers.current, gaUsersPrevious: gaUsers.previous } : {},
6612
+ ...search ? { searchFoundPage1: search.foundOnPage1 } : {},
6613
+ ...search?.foundOnPage1 && search.position !== null ? { searchPosition: search.position } : {}
6614
+ };
6440
6615
  let report;
6441
6616
  let statusKind;
6442
6617
  const existing = await findReportByPeriod(base, w.id, "Announcement", period);
6443
6618
  if (existing) {
6444
- await updateReportScores(base, existing.id, scores, now);
6619
+ await updateReportScores(base, existing.id, scores, now, enrichment);
6445
6620
  report = existing;
6446
6621
  statusKind = "reused";
6447
6622
  } else {
6448
- report = await createDraft(base, draftInputFor2(w, scores, now, period));
6623
+ report = await createDraft(base, draftInputFor2(w, scores, now, period, enrichment));
6449
6624
  statusKind = "drafted";
6450
6625
  }
6451
6626
  const slug = siteSlug(w.name);
@@ -6455,18 +6630,17 @@ async function announce(deps) {
6455
6630
  reportType: "Announcement",
6456
6631
  completedOn: now,
6457
6632
  lighthouse: scores,
6633
+ gaUsersCurrent: gaUsers?.current,
6634
+ gaUsersPrevious: gaUsers?.previous,
6635
+ searchPosition: search?.foundOnPage1 ? search.position ?? void 0 : void 0,
6458
6636
  lastTestedDate: null,
6459
6637
  commentary: null,
6460
6638
  copy: resolveCopy(w),
6461
6639
  headerImageCid: `${slug}-header`,
6462
- // The client's go-forward pace, read straight off the Websites row — the email
6463
- // states each cadence ("Full site testing every quarter", etc.); a "None" pace
6464
- // is omitted so we never claim a cadence the site isn't on.
6465
- cadence: { maintenance: w.maintenanceFreq, testing: w.testingFreq },
6466
- // Default-on fleet-wide for v1: both recent-improvement callouts render for every
6467
- // site. Operator review (the draft never auto-sends) is the relevance backstop;
6468
- // per-site conditioning of these flags is a future lever, not a v1 requirement.
6469
- improvements: { resendForms: true, svelte5: true }
6640
+ // cadence (the client's go-forward pace, "None" omitted) + default-on improvement
6641
+ // callouts. Shared with the send re-render via announcementSiteExtras so the sent
6642
+ // email matches this reviewed preview.
6643
+ ...announcementSiteExtras(w)
6470
6644
  });
6471
6645
  try {
6472
6646
  await uploadAttachment(
@@ -6481,9 +6655,19 @@ async function announce(deps) {
6481
6655
  `\u26A0 Announcement preview upload skipped for ${w.name}: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
6482
6656
  );
6483
6657
  }
6484
- await setDraftReady(base, report.id, true);
6658
+ const queue = await queueDraft(base, {
6659
+ id: report.id,
6660
+ siteId: w.id,
6661
+ reportType: "Announcement"
6662
+ });
6485
6663
  const recipientMissing = !(w.reportRecipientsTo && w.reportRecipientsTo.trim());
6486
- results.push({ site: w.name, status: statusKind, reportId: report.id, recipientMissing });
6664
+ results.push({
6665
+ site: w.name,
6666
+ status: statusKind,
6667
+ reportId: report.id,
6668
+ recipientMissing,
6669
+ queued: queue.queued
6670
+ });
6487
6671
  } catch (err) {
6488
6672
  results.push({
6489
6673
  site: w.name,
@@ -6505,7 +6689,7 @@ function scoresFromRow(w) {
6505
6689
  seo: w.seoScore
6506
6690
  };
6507
6691
  }
6508
- function draftInputFor2(w, scores, now, period) {
6692
+ function draftInputFor2(w, scores, now, period, enrichment) {
6509
6693
  return {
6510
6694
  reportId: `${w.name} \u2014 Announcement \u2014 ${now.toISOString().slice(0, 10)}`,
6511
6695
  siteId: w.id,
@@ -6516,7 +6700,8 @@ function draftInputFor2(w, scores, now, period) {
6516
6700
  completedOn: now,
6517
6701
  lighthouse: scores,
6518
6702
  lastTestedDate: null,
6519
- subjectOverride: `Your testing & maintenance schedule for ${w.name}`
6703
+ subjectOverride: `Your testing & maintenance schedule for ${w.name}`,
6704
+ ...enrichment
6520
6705
  };
6521
6706
  }
6522
6707