@reddoorla/maintenance 0.54.4 → 0.56.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/README.md +3 -1
- package/dist/airtable-2BJX4ATJ.js +9 -0
- package/dist/airtable-2BJX4ATJ.js.map +1 -0
- package/dist/announce-JEVCDDKL.js +171 -0
- package/dist/announce-JEVCDDKL.js.map +1 -0
- package/dist/bump-deps-O3SOLQYY.js +57 -0
- package/dist/bump-deps-O3SOLQYY.js.map +1 -0
- package/dist/chunk-2X322ZES.js +99 -0
- package/dist/chunk-2X322ZES.js.map +1 -0
- package/dist/chunk-4NAOR44V.js +153 -0
- package/dist/chunk-4NAOR44V.js.map +1 -0
- package/dist/chunk-4PY4I5OR.js +1167 -0
- package/dist/chunk-4PY4I5OR.js.map +1 -0
- package/dist/chunk-5WL3ET24.js +203 -0
- package/dist/chunk-5WL3ET24.js.map +1 -0
- package/dist/chunk-6LIWDIXU.js +38 -0
- package/dist/chunk-6LIWDIXU.js.map +1 -0
- package/dist/chunk-6VAL7XJT.js +52 -0
- package/dist/chunk-6VAL7XJT.js.map +1 -0
- package/dist/chunk-BWLUNY7L.js +585 -0
- package/dist/chunk-BWLUNY7L.js.map +1 -0
- package/dist/chunk-CASCWQQX.js +161 -0
- package/dist/chunk-CASCWQQX.js.map +1 -0
- package/dist/chunk-CLVTVDOU.js +274 -0
- package/dist/chunk-CLVTVDOU.js.map +1 -0
- package/dist/chunk-D3DQFC32.js +79 -0
- package/dist/chunk-D3DQFC32.js.map +1 -0
- package/dist/chunk-DIVB6ZF7.js +217 -0
- package/dist/chunk-DIVB6ZF7.js.map +1 -0
- package/dist/chunk-EYODPUHV.js +37 -0
- package/dist/chunk-EYODPUHV.js.map +1 -0
- package/dist/chunk-FDIFTIXC.js +70 -0
- package/dist/chunk-FDIFTIXC.js.map +1 -0
- package/dist/chunk-FUD4I2ZW.js +35 -0
- package/dist/chunk-FUD4I2ZW.js.map +1 -0
- package/dist/chunk-HG5DTFNF.js +62 -0
- package/dist/chunk-HG5DTFNF.js.map +1 -0
- package/dist/chunk-HN6I2XWI.js +65 -0
- package/dist/chunk-HN6I2XWI.js.map +1 -0
- package/dist/chunk-HVOOCK6L.js +84 -0
- package/dist/chunk-HVOOCK6L.js.map +1 -0
- package/dist/chunk-IMIR7SC2.js +51 -0
- package/dist/chunk-IMIR7SC2.js.map +1 -0
- package/dist/chunk-J3OPWFZE.js +64 -0
- package/dist/chunk-J3OPWFZE.js.map +1 -0
- package/dist/chunk-JNXCDUYB.js +211 -0
- package/dist/chunk-JNXCDUYB.js.map +1 -0
- package/dist/chunk-JQG222CI.js +49 -0
- package/dist/chunk-JQG222CI.js.map +1 -0
- package/dist/chunk-LKGVSM2O.js +43 -0
- package/dist/chunk-LKGVSM2O.js.map +1 -0
- package/dist/chunk-LYCKN7ZR.js +41 -0
- package/dist/chunk-LYCKN7ZR.js.map +1 -0
- package/dist/chunk-O65I7G7G.js +246 -0
- package/dist/chunk-O65I7G7G.js.map +1 -0
- package/dist/chunk-OCS7SZUZ.js +243 -0
- package/dist/chunk-OCS7SZUZ.js.map +1 -0
- package/dist/chunk-OGGRFBL4.js +14 -0
- package/dist/chunk-OGGRFBL4.js.map +1 -0
- package/dist/chunk-OQPMZ6BM.js +368 -0
- package/dist/chunk-OQPMZ6BM.js.map +1 -0
- package/dist/chunk-OTGB6YQO.js +52 -0
- package/dist/chunk-OTGB6YQO.js.map +1 -0
- package/dist/chunk-PF6BNYGE.js +162 -0
- package/dist/chunk-PF6BNYGE.js.map +1 -0
- package/dist/chunk-QTHKOIFJ.js +13 -0
- package/dist/chunk-QTHKOIFJ.js.map +1 -0
- package/dist/chunk-U5SZ4FOP.js +26 -0
- package/dist/chunk-U5SZ4FOP.js.map +1 -0
- package/dist/chunk-VXSX2FHO.js +269 -0
- package/dist/chunk-VXSX2FHO.js.map +1 -0
- package/dist/chunk-W3G7XFBZ.js +444 -0
- package/dist/chunk-W3G7XFBZ.js.map +1 -0
- package/dist/chunk-WEKL2HYJ.js +31 -0
- package/dist/chunk-WEKL2HYJ.js.map +1 -0
- package/dist/chunk-WVRPOXKH.js +74 -0
- package/dist/chunk-WVRPOXKH.js.map +1 -0
- package/dist/chunk-XKOZUVK2.js +121 -0
- package/dist/chunk-XKOZUVK2.js.map +1 -0
- package/dist/chunk-XO2RUYVM.js +138 -0
- package/dist/chunk-XO2RUYVM.js.map +1 -0
- package/dist/chunk-XTK5VIQB.js +97 -0
- package/dist/chunk-XTK5VIQB.js.map +1 -0
- package/dist/chunk-XXTZBPUY.js +9 -0
- package/dist/chunk-XXTZBPUY.js.map +1 -0
- package/dist/chunk-ZYBOT5PM.js +153 -0
- package/dist/chunk-ZYBOT5PM.js.map +1 -0
- package/dist/cli/bin.js +75 -7689
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +37 -2330
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-EPVO3FEH.js +10 -0
- package/dist/client-EPVO3FEH.js.map +1 -0
- package/dist/client-JA6QLEDW.js +48 -0
- package/dist/client-JA6QLEDW.js.map +1 -0
- package/dist/configs/lighthouse.js +4 -31
- package/dist/configs/lighthouse.js.map +1 -1
- package/dist/configs/playwright-a11y.js +6 -33
- package/dist/configs/playwright-a11y.js.map +1 -1
- package/dist/convert-to-pnpm-UR64X3OX.js +51 -0
- package/dist/convert-to-pnpm-UR64X3OX.js.map +1 -0
- package/dist/db-AXKMRTVA.js +23 -0
- package/dist/db-AXKMRTVA.js.map +1 -0
- package/dist/digest-YTO73BEO.js +408 -0
- package/dist/digest-YTO73BEO.js.map +1 -0
- package/dist/forms/index.js +3 -8
- package/dist/forms/index.js.map +1 -1
- package/dist/github-signals-YD64R2AF.js +122 -0
- package/dist/github-signals-YD64R2AF.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +102 -4741
- package/dist/index.js.map +1 -1
- package/dist/init-JITQXFVL.js +85 -0
- package/dist/init-JITQXFVL.js.map +1 -0
- package/dist/launch-HXHEL6ID.js +207 -0
- package/dist/launch-HXHEL6ID.js.map +1 -0
- package/dist/lighthouse-airtable-4FKYKFH6.js +12 -0
- package/dist/lighthouse-airtable-4FKYKFH6.js.map +1 -0
- package/dist/migrate-P2LS4WXI.js +7 -0
- package/dist/migrate-P2LS4WXI.js.map +1 -0
- package/dist/onboard-TCQOP2W4.js +69 -0
- package/dist/onboard-TCQOP2W4.js.map +1 -0
- package/dist/orchestrate-RMSBHLQK.js +19 -0
- package/dist/orchestrate-RMSBHLQK.js.map +1 -0
- package/dist/recipes/sync-configs.js +9 -505
- package/dist/recipes/sync-configs.js.map +1 -1
- package/dist/renovate-dispatch-ZGLXNPEK.js +97 -0
- package/dist/renovate-dispatch-ZGLXNPEK.js.map +1 -0
- package/dist/report-SD3CNBOQ.js +240 -0
- package/dist/report-SD3CNBOQ.js.map +1 -0
- package/dist/resend-G2BJMAQV.js +7 -0
- package/dist/resend-G2BJMAQV.js.map +1 -0
- package/dist/self-updating-CIUMOMYI.js +61 -0
- package/dist/self-updating-CIUMOMYI.js.map +1 -0
- package/dist/svelte-codemods-BYRJAU7I.js +51 -0
- package/dist/svelte-codemods-BYRJAU7I.js.map +1 -0
- package/dist/sync-configs-JPRLAW5J.js +109 -0
- package/dist/sync-configs-JPRLAW5J.js.map +1 -0
- package/dist/upgrade-ERA45EVR.js +64 -0
- package/dist/upgrade-ERA45EVR.js.map +1 -0
- package/dist/util/git.js +19 -76
- package/dist/util/git.js.map +1 -1
- package/dist/util/pkg.js +5 -45
- package/dist/util/pkg.js.map +1 -1
- package/dist/websites-X5DLC2PC.js +43 -0
- package/dist/websites-X5DLC2PC.js.map +1 -0
- package/dist/write-audits-to-airtable-CE4POBAF.js +13 -0
- package/dist/write-audits-to-airtable-CE4POBAF.js.map +1 -0
- package/package.json +13 -16
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHtml
|
|
3
|
+
} from "./chunk-OCS7SZUZ.js";
|
|
4
|
+
import {
|
|
5
|
+
isHttpUrl
|
|
6
|
+
} from "./chunk-U5SZ4FOP.js";
|
|
7
|
+
|
|
8
|
+
// src/reports/copy.ts
|
|
9
|
+
var DEFAULT_COPY = {
|
|
10
|
+
maintenanceIntro: "Includes checking the hosting, DNS, Content Management System (CMS, if applicable), search indexing and security of the site for major flaws and updating as necessary.",
|
|
11
|
+
maintenanceChecks: [
|
|
12
|
+
"Deploy & Function Health",
|
|
13
|
+
"CMS Checked",
|
|
14
|
+
"Domain, DNS & SSL",
|
|
15
|
+
"Google Indexed",
|
|
16
|
+
"Security Updates",
|
|
17
|
+
"Uptime Checked"
|
|
18
|
+
],
|
|
19
|
+
testingIntro: "Testing includes checks similar to those at launch: testing on common browsers and operating systems, at different screen sizes, and checking every function, and updating all packages for performance rather than just those needed for security.",
|
|
20
|
+
testingChecklist: [
|
|
21
|
+
"Desktop Browsers",
|
|
22
|
+
"Mobile Browsers",
|
|
23
|
+
"Page Titles & Meta",
|
|
24
|
+
"Links & Navigation",
|
|
25
|
+
"Form Functionality",
|
|
26
|
+
"Interactions & Animations",
|
|
27
|
+
"Tested After Updates"
|
|
28
|
+
],
|
|
29
|
+
notesHeader: "NOTES",
|
|
30
|
+
seoCta: "Contact us if you are interested in more in-depth data or have questions about SEO.",
|
|
31
|
+
contact: ["Just hit reply.", "We're here to help in any way we can."],
|
|
32
|
+
footerOrg: "Reddoor Creative, LLC",
|
|
33
|
+
footerAddress: ["29027 Dapper Dan", "Fair Oaks Ranch, TX 78015"],
|
|
34
|
+
launchHeading: "LAUNCHED",
|
|
35
|
+
launchBody: "Your site is live. We've set it up on the Reddoor stack with hosting, security, and automatic maintenance so it stays fast and healthy. Here's what's in place:",
|
|
36
|
+
launchSetupItems: [
|
|
37
|
+
"Hosting, DNS, and SSL configured",
|
|
38
|
+
"Continuous integration + automatic dependency updates",
|
|
39
|
+
"Analytics and uptime monitoring"
|
|
40
|
+
],
|
|
41
|
+
announceHeading: "YOUR ONGOING SITE CARE",
|
|
42
|
+
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:",
|
|
43
|
+
announceImprovementResend: "Your contact forms now deliver straight to your inbox through reliable infrastructure, so no inquiry slips through the cracks.",
|
|
44
|
+
announceImprovementSvelte5: "We've modernized your site to the latest framework \u2014 it's faster, more secure, and built to last.",
|
|
45
|
+
announceCadence: "After each one we'll send you a short report like this \u2014 there's nothing you need to do.",
|
|
46
|
+
announceOpenDoor: "And if you'd ever like to expand the scope, add features, or freshen anything up, just let us know."
|
|
47
|
+
};
|
|
48
|
+
function override(v) {
|
|
49
|
+
if (typeof v !== "string") return null;
|
|
50
|
+
const t = v.trim();
|
|
51
|
+
return t.length > 0 ? t : null;
|
|
52
|
+
}
|
|
53
|
+
function splitLines(s) {
|
|
54
|
+
return s.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
55
|
+
}
|
|
56
|
+
function resolveCopy(site) {
|
|
57
|
+
const intro = override(site.copyIntro);
|
|
58
|
+
const contact = override(site.copyContact);
|
|
59
|
+
const footer = override(site.copyFooter);
|
|
60
|
+
const footerLines = footer ? splitLines(footer) : null;
|
|
61
|
+
return {
|
|
62
|
+
...DEFAULT_COPY,
|
|
63
|
+
maintenanceIntro: intro ?? DEFAULT_COPY.maintenanceIntro,
|
|
64
|
+
contact: contact ? splitLines(contact) : DEFAULT_COPY.contact,
|
|
65
|
+
footerOrg: footerLines?.[0] ?? DEFAULT_COPY.footerOrg,
|
|
66
|
+
footerAddress: footerLines ? footerLines.slice(1) : DEFAULT_COPY.footerAddress
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/reports/maintenance-email/assets/index.ts
|
|
71
|
+
import { readFile } from "fs/promises";
|
|
72
|
+
import { existsSync } from "fs";
|
|
73
|
+
import { dirname, join } from "path";
|
|
74
|
+
import { fileURLToPath } from "url";
|
|
75
|
+
var CHECK_CID = "rd-check-png";
|
|
76
|
+
var BLURRED_CID = "rd-blurred-tests-jpg";
|
|
77
|
+
var cachedAssetsDir = null;
|
|
78
|
+
function resolveAssetsDir() {
|
|
79
|
+
if (cachedAssetsDir) return cachedAssetsDir;
|
|
80
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
81
|
+
while (true) {
|
|
82
|
+
const srcCandidate = join(dir, "src", "reports", "maintenance-email", "assets", "check.png");
|
|
83
|
+
if (existsSync(srcCandidate)) {
|
|
84
|
+
cachedAssetsDir = dirname(srcCandidate);
|
|
85
|
+
return cachedAssetsDir;
|
|
86
|
+
}
|
|
87
|
+
const distCandidate = join(dir, "dist", "reports", "maintenance-email", "assets", "check.png");
|
|
88
|
+
if (existsSync(distCandidate)) {
|
|
89
|
+
cachedAssetsDir = dirname(distCandidate);
|
|
90
|
+
return cachedAssetsDir;
|
|
91
|
+
}
|
|
92
|
+
const parent = dirname(dir);
|
|
93
|
+
if (parent === dir) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`loadBundledImages: could not locate maintenance-email assets dir by walking up from ${fileURLToPath(import.meta.url)}. Checked both src/ and dist/ layouts.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
dir = parent;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function loadBundledImages() {
|
|
102
|
+
const assetsDir = resolveAssetsDir();
|
|
103
|
+
const [check, blurred] = await Promise.all([
|
|
104
|
+
readFile(join(assetsDir, "check.png")),
|
|
105
|
+
readFile(join(assetsDir, "blurredTests.jpg"))
|
|
106
|
+
]);
|
|
107
|
+
return {
|
|
108
|
+
check: {
|
|
109
|
+
bytes: new Uint8Array(check),
|
|
110
|
+
contentType: "image/png",
|
|
111
|
+
cid: CHECK_CID,
|
|
112
|
+
filename: "check.png"
|
|
113
|
+
},
|
|
114
|
+
blurred: {
|
|
115
|
+
bytes: new Uint8Array(blurred),
|
|
116
|
+
contentType: "image/jpeg",
|
|
117
|
+
cid: BLURRED_CID,
|
|
118
|
+
filename: "blurredTests.jpg"
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/reports/email-sections.ts
|
|
124
|
+
var escapeXml = escapeHtml;
|
|
125
|
+
var RED = "#C00";
|
|
126
|
+
var GREY = "#757575";
|
|
127
|
+
var BORDER = "#CCCCCC";
|
|
128
|
+
var TREND_UP = "#2E7D32";
|
|
129
|
+
var TREND_NEUTRAL = GREY;
|
|
130
|
+
var CHECK_PNG = `cid:${CHECK_CID}`;
|
|
131
|
+
function fmtUsers(n) {
|
|
132
|
+
return n.toLocaleString("en-US");
|
|
133
|
+
}
|
|
134
|
+
function checklistRowsSection(rows, opts) {
|
|
135
|
+
return rows.map((label, i) => {
|
|
136
|
+
const isLast = i === rows.length - 1;
|
|
137
|
+
const border = isLast ? "" : ` border-bottom="solid ${BORDER} 1px"`;
|
|
138
|
+
const lastPad = isLast ? ` padding-bottom="${opts.lastPaddingBottom}"` : "";
|
|
139
|
+
return `
|
|
140
|
+
<mj-section background-color="${opts.background}" padding="0px"${lastPad}>
|
|
141
|
+
<mj-group>
|
|
142
|
+
<mj-column padding-left="0px" width="90%"${border}>
|
|
143
|
+
<mj-text height="25px" padding-left="0px" color="${GREY}" padding-top="20px" padding-bottom="7.5px" font-size="16px">${escapeXml(label)}</mj-text>
|
|
144
|
+
</mj-column>
|
|
145
|
+
<mj-column width="10%"${border} padding-top="15px">
|
|
146
|
+
<mj-image align="right" padding-right="0px" width="20px" height="20px" padding-top="2.5px" padding-bottom="15px" src="${CHECK_PNG}" />
|
|
147
|
+
</mj-column>
|
|
148
|
+
</mj-group>
|
|
149
|
+
</mj-section>`;
|
|
150
|
+
}).join("");
|
|
151
|
+
}
|
|
152
|
+
var LIGHTHOUSE_ROWS = [
|
|
153
|
+
{ label: "Performance", key: "performance", range: "Acceptable 50\u201389 // Ideal 90\u2013100" },
|
|
154
|
+
{ label: "Readability (A11y)", key: "accessibility", range: "Acceptable 80\u201399 // Ideal 100" },
|
|
155
|
+
{ label: "Best Practices", key: "bestPractices", range: "Acceptable 60\u201379 // Ideal 80\u2013100" },
|
|
156
|
+
{ label: "Site Structure", key: "seo", range: "Acceptable 50\u201389 // Ideal 90\u2013100" }
|
|
157
|
+
];
|
|
158
|
+
function lighthouseScoresSection(lighthouse, opts = {}) {
|
|
159
|
+
const background = opts.background ?? "#F4F4F4";
|
|
160
|
+
const sectionPad = opts.pad ? ` padding-top="${opts.pad}" padding-bottom="${opts.pad}"` : "";
|
|
161
|
+
const labelTop = opts.pad ?? "55px";
|
|
162
|
+
const footnoteBottom = opts.pad ? "0px" : "36px";
|
|
163
|
+
const rows = LIGHTHOUSE_ROWS.map(
|
|
164
|
+
({ label, key, range }, i) => `
|
|
165
|
+
<mj-text color="${RED}" font-size="20px" font-weight="300" padding-top="25px">${label}</mj-text>
|
|
166
|
+
<mj-text color="${RED}" font-size="44px" font-weight="400" padding-top="0px">${lighthouse[key]}</mj-text>
|
|
167
|
+
<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 ? `
|
|
168
|
+
<mj-divider border-width="1px" border-style="solid" border-color="${BORDER}" padding="0" />` : ""}`
|
|
169
|
+
).join("");
|
|
170
|
+
return `
|
|
171
|
+
<mj-section background-color="${background}"${sectionPad}>
|
|
172
|
+
<mj-column>
|
|
173
|
+
<mj-text color="${RED}" font-size="20px" font-weight="700" padding-top="${labelTop}">LIGHTHOUSE SCORES*</mj-text>${rows}
|
|
174
|
+
<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>
|
|
175
|
+
</mj-column>
|
|
176
|
+
</mj-section>`;
|
|
177
|
+
}
|
|
178
|
+
function analyticsTrendLine(cur, prev) {
|
|
179
|
+
if (cur === void 0 || prev === void 0) {
|
|
180
|
+
return trendLine(TREND_NEUTRAL, `Last Period: ${prev !== void 0 ? fmtUsers(prev) : "\u2014"}`);
|
|
181
|
+
}
|
|
182
|
+
if (prev === 0) {
|
|
183
|
+
return cur > 0 ? trendLine(TREND_UP, "\u25B2 New this period (0 last period)") : trendLine(TREND_NEUTRAL, "Last Period: 0");
|
|
184
|
+
}
|
|
185
|
+
const pct = Math.round((cur - prev) / prev * 100);
|
|
186
|
+
const range = `(${fmtUsers(prev)} \u2192 ${fmtUsers(cur)})`;
|
|
187
|
+
if (pct > 0) return trendLine(TREND_UP, `\u25B2 ${pct}% vs last period ${range}`);
|
|
188
|
+
if (pct < 0) return trendLine(TREND_NEUTRAL, `\u25BC ${Math.abs(pct)}% vs last period ${range}`);
|
|
189
|
+
return trendLine(TREND_NEUTRAL, `No change vs last period (${fmtUsers(prev)})`);
|
|
190
|
+
}
|
|
191
|
+
function trendLine(color, text) {
|
|
192
|
+
return `<mj-text color="${color}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${text}</mj-text>`;
|
|
193
|
+
}
|
|
194
|
+
function footnoteLine(text) {
|
|
195
|
+
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>`;
|
|
196
|
+
}
|
|
197
|
+
function analyticsSection(opts) {
|
|
198
|
+
const users = opts.current !== void 0 ? fmtUsers(opts.current) : "\u2014";
|
|
199
|
+
const body = (opts.bodyLines ?? []).map((l) => trendLine(TREND_NEUTRAL, l)).join("\n ");
|
|
200
|
+
const footnotes = (opts.footnoteLines ?? []).map(footnoteLine).join("\n ");
|
|
201
|
+
const sectionPad = opts.pad ? ` padding-top="${opts.pad}" padding-bottom="${opts.pad}"` : "";
|
|
202
|
+
const labelTop = opts.pad ?? "75px";
|
|
203
|
+
return `
|
|
204
|
+
<mj-section background-color="${opts.background}"${sectionPad}>
|
|
205
|
+
<mj-column>
|
|
206
|
+
<mj-text color="${RED}" font-size="20px" font-weight="700" padding-top="${labelTop}">ANALYTICS</mj-text>
|
|
207
|
+
<mj-text color="${RED}" font-size="44px" font-weight="400">${users} Users</mj-text>
|
|
208
|
+
${analyticsTrendLine(opts.current, opts.previous)}
|
|
209
|
+
${body}
|
|
210
|
+
${footnotes}
|
|
211
|
+
</mj-column>
|
|
212
|
+
</mj-section>`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/reports/maintenance-email/template.ts
|
|
216
|
+
var escapeXml2 = escapeHtml;
|
|
217
|
+
var BLURRED_TESTS = `cid:${BLURRED_CID}`;
|
|
218
|
+
function fmtDate(d) {
|
|
219
|
+
if (!d || Number.isNaN(d.getTime())) return "";
|
|
220
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
221
|
+
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
222
|
+
const yyyy = d.getUTCFullYear();
|
|
223
|
+
return `${mm}.${dd}.${yyyy}`;
|
|
224
|
+
}
|
|
225
|
+
function maintenanceChecksSection(copy, searchPosition) {
|
|
226
|
+
const googleLabel = searchPosition !== void 0 ? `Page 1 Google Result (#${searchPosition})` : copy.maintenanceChecks[3] ?? "";
|
|
227
|
+
const rows = copy.maintenanceChecks.map((label, i) => i === 3 ? googleLabel : label);
|
|
228
|
+
return checklistRowsSection(rows, { background: "white", lastPaddingBottom: "36px" });
|
|
229
|
+
}
|
|
230
|
+
function testingChecklistSection(copy) {
|
|
231
|
+
return checklistRowsSection(copy.testingChecklist, {
|
|
232
|
+
background: "#F4F4F4",
|
|
233
|
+
lastPaddingBottom: "60px"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function maintenanceTestingPlaceholder(lastTested) {
|
|
237
|
+
return `
|
|
238
|
+
<mj-section background-color="#F4F4F4">
|
|
239
|
+
<mj-column>
|
|
240
|
+
<mj-image href="mailto:info@reddoorla.com" src="${BLURRED_TESTS}" />
|
|
241
|
+
</mj-column>
|
|
242
|
+
</mj-section>
|
|
243
|
+
<mj-section background-color="#F4F4F4" padding-top="0px">
|
|
244
|
+
<mj-column>
|
|
245
|
+
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">Last Tested: ${fmtDate(lastTested)}</mj-text>
|
|
246
|
+
</mj-column>
|
|
247
|
+
</mj-section>`;
|
|
248
|
+
}
|
|
249
|
+
function testingIntroSection(copy) {
|
|
250
|
+
return `
|
|
251
|
+
<mj-section background-color="#F4F4F4">
|
|
252
|
+
<mj-column>
|
|
253
|
+
<mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">TESTING</mj-text>
|
|
254
|
+
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml2(copy.testingIntro)}</mj-text>
|
|
255
|
+
</mj-column>
|
|
256
|
+
</mj-section>`;
|
|
257
|
+
}
|
|
258
|
+
function commentarySection(text, copy) {
|
|
259
|
+
return `
|
|
260
|
+
<mj-section background-color="white">
|
|
261
|
+
<mj-column>
|
|
262
|
+
<mj-text color="#C00" font-size="20px" font-weight="700" padding-top="55px">${escapeXml2(copy.notesHeader)}</mj-text>
|
|
263
|
+
<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>
|
|
264
|
+
</mj-column>
|
|
265
|
+
</mj-section>`;
|
|
266
|
+
}
|
|
267
|
+
function hasHeaderDims(data) {
|
|
268
|
+
return Boolean(data.headerWidth && data.headerHeight && data.headerBgColor);
|
|
269
|
+
}
|
|
270
|
+
function headerImageTag(data) {
|
|
271
|
+
const src = `cid:${data.headerImageCid}`;
|
|
272
|
+
const alt = `${escapeXml2(data.siteName)} maintenance report`;
|
|
273
|
+
const href = isHttpUrl(data.siteUrl) ? escapeXml2(data.siteUrl) : "#";
|
|
274
|
+
if (hasHeaderDims(data)) {
|
|
275
|
+
return `<mj-image href="${href}" src="${src}" alt="${alt}" width="${data.headerWidth}px" css-class="rd-header" container-background-color="${data.headerBgColor}" />`;
|
|
276
|
+
}
|
|
277
|
+
return `<mj-image href="${href}" src="${src}" alt="${alt}" />`;
|
|
278
|
+
}
|
|
279
|
+
function headerStyleBlock(data) {
|
|
280
|
+
if (!hasHeaderDims(data)) return "";
|
|
281
|
+
return `<mj-style>.rd-header img { height: auto !important; aspect-ratio: ${data.headerWidth} / ${data.headerHeight}; }</mj-style>`;
|
|
282
|
+
}
|
|
283
|
+
function buildMjml(data) {
|
|
284
|
+
const copy = data.copy ?? DEFAULT_COPY;
|
|
285
|
+
const isTesting = data.reportType === "Testing";
|
|
286
|
+
const previewText = `Checked up on ${escapeXml2(data.siteName)}`;
|
|
287
|
+
return `<mjml>
|
|
288
|
+
<mj-head>
|
|
289
|
+
<mj-attributes>
|
|
290
|
+
<mj-text font-family="helvetica, sans-serif" padding-left="5px" padding-right="5px" />
|
|
291
|
+
<mj-section padding-left="11%" padding-right="11%"/>
|
|
292
|
+
<mj-image padding="0px" />
|
|
293
|
+
</mj-attributes>
|
|
294
|
+
<mj-preview>${previewText}</mj-preview>
|
|
295
|
+
${headerStyleBlock(data)}
|
|
296
|
+
</mj-head>
|
|
297
|
+
<mj-body background-color="white">
|
|
298
|
+
<mj-section background-color="#F4F4F4" padding-top="0px" padding-bottom="0px" padding-left="0px" padding-right="0px">
|
|
299
|
+
<mj-column>
|
|
300
|
+
${headerImageTag(data)}
|
|
301
|
+
</mj-column>
|
|
302
|
+
</mj-section>
|
|
303
|
+
<mj-section background-color="white">
|
|
304
|
+
<mj-column>
|
|
305
|
+
<mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">COMPLETED ON</mj-text>
|
|
306
|
+
<mj-text color="#C00" font-size="44px" font-weight="400">${fmtDate(data.completedOn)}</mj-text>
|
|
307
|
+
<mj-text color="#C00" font-size="20px" font-weight="700" padding-top="75px">MAINTENANCE CHECKS</mj-text>
|
|
308
|
+
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml2(copy.maintenanceIntro)}</mj-text>
|
|
309
|
+
</mj-column>
|
|
310
|
+
</mj-section>
|
|
311
|
+
${maintenanceChecksSection(copy, data.searchPosition)}
|
|
312
|
+
${lighthouseScoresSection(data.lighthouse)}
|
|
313
|
+
${analyticsSection({
|
|
314
|
+
current: data.gaUsersCurrent,
|
|
315
|
+
previous: data.gaUsersPrevious,
|
|
316
|
+
background: "white",
|
|
317
|
+
footnoteLines: [escapeXml2(copy.seoCta)]
|
|
318
|
+
})}
|
|
319
|
+
${isTesting ? testingIntroSection(copy) + testingChecklistSection(copy) : maintenanceTestingPlaceholder(data.lastTestedDate)}
|
|
320
|
+
${data.commentary ? commentarySection(data.commentary, copy) : ""}
|
|
321
|
+
<mj-section background-color="white">
|
|
322
|
+
<mj-column padding-top="36px">
|
|
323
|
+
<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>
|
|
324
|
+
${copy.contact.map(
|
|
325
|
+
(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>`
|
|
326
|
+
).join("\n ")}
|
|
327
|
+
<mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
|
|
328
|
+
<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>
|
|
329
|
+
<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>
|
|
330
|
+
${[copy.footerOrg, ...copy.footerAddress].map(
|
|
331
|
+
(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>`
|
|
332
|
+
).join("\n ")}
|
|
333
|
+
</mj-column>
|
|
334
|
+
</mj-section>
|
|
335
|
+
</mj-body>
|
|
336
|
+
</mjml>`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/reports/announcement-email/template.ts
|
|
340
|
+
var FREQ_PHRASE = {
|
|
341
|
+
Monthly: "every month",
|
|
342
|
+
Quarterly: "every quarter",
|
|
343
|
+
Yearly: "every year"
|
|
344
|
+
};
|
|
345
|
+
var RED2 = "#C00";
|
|
346
|
+
var GREY2 = "#757575";
|
|
347
|
+
var SECTION_PAD = "40px";
|
|
348
|
+
function sectionLabel(text) {
|
|
349
|
+
return `<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="0px">${escapeXml2(text)}</mj-text>`;
|
|
350
|
+
}
|
|
351
|
+
function bodyLine(text, paddingTop = "8px") {
|
|
352
|
+
return `<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="${paddingTop}">${escapeXml2(text)}</mj-text>`;
|
|
353
|
+
}
|
|
354
|
+
function announcementSiteExtras(site) {
|
|
355
|
+
return {
|
|
356
|
+
cadence: { maintenance: site.maintenanceFreq, testing: site.testingFreq },
|
|
357
|
+
improvements: { resendForms: true, svelte5: true }
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function buildAnnouncementMjml(data) {
|
|
361
|
+
const copy = data.copy ?? DEFAULT_COPY;
|
|
362
|
+
const previewText = "Your monthly report from Reddoor";
|
|
363
|
+
const cad = data.cadence;
|
|
364
|
+
const hasMaint = Boolean(cad && cad.maintenance !== "None");
|
|
365
|
+
const hasTesting = Boolean(cad && cad.testing !== "None");
|
|
366
|
+
const improvementItems = [];
|
|
367
|
+
if (data.improvements?.resendForms) improvementItems.push(copy.announceImprovementResend);
|
|
368
|
+
if (data.improvements?.svelte5) improvementItems.push(copy.announceImprovementSvelte5);
|
|
369
|
+
const hasImpr = improvementItems.length > 0;
|
|
370
|
+
const BANDS = ["white", "#F4F4F4"];
|
|
371
|
+
let bandN = 0;
|
|
372
|
+
const nextBg = () => BANDS[bandN++ % 2];
|
|
373
|
+
const introBg = nextBg();
|
|
374
|
+
const maintBg = hasMaint ? nextBg() : "";
|
|
375
|
+
const testBg = hasTesting ? nextBg() : "";
|
|
376
|
+
const lighthouseBg = nextBg();
|
|
377
|
+
const analyticsBg = nextBg();
|
|
378
|
+
const improvementsBg = hasImpr ? nextBg() : "";
|
|
379
|
+
const contactBg = nextBg();
|
|
380
|
+
const maintenanceSection = cad && cad.maintenance !== "None" ? `
|
|
381
|
+
<mj-section background-color="${maintBg}" padding-top="${SECTION_PAD}" padding-bottom="0px">
|
|
382
|
+
<mj-column>
|
|
383
|
+
${sectionLabel("MAINTENANCE CHECKS")}
|
|
384
|
+
${bodyLine(`${copy.maintenanceIntro} We do this ${FREQ_PHRASE[cad.maintenance]}.${cad.testing === "None" ? ` ${copy.announceCadence}` : ""}`)}
|
|
385
|
+
</mj-column>
|
|
386
|
+
</mj-section>${checklistRowsSection(copy.maintenanceChecks, {
|
|
387
|
+
background: maintBg,
|
|
388
|
+
lastPaddingBottom: SECTION_PAD
|
|
389
|
+
})}` : "";
|
|
390
|
+
const testingSection = cad && cad.testing !== "None" ? `
|
|
391
|
+
<mj-section background-color="${testBg}" padding-top="${SECTION_PAD}" padding-bottom="0px">
|
|
392
|
+
<mj-column>
|
|
393
|
+
${sectionLabel("TESTING")}
|
|
394
|
+
${bodyLine(`${copy.testingIntro} We run a full test ${FREQ_PHRASE[cad.testing]}. ${copy.announceCadence}`)}
|
|
395
|
+
</mj-column>
|
|
396
|
+
</mj-section>${checklistRowsSection(copy.testingChecklist, {
|
|
397
|
+
background: testBg,
|
|
398
|
+
lastPaddingBottom: SECTION_PAD
|
|
399
|
+
})}` : "";
|
|
400
|
+
const analytics = analyticsSection({
|
|
401
|
+
current: data.gaUsersCurrent,
|
|
402
|
+
previous: data.gaUsersPrevious,
|
|
403
|
+
background: analyticsBg,
|
|
404
|
+
pad: SECTION_PAD,
|
|
405
|
+
bodyLines: data.searchPosition !== void 0 ? [`Page 1 Google result (#${data.searchPosition}) for your brand search`] : []
|
|
406
|
+
});
|
|
407
|
+
const improvementsSection = hasImpr ? `
|
|
408
|
+
<mj-section background-color="${improvementsBg}" padding-top="${SECTION_PAD}" padding-bottom="${SECTION_PAD}">
|
|
409
|
+
<mj-column>
|
|
410
|
+
${sectionLabel("RECENT IMPROVEMENTS")}
|
|
411
|
+
${improvementItems.map((item) => bodyLine(item)).join("\n ")}
|
|
412
|
+
${bodyLine(copy.announceOpenDoor, "16px")}
|
|
413
|
+
</mj-column>
|
|
414
|
+
</mj-section>` : "";
|
|
415
|
+
const contactRows = copy.contact.map(
|
|
416
|
+
(line) => `
|
|
417
|
+
<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
|
|
418
|
+
).join("");
|
|
419
|
+
const footerAddressRows = copy.footerAddress.map(
|
|
420
|
+
(line) => `
|
|
421
|
+
<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>`
|
|
422
|
+
).join("");
|
|
423
|
+
return `<mjml>
|
|
424
|
+
<mj-head>
|
|
425
|
+
<mj-attributes>
|
|
426
|
+
<mj-text font-family="helvetica, sans-serif" padding-left="5px" padding-right="5px" />
|
|
427
|
+
<mj-section padding-left="11%" padding-right="11%"/>
|
|
428
|
+
<mj-image padding="0px" />
|
|
429
|
+
</mj-attributes>
|
|
430
|
+
<mj-preview>${escapeXml2(previewText)}</mj-preview>
|
|
431
|
+
${headerStyleBlock(data)}
|
|
432
|
+
</mj-head>
|
|
433
|
+
<mj-body background-color="white">
|
|
434
|
+
<mj-section background-color="#F4F4F4" padding-top="0px" padding-bottom="0px" padding-left="0px" padding-right="0px">
|
|
435
|
+
<mj-column>${headerImageTag(data)}</mj-column>
|
|
436
|
+
</mj-section>
|
|
437
|
+
<mj-section background-color="${introBg}" padding-top="${SECTION_PAD}" padding-bottom="${SECTION_PAD}">
|
|
438
|
+
<mj-column>
|
|
439
|
+
${sectionLabel(copy.announceHeading)}
|
|
440
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="20px">Prepared for ${escapeXml2(data.siteName)}</mj-text>
|
|
441
|
+
${bodyLine(copy.announceBody)}
|
|
442
|
+
</mj-column>
|
|
443
|
+
</mj-section>
|
|
444
|
+
${maintenanceSection}
|
|
445
|
+
${testingSection}
|
|
446
|
+
${lighthouseScoresSection(data.lighthouse, { background: lighthouseBg, pad: SECTION_PAD })}
|
|
447
|
+
${analytics}
|
|
448
|
+
${improvementsSection}
|
|
449
|
+
<mj-section background-color="${contactBg}" padding-top="${SECTION_PAD}" padding-bottom="${SECTION_PAD}">
|
|
450
|
+
<mj-column>
|
|
451
|
+
<mj-text color="${RED2}" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" padding-top="0px" line-height="36px">Any questions, concerns or requests?</mj-text>
|
|
452
|
+
${contactRows}
|
|
453
|
+
<mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
|
|
454
|
+
<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>
|
|
455
|
+
<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>
|
|
456
|
+
<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>
|
|
457
|
+
${footerAddressRows}
|
|
458
|
+
</mj-column>
|
|
459
|
+
</mj-section>
|
|
460
|
+
</mj-body>
|
|
461
|
+
</mjml>`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/reports/render.ts
|
|
465
|
+
import mjml2html from "mjml";
|
|
466
|
+
|
|
467
|
+
// src/reports/launch-email/template.ts
|
|
468
|
+
var RED3 = "#C00";
|
|
469
|
+
var GREY3 = "#757575";
|
|
470
|
+
function buildLaunchMjml(data) {
|
|
471
|
+
const copy = data.copy ?? DEFAULT_COPY;
|
|
472
|
+
const previewText = `${escapeXml2(data.siteName)} is live`;
|
|
473
|
+
const setupRows = copy.launchSetupItems.map(
|
|
474
|
+
(item) => `
|
|
475
|
+
<mj-text color="${GREY3}" 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>`
|
|
476
|
+
).join("");
|
|
477
|
+
const contactRows = copy.contact.map(
|
|
478
|
+
(line) => `
|
|
479
|
+
<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
|
|
480
|
+
).join("");
|
|
481
|
+
const footerAddressRows = copy.footerAddress.map(
|
|
482
|
+
(line) => `
|
|
483
|
+
<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>`
|
|
484
|
+
).join("");
|
|
485
|
+
return `<mjml>
|
|
486
|
+
<mj-head>
|
|
487
|
+
<mj-attributes>
|
|
488
|
+
<mj-text font-family="helvetica, sans-serif" padding-left="5px" padding-right="5px" />
|
|
489
|
+
<mj-section padding-left="11%" padding-right="11%"/>
|
|
490
|
+
<mj-image padding="0px" />
|
|
491
|
+
</mj-attributes>
|
|
492
|
+
<mj-preview>${previewText}</mj-preview>
|
|
493
|
+
${headerStyleBlock(data)}
|
|
494
|
+
</mj-head>
|
|
495
|
+
<mj-body background-color="white">
|
|
496
|
+
<mj-section background-color="#F4F4F4" padding-top="0px" padding-bottom="0px" padding-left="0px" padding-right="0px">
|
|
497
|
+
<mj-column>${headerImageTag(data)}</mj-column>
|
|
498
|
+
</mj-section>
|
|
499
|
+
<mj-section background-color="white">
|
|
500
|
+
<mj-column>
|
|
501
|
+
<mj-text color="${RED3}" font-size="20px" font-weight="700" padding-top="75px">${escapeXml2(copy.launchHeading)}</mj-text>
|
|
502
|
+
<mj-text color="${RED3}" font-size="44px" font-weight="400">${fmtDate(data.completedOn)}</mj-text>
|
|
503
|
+
<mj-text color="${GREY3}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="20px">${escapeXml2(copy.launchBody)}</mj-text>
|
|
504
|
+
${setupRows}
|
|
505
|
+
</mj-column>
|
|
506
|
+
</mj-section>
|
|
507
|
+
<mj-section background-color="white">
|
|
508
|
+
<mj-column padding-top="36px">
|
|
509
|
+
<mj-text color="${RED3}" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" padding-top="36px" line-height="36px">Any questions, concerns or requests?</mj-text>
|
|
510
|
+
${contactRows}
|
|
511
|
+
<mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
|
|
512
|
+
<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>
|
|
513
|
+
<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>
|
|
514
|
+
<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>
|
|
515
|
+
${footerAddressRows}
|
|
516
|
+
</mj-column>
|
|
517
|
+
</mj-section>
|
|
518
|
+
</mj-body>
|
|
519
|
+
</mjml>`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/reports/render.ts
|
|
523
|
+
async function renderReportHtml(data) {
|
|
524
|
+
const mjml = data.reportType === "Launch" ? buildLaunchMjml(data) : data.reportType === "Announcement" ? buildAnnouncementMjml(data) : buildMjml(data);
|
|
525
|
+
const out = await mjml2html(mjml, { validationLevel: "strict" });
|
|
526
|
+
return { html: out.html, warnings: out.errors ?? [] };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/reports/airtable/attachments.ts
|
|
530
|
+
function looksLikeHtml(bytes) {
|
|
531
|
+
const start = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
|
|
532
|
+
const head = Buffer.from(bytes.slice(start, start + 64)).toString("ascii").replace(/^[\s]+/, "").toLowerCase();
|
|
533
|
+
return head.startsWith("<!doctype html") || head.startsWith("<html") || head.startsWith("<head");
|
|
534
|
+
}
|
|
535
|
+
async function fetchAttachmentBytes(url) {
|
|
536
|
+
const res = await fetch(url);
|
|
537
|
+
if (!res.ok) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`Failed to fetch Airtable attachment ${res.status} ${res.statusText} (url=${url})`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
543
|
+
const ab = await res.arrayBuffer();
|
|
544
|
+
const bytes = new Uint8Array(ab);
|
|
545
|
+
const isImageType = contentType.toLowerCase().startsWith("image/");
|
|
546
|
+
if (!isImageType && looksLikeHtml(bytes)) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`Airtable attachment did not return image data (content-type="${contentType}", body looks like an HTML page \u2014 the signed URL may have expired) (url=${url})`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
return { bytes, contentType };
|
|
552
|
+
}
|
|
553
|
+
async function uploadAttachment(recordId, fieldName, body, filename, contentType) {
|
|
554
|
+
const apiKey = process.env.AIRTABLE_PAT;
|
|
555
|
+
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
556
|
+
if (!apiKey || !baseId) {
|
|
557
|
+
throw new Error("AIRTABLE_PAT and AIRTABLE_BASE_ID must be set");
|
|
558
|
+
}
|
|
559
|
+
const base64 = typeof body === "string" ? Buffer.from(body, "utf-8").toString("base64") : Buffer.from(body).toString("base64");
|
|
560
|
+
const payload = { contentType, file: base64, filename };
|
|
561
|
+
const url = `https://content.airtable.com/v0/${baseId}/${recordId}/${encodeURIComponent(fieldName)}/uploadAttachment`;
|
|
562
|
+
const res = await fetch(url, {
|
|
563
|
+
method: "POST",
|
|
564
|
+
headers: {
|
|
565
|
+
Authorization: `Bearer ${apiKey}`,
|
|
566
|
+
"Content-Type": "application/json"
|
|
567
|
+
},
|
|
568
|
+
body: JSON.stringify(payload)
|
|
569
|
+
});
|
|
570
|
+
if (!res.ok) {
|
|
571
|
+
throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export {
|
|
576
|
+
resolveCopy,
|
|
577
|
+
CHECK_CID,
|
|
578
|
+
BLURRED_CID,
|
|
579
|
+
loadBundledImages,
|
|
580
|
+
announcementSiteExtras,
|
|
581
|
+
renderReportHtml,
|
|
582
|
+
fetchAttachmentBytes,
|
|
583
|
+
uploadAttachment
|
|
584
|
+
};
|
|
585
|
+
//# sourceMappingURL=chunk-BWLUNY7L.js.map
|