@reddoorla/maintenance 0.41.0 → 0.43.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 +331 -17
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/forms/index.d.ts +11 -2
- package/dist/forms/index.js +1 -1
- package/dist/forms/index.js.map +1 -1
- package/dist/index.d.ts +33 -2
- package/dist/index.js +321 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/bin.js
CHANGED
|
@@ -575,6 +575,39 @@ var init_write_audits_to_airtable = __esm({
|
|
|
575
575
|
}
|
|
576
576
|
});
|
|
577
577
|
|
|
578
|
+
// src/reports/checklist.ts
|
|
579
|
+
function checklistFor(type) {
|
|
580
|
+
return type === "Maintenance" ? MAINTENANCE_CHECKLIST : type === "Testing" ? TESTING_CHECKLIST : [];
|
|
581
|
+
}
|
|
582
|
+
function isChecklistComplete(report) {
|
|
583
|
+
return checklistFor(report.reportType).every((i) => report.checklist[i.field] === true);
|
|
584
|
+
}
|
|
585
|
+
var MAINTENANCE_CHECKLIST, TESTING_CHECKLIST, ALL_CHECKLIST_FIELDS;
|
|
586
|
+
var init_checklist = __esm({
|
|
587
|
+
"src/reports/checklist.ts"() {
|
|
588
|
+
"use strict";
|
|
589
|
+
MAINTENANCE_CHECKLIST = [
|
|
590
|
+
{ key: "logs", label: "Reviewed Logs", field: "Maint: Reviewed Logs" },
|
|
591
|
+
{ key: "cms", label: "CMS Checked", field: "Maint: CMS Checked" },
|
|
592
|
+
{ key: "dns", label: "DNS Checked", field: "Maint: DNS Checked" },
|
|
593
|
+
{ key: "google", label: "Google Indexed", field: "Maint: Google Indexed" },
|
|
594
|
+
{ key: "cert", label: "Reviewed Certificate", field: "Maint: Reviewed Certificate" },
|
|
595
|
+
{ key: "security", label: "Security Updates", field: "Maint: Security Updates" }
|
|
596
|
+
];
|
|
597
|
+
TESTING_CHECKLIST = [
|
|
598
|
+
{ key: "desktop", label: "Desktop Browsers", field: "Test: Desktop Browsers" },
|
|
599
|
+
{ key: "mobile", label: "Mobile Browsers", field: "Test: Mobile Browsers" },
|
|
600
|
+
{ key: "packages", label: "Package Updates", field: "Test: Package Updates" },
|
|
601
|
+
{ key: "bottle", label: "Bottlenecks", field: "Test: Bottlenecks" },
|
|
602
|
+
{ key: "forms", label: "Form Functionality", field: "Test: Form Functionality" },
|
|
603
|
+
{ key: "animation", label: "Animation Functionality", field: "Test: Animation Functionality" }
|
|
604
|
+
];
|
|
605
|
+
ALL_CHECKLIST_FIELDS = [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map(
|
|
606
|
+
(i) => i.field
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
578
611
|
// src/reports/airtable/reports.ts
|
|
579
612
|
function toReportType(raw) {
|
|
580
613
|
if (raw && REPORT_TYPES.includes(raw)) return raw;
|
|
@@ -613,7 +646,8 @@ function mapRow2(rec) {
|
|
|
613
646
|
approvedBy: f["Approved By"] ?? null,
|
|
614
647
|
deliveryStatus: f["Delivery status"] ?? "pending",
|
|
615
648
|
renderedHtmlAttachment: html,
|
|
616
|
-
resendMessageId: f["Resend message ID"] ?? null
|
|
649
|
+
resendMessageId: f["Resend message ID"] ?? null,
|
|
650
|
+
checklist: Object.fromEntries(ALL_CHECKLIST_FIELDS.map((name) => [name, Boolean(f[name])]))
|
|
617
651
|
};
|
|
618
652
|
}
|
|
619
653
|
function lighthouseFromFields(f) {
|
|
@@ -651,6 +685,7 @@ async function createDraft(base, input) {
|
|
|
651
685
|
if (input.searchFoundPage1 !== void 0) fields["Search found page 1"] = input.searchFoundPage1;
|
|
652
686
|
if (input.searchPosition !== void 0) fields["Search position"] = input.searchPosition;
|
|
653
687
|
if (input.period !== void 0) fields["Period"] = input.period;
|
|
688
|
+
if (input.subjectOverride !== void 0) fields["Subject override"] = input.subjectOverride;
|
|
654
689
|
const created = await base(REPORTS_TABLE).create([{ fields }]);
|
|
655
690
|
const rec = created[0];
|
|
656
691
|
if (!rec) throw new Error("Airtable create returned no records");
|
|
@@ -716,8 +751,9 @@ var REPORTS_TABLE, REPORT_TYPES;
|
|
|
716
751
|
var init_reports = __esm({
|
|
717
752
|
"src/reports/airtable/reports.ts"() {
|
|
718
753
|
"use strict";
|
|
754
|
+
init_checklist();
|
|
719
755
|
REPORTS_TABLE = "Reports";
|
|
720
|
-
REPORT_TYPES = ["Maintenance", "Testing", "Launch"];
|
|
756
|
+
REPORT_TYPES = ["Maintenance", "Testing", "Launch", "Announcement"];
|
|
721
757
|
}
|
|
722
758
|
});
|
|
723
759
|
|
|
@@ -777,7 +813,18 @@ var init_copy = __esm({
|
|
|
777
813
|
"Hosting, DNS, and SSL configured",
|
|
778
814
|
"Continuous integration + automatic dependency updates",
|
|
779
815
|
"Analytics and uptime monitoring"
|
|
780
|
-
]
|
|
816
|
+
],
|
|
817
|
+
announceHeading: "YOUR ONGOING SITE CARE",
|
|
818
|
+
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:",
|
|
819
|
+
announceCadenceHeading: "WHAT TO EXPECT",
|
|
820
|
+
announceTestingLabel: "Full site testing",
|
|
821
|
+
announceMaintenanceLabel: "Routine maintenance",
|
|
822
|
+
announceMonitorItems: ["Performance", "Accessibility", "Security", "Uptime"],
|
|
823
|
+
announcePreviewLabel: "From your latest full site test:",
|
|
824
|
+
announceImprovementResend: "Your contact forms now deliver straight to your inbox through reliable infrastructure, so no inquiry slips through the cracks.",
|
|
825
|
+
announceImprovementSvelte5: "We've modernized your site to the latest framework \u2014 it's faster, more secure, and built to last.",
|
|
826
|
+
announceCadence: "After each one we'll send you a short report like this \u2014 there's nothing you need to do.",
|
|
827
|
+
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."
|
|
781
828
|
};
|
|
782
829
|
}
|
|
783
830
|
});
|
|
@@ -1114,10 +1161,137 @@ var init_template2 = __esm({
|
|
|
1114
1161
|
}
|
|
1115
1162
|
});
|
|
1116
1163
|
|
|
1164
|
+
// src/reports/announcement-email/template.ts
|
|
1165
|
+
function buildAnnouncementMjml(data) {
|
|
1166
|
+
const copy = data.copy ?? DEFAULT_COPY;
|
|
1167
|
+
const previewText = "Your monthly report from Reddoor";
|
|
1168
|
+
const improvementItems = [];
|
|
1169
|
+
if (data.improvements?.resendForms) improvementItems.push(copy.announceImprovementResend);
|
|
1170
|
+
if (data.improvements?.svelte5) improvementItems.push(copy.announceImprovementSvelte5);
|
|
1171
|
+
const improvementsSection = improvementItems.length > 0 ? `
|
|
1172
|
+
<mj-section background-color="white">
|
|
1173
|
+
<mj-column>
|
|
1174
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">RECENT IMPROVEMENTS</mj-text>
|
|
1175
|
+
${improvementItems.map(
|
|
1176
|
+
(item) => `
|
|
1177
|
+
<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>`
|
|
1178
|
+
).join("")}
|
|
1179
|
+
</mj-column>
|
|
1180
|
+
</mj-section>` : "";
|
|
1181
|
+
const cad = data.cadence;
|
|
1182
|
+
const cadenceLines = [];
|
|
1183
|
+
if (cad && cad.testing !== "None")
|
|
1184
|
+
cadenceLines.push(`${copy.announceTestingLabel} \u2014 ${FREQ_PHRASE[cad.testing]}`);
|
|
1185
|
+
if (cad && cad.maintenance !== "None")
|
|
1186
|
+
cadenceLines.push(`${copy.announceMaintenanceLabel} \u2014 ${FREQ_PHRASE[cad.maintenance]}`);
|
|
1187
|
+
const cadenceSection = cadenceLines.length > 0 ? `
|
|
1188
|
+
<mj-section background-color="white">
|
|
1189
|
+
<mj-column>
|
|
1190
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">${escapeXml(copy.announceCadenceHeading)}</mj-text>
|
|
1191
|
+
${cadenceLines.map(
|
|
1192
|
+
(line) => `
|
|
1193
|
+
<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(line)}</mj-text>`
|
|
1194
|
+
).join("")}
|
|
1195
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="12px">${escapeXml(copy.announceCadence)}</mj-text>
|
|
1196
|
+
</mj-column>
|
|
1197
|
+
</mj-section>` : "";
|
|
1198
|
+
const monitorRows = copy.announceMonitorItems.map(
|
|
1199
|
+
(item) => `
|
|
1200
|
+
<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>`
|
|
1201
|
+
).join("");
|
|
1202
|
+
const scoreRows = SCORE_PREVIEW.map(
|
|
1203
|
+
({ label, key }) => `
|
|
1204
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="300" padding-top="25px">${label}</mj-text>
|
|
1205
|
+
<mj-text color="${RED2}" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse[key]}</mj-text>`
|
|
1206
|
+
).join("");
|
|
1207
|
+
const contactRows = copy.contact.map(
|
|
1208
|
+
(line) => `
|
|
1209
|
+
<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
|
|
1210
|
+
).join("");
|
|
1211
|
+
const footerAddressRows = copy.footerAddress.map(
|
|
1212
|
+
(line) => `
|
|
1213
|
+
<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>`
|
|
1214
|
+
).join("");
|
|
1215
|
+
return `<mjml>
|
|
1216
|
+
<mj-head>
|
|
1217
|
+
<mj-attributes>
|
|
1218
|
+
<mj-text font-family="helvetica, sans-serif" padding-left="5px" padding-right="5px" />
|
|
1219
|
+
<mj-section padding-left="11%" padding-right="11%"/>
|
|
1220
|
+
<mj-image padding="0px" />
|
|
1221
|
+
</mj-attributes>
|
|
1222
|
+
<mj-preview>${escapeXml(previewText)}</mj-preview>
|
|
1223
|
+
${headerStyleBlock(data)}
|
|
1224
|
+
</mj-head>
|
|
1225
|
+
<mj-body background-color="white">
|
|
1226
|
+
<mj-section background-color="#F4F4F4" padding-top="0px" padding-bottom="0px" padding-left="0px" padding-right="0px">
|
|
1227
|
+
<mj-column>${headerImageTag(data)}</mj-column>
|
|
1228
|
+
</mj-section>
|
|
1229
|
+
<mj-section background-color="white">
|
|
1230
|
+
<mj-column>
|
|
1231
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="75px">${escapeXml(copy.announceHeading)}</mj-text>
|
|
1232
|
+
<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>
|
|
1233
|
+
<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>
|
|
1234
|
+
</mj-column>
|
|
1235
|
+
</mj-section>
|
|
1236
|
+
${cadenceSection}
|
|
1237
|
+
${improvementsSection}
|
|
1238
|
+
<mj-section background-color="white">
|
|
1239
|
+
<mj-column>
|
|
1240
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">WHAT WE MONITOR</mj-text>
|
|
1241
|
+
${monitorRows}
|
|
1242
|
+
</mj-column>
|
|
1243
|
+
</mj-section>
|
|
1244
|
+
<mj-section background-color="#F4F4F4">
|
|
1245
|
+
<mj-column>
|
|
1246
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="55px">${escapeXml(copy.announcePreviewLabel)}</mj-text>
|
|
1247
|
+
${scoreRows}
|
|
1248
|
+
</mj-column>
|
|
1249
|
+
</mj-section>
|
|
1250
|
+
<mj-section background-color="white">
|
|
1251
|
+
<mj-column>
|
|
1252
|
+
<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>
|
|
1253
|
+
</mj-column>
|
|
1254
|
+
</mj-section>
|
|
1255
|
+
<mj-section background-color="white">
|
|
1256
|
+
<mj-column padding-top="36px">
|
|
1257
|
+
<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>
|
|
1258
|
+
${contactRows}
|
|
1259
|
+
<mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
|
|
1260
|
+
<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>
|
|
1261
|
+
<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>
|
|
1262
|
+
<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>
|
|
1263
|
+
${footerAddressRows}
|
|
1264
|
+
</mj-column>
|
|
1265
|
+
</mj-section>
|
|
1266
|
+
</mj-body>
|
|
1267
|
+
</mjml>`;
|
|
1268
|
+
}
|
|
1269
|
+
var FREQ_PHRASE, RED2, GREY2, SCORE_PREVIEW;
|
|
1270
|
+
var init_template3 = __esm({
|
|
1271
|
+
"src/reports/announcement-email/template.ts"() {
|
|
1272
|
+
"use strict";
|
|
1273
|
+
init_copy();
|
|
1274
|
+
init_template();
|
|
1275
|
+
FREQ_PHRASE = {
|
|
1276
|
+
Monthly: "every month",
|
|
1277
|
+
Quarterly: "every quarter",
|
|
1278
|
+
Yearly: "every year"
|
|
1279
|
+
};
|
|
1280
|
+
RED2 = "#C00";
|
|
1281
|
+
GREY2 = "#757575";
|
|
1282
|
+
SCORE_PREVIEW = [
|
|
1283
|
+
{ label: "Performance", key: "performance" },
|
|
1284
|
+
{ label: "Readability", key: "accessibility" },
|
|
1285
|
+
{ label: "Best Practices", key: "bestPractices" },
|
|
1286
|
+
{ label: "Site Structure", key: "seo" }
|
|
1287
|
+
];
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1117
1291
|
// src/reports/render.ts
|
|
1118
1292
|
import mjml2html from "mjml";
|
|
1119
1293
|
async function renderReportHtml(data) {
|
|
1120
|
-
const mjml = data.reportType === "Launch" ? buildLaunchMjml(data) : buildMjml(data);
|
|
1294
|
+
const mjml = data.reportType === "Launch" ? buildLaunchMjml(data) : data.reportType === "Announcement" ? buildAnnouncementMjml(data) : buildMjml(data);
|
|
1121
1295
|
const out = await mjml2html(mjml, { validationLevel: "strict" });
|
|
1122
1296
|
return { html: out.html, warnings: out.errors ?? [] };
|
|
1123
1297
|
}
|
|
@@ -1126,6 +1300,7 @@ var init_render = __esm({
|
|
|
1126
1300
|
"use strict";
|
|
1127
1301
|
init_template();
|
|
1128
1302
|
init_template2();
|
|
1303
|
+
init_template3();
|
|
1129
1304
|
}
|
|
1130
1305
|
});
|
|
1131
1306
|
|
|
@@ -1419,16 +1594,16 @@ __export(digest_exports, {
|
|
|
1419
1594
|
runDigest: () => runDigest
|
|
1420
1595
|
});
|
|
1421
1596
|
function readySection(items) {
|
|
1422
|
-
const heading = `<h2 style="color:${
|
|
1597
|
+
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>`;
|
|
1423
1598
|
if (items.length === 0) {
|
|
1424
|
-
return `${heading}<p style="color:${
|
|
1599
|
+
return `${heading}<p style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;margin:0">Nothing waiting on you.</p>`;
|
|
1425
1600
|
}
|
|
1426
1601
|
const rows = items.map((it) => {
|
|
1427
1602
|
const safeUrl = it.dashboardUrl.startsWith("https://") ? it.dashboardUrl : void 0;
|
|
1428
1603
|
const link = safeUrl ? `<a href="${escapeHtml(safeUrl)}" style="${ANCHOR_STYLE}">review & approve</a>` : `review & approve`;
|
|
1429
1604
|
return `
|
|
1430
1605
|
<tr>
|
|
1431
|
-
<td style="color:${
|
|
1606
|
+
<td style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px">
|
|
1432
1607
|
<strong style="color:#222">${escapeHtml(it.siteName)}</strong> \u2014 ${escapeHtml(it.reportType)} (${escapeHtml(it.period)})
|
|
1433
1608
|
\u2014 ${link}
|
|
1434
1609
|
</td>
|
|
@@ -1438,15 +1613,15 @@ function readySection(items) {
|
|
|
1438
1613
|
}
|
|
1439
1614
|
function attentionBadge(status) {
|
|
1440
1615
|
if (status === "new")
|
|
1441
|
-
return `<strong style="color:${
|
|
1616
|
+
return `<strong style="color:${RED3};font-family:helvetica,sans-serif">NEW</strong> `;
|
|
1442
1617
|
if (status === "worse")
|
|
1443
|
-
return `<strong style="color:${
|
|
1618
|
+
return `<strong style="color:${RED3};font-family:helvetica,sans-serif">WORSE</strong> `;
|
|
1444
1619
|
return "";
|
|
1445
1620
|
}
|
|
1446
1621
|
function attentionSection(items) {
|
|
1447
|
-
const heading = `<h2 style="color:${
|
|
1622
|
+
const heading = `<h2 style="color:${RED3};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px">Needs attention</h2>`;
|
|
1448
1623
|
if (items.length === 0) {
|
|
1449
|
-
return `${heading}<p style="color:${
|
|
1624
|
+
return `${heading}<p style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;margin:0">All clear \u2014 nothing needs attention.</p>`;
|
|
1450
1625
|
}
|
|
1451
1626
|
const bySite = /* @__PURE__ */ new Map();
|
|
1452
1627
|
for (const it of items) {
|
|
@@ -1463,7 +1638,7 @@ function attentionSection(items) {
|
|
|
1463
1638
|
const titleHtml = safeUrl ? `<a href="${escapeHtml(safeUrl)}" style="${ANCHOR_STYLE}">${escapeHtml(it.title)}</a>` : escapeHtml(it.title);
|
|
1464
1639
|
return `
|
|
1465
1640
|
<tr>
|
|
1466
|
-
<td style="color:${
|
|
1641
|
+
<td style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px">${attentionBadge(it.status)}${titleHtml}</td>
|
|
1467
1642
|
</tr>`;
|
|
1468
1643
|
}).join("");
|
|
1469
1644
|
return `
|
|
@@ -1588,7 +1763,7 @@ function renderDigestHtml(sections) {
|
|
|
1588
1763
|
<table width="600" style="border-collapse:collapse">
|
|
1589
1764
|
<tr>
|
|
1590
1765
|
<td>
|
|
1591
|
-
<h1 style="color:${
|
|
1766
|
+
<h1 style="color:${RED3};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px">Your fleet today</h1>
|
|
1592
1767
|
${readySection(sections.readyForYourYes)}
|
|
1593
1768
|
${attentionSection(sections.needsAttention)}
|
|
1594
1769
|
</td>
|
|
@@ -1600,7 +1775,7 @@ function renderDigestHtml(sections) {
|
|
|
1600
1775
|
</body>
|
|
1601
1776
|
</html>`;
|
|
1602
1777
|
}
|
|
1603
|
-
var
|
|
1778
|
+
var GREY3, RED3, ANCHOR_STYLE, SEVERITY_ORDER, FROM_ADDRESS, DIGEST_OPERATOR_FALLBACK;
|
|
1604
1779
|
var init_digest = __esm({
|
|
1605
1780
|
"src/reports/digest.ts"() {
|
|
1606
1781
|
"use strict";
|
|
@@ -1612,9 +1787,9 @@ var init_digest = __esm({
|
|
|
1612
1787
|
init_digest_collectors();
|
|
1613
1788
|
init_digest_state();
|
|
1614
1789
|
init_html();
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
ANCHOR_STYLE = `color:${
|
|
1790
|
+
GREY3 = "#757575";
|
|
1791
|
+
RED3 = "#C00";
|
|
1792
|
+
ANCHOR_STYLE = `color:${RED3};font-family:helvetica,sans-serif`;
|
|
1618
1793
|
SEVERITY_ORDER = { critical: 0, warning: 1 };
|
|
1619
1794
|
FROM_ADDRESS = "Reddoor Reports <reports@reddoorla.com>";
|
|
1620
1795
|
DIGEST_OPERATOR_FALLBACK = "info@reddoorla.com";
|
|
@@ -1712,6 +1887,13 @@ async function sendApprovedReports(options = {}) {
|
|
|
1712
1887
|
return { output: lines.join("\n"), code: anyFailed ? 1 : 0 };
|
|
1713
1888
|
}
|
|
1714
1889
|
async function sendOne(client, base, site, report) {
|
|
1890
|
+
if (!isChecklistComplete(report)) {
|
|
1891
|
+
const items = checklistFor(report.reportType);
|
|
1892
|
+
const done = items.filter((i) => report.checklist[i.field] === true).length;
|
|
1893
|
+
throw new Error(
|
|
1894
|
+
`Report ${report.reportId} checklist incomplete \u2014 ${done}/${items.length} items checked`
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1715
1897
|
if (!site.headerImage) {
|
|
1716
1898
|
throw new Error(`Site '${site.name}' has no Header image set on the Websites row`);
|
|
1717
1899
|
}
|
|
@@ -1855,6 +2037,7 @@ var init_orchestrate = __esm({
|
|
|
1855
2037
|
init_header_image();
|
|
1856
2038
|
init_resend();
|
|
1857
2039
|
init_idempotency();
|
|
2040
|
+
init_checklist();
|
|
1858
2041
|
FROM_ADDRESS2 = "Reddoor Reports <reports@reddoorla.com>";
|
|
1859
2042
|
REPLY_TO = "info@reddoorla.com";
|
|
1860
2043
|
MONTHS2 = [
|
|
@@ -6211,6 +6394,131 @@ async function runLaunchCommand(site, opts) {
|
|
|
6211
6394
|
return { output: formatResult9(result), code: result.complete ? 0 : 1 };
|
|
6212
6395
|
}
|
|
6213
6396
|
|
|
6397
|
+
// src/recipes/announce.ts
|
|
6398
|
+
init_client();
|
|
6399
|
+
init_websites();
|
|
6400
|
+
init_reports();
|
|
6401
|
+
init_attachments();
|
|
6402
|
+
init_render();
|
|
6403
|
+
init_copy();
|
|
6404
|
+
async function announce(deps) {
|
|
6405
|
+
const base = deps?.base ?? openBase(readAirtableConfig());
|
|
6406
|
+
const now = deps?.now ?? /* @__PURE__ */ new Date();
|
|
6407
|
+
const websites = await listWebsites(base);
|
|
6408
|
+
let targets = websites.filter((w) => w.status === "maintenance");
|
|
6409
|
+
if (deps?.site) {
|
|
6410
|
+
const wanted = siteSlug(deps.site);
|
|
6411
|
+
targets = targets.filter((w) => siteSlug(w.name) === wanted);
|
|
6412
|
+
}
|
|
6413
|
+
const period = now.toISOString().slice(0, 7);
|
|
6414
|
+
const results = [];
|
|
6415
|
+
for (const w of targets) {
|
|
6416
|
+
try {
|
|
6417
|
+
const scores = scoresFromRow(w);
|
|
6418
|
+
if (scores === null) {
|
|
6419
|
+
results.push({ site: w.name, status: "skipped-no-scores" });
|
|
6420
|
+
continue;
|
|
6421
|
+
}
|
|
6422
|
+
let report;
|
|
6423
|
+
let statusKind;
|
|
6424
|
+
const existing = await findReportByPeriod(base, w.id, "Announcement", period);
|
|
6425
|
+
if (existing) {
|
|
6426
|
+
await updateReportScores(base, existing.id, scores, now);
|
|
6427
|
+
report = existing;
|
|
6428
|
+
statusKind = "reused";
|
|
6429
|
+
} else {
|
|
6430
|
+
report = await createDraft(base, draftInputFor2(w, scores, now, period));
|
|
6431
|
+
statusKind = "drafted";
|
|
6432
|
+
}
|
|
6433
|
+
const slug = siteSlug(w.name);
|
|
6434
|
+
const { html } = await renderReportHtml({
|
|
6435
|
+
siteName: w.name,
|
|
6436
|
+
siteUrl: w.url,
|
|
6437
|
+
reportType: "Announcement",
|
|
6438
|
+
completedOn: now,
|
|
6439
|
+
lighthouse: scores,
|
|
6440
|
+
lastTestedDate: null,
|
|
6441
|
+
commentary: null,
|
|
6442
|
+
copy: resolveCopy(w),
|
|
6443
|
+
headerImageCid: `${slug}-header`,
|
|
6444
|
+
// The client's go-forward pace, read straight off the Websites row — the email
|
|
6445
|
+
// states each cadence ("Full site testing — every quarter", etc.); a "None" pace
|
|
6446
|
+
// is omitted so we never claim a cadence the site isn't on.
|
|
6447
|
+
cadence: { maintenance: w.maintenanceFreq, testing: w.testingFreq },
|
|
6448
|
+
// Default-on fleet-wide for v1: both recent-improvement callouts render for every
|
|
6449
|
+
// site. Operator review (the draft never auto-sends) is the relevance backstop;
|
|
6450
|
+
// per-site conditioning of these flags is a future lever, not a v1 requirement.
|
|
6451
|
+
improvements: { resendForms: true, svelte5: true }
|
|
6452
|
+
});
|
|
6453
|
+
try {
|
|
6454
|
+
await uploadAttachment(
|
|
6455
|
+
report.id,
|
|
6456
|
+
"Rendered HTML",
|
|
6457
|
+
html,
|
|
6458
|
+
`${slug}-${now.toISOString().slice(0, 10)}.html`,
|
|
6459
|
+
"text/html"
|
|
6460
|
+
);
|
|
6461
|
+
} catch (uploadErr) {
|
|
6462
|
+
console.warn(
|
|
6463
|
+
`\u26A0 Announcement preview upload skipped for ${w.name}: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
|
|
6464
|
+
);
|
|
6465
|
+
}
|
|
6466
|
+
await setDraftReady(base, report.id, true);
|
|
6467
|
+
const recipientMissing = !(w.reportRecipientsTo && w.reportRecipientsTo.trim());
|
|
6468
|
+
results.push({ site: w.name, status: statusKind, reportId: report.id, recipientMissing });
|
|
6469
|
+
} catch (err) {
|
|
6470
|
+
results.push({
|
|
6471
|
+
site: w.name,
|
|
6472
|
+
status: "error",
|
|
6473
|
+
message: err instanceof Error ? err.message : String(err)
|
|
6474
|
+
});
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
return { results };
|
|
6478
|
+
}
|
|
6479
|
+
function scoresFromRow(w) {
|
|
6480
|
+
if (w.pScore === null || w.rScore === null || w.bpScore === null || w.seoScore === null) {
|
|
6481
|
+
return null;
|
|
6482
|
+
}
|
|
6483
|
+
return {
|
|
6484
|
+
performance: w.pScore,
|
|
6485
|
+
accessibility: w.rScore,
|
|
6486
|
+
bestPractices: w.bpScore,
|
|
6487
|
+
seo: w.seoScore
|
|
6488
|
+
};
|
|
6489
|
+
}
|
|
6490
|
+
function draftInputFor2(w, scores, now, period) {
|
|
6491
|
+
return {
|
|
6492
|
+
reportId: `${w.name} \u2014 Announcement \u2014 ${now.toISOString().slice(0, 10)}`,
|
|
6493
|
+
siteId: w.id,
|
|
6494
|
+
reportType: "Announcement",
|
|
6495
|
+
period,
|
|
6496
|
+
periodStart: now,
|
|
6497
|
+
periodEnd: now,
|
|
6498
|
+
completedOn: now,
|
|
6499
|
+
lighthouse: scores,
|
|
6500
|
+
lastTestedDate: null,
|
|
6501
|
+
subjectOverride: `Your testing & maintenance schedule for ${w.name}`
|
|
6502
|
+
};
|
|
6503
|
+
}
|
|
6504
|
+
|
|
6505
|
+
// src/cli/commands/announce.ts
|
|
6506
|
+
function formatSiteResult(r) {
|
|
6507
|
+
if (r.status === "skipped-no-scores") return `[${r.site}] skipped-no-scores`;
|
|
6508
|
+
if (r.status === "error") return `[${r.site}] error: ${r.message}`;
|
|
6509
|
+
const note = r.recipientMissing ? " \u26A0 recipient missing" : "";
|
|
6510
|
+
return `[${r.site}] ${r.status}${note}`;
|
|
6511
|
+
}
|
|
6512
|
+
function formatAnnounceResult(result) {
|
|
6513
|
+
if (result.results.length === 0) return "No maintenance sites to announce.";
|
|
6514
|
+
return result.results.map(formatSiteResult).join("\n");
|
|
6515
|
+
}
|
|
6516
|
+
async function runAnnounceCommand(site, _opts) {
|
|
6517
|
+
const result = await announce(site ? { site } : {});
|
|
6518
|
+
const hadError = result.results.some((r) => r.status === "error");
|
|
6519
|
+
return { output: formatAnnounceResult(result), code: hadError ? 1 : 0 };
|
|
6520
|
+
}
|
|
6521
|
+
|
|
6214
6522
|
// src/cli/commands/github-signals.ts
|
|
6215
6523
|
init_client();
|
|
6216
6524
|
init_websites();
|
|
@@ -6464,6 +6772,12 @@ cli.command(
|
|
|
6464
6772
|
).action(
|
|
6465
6773
|
async (site, opts) => runOrExit(() => runLaunchCommand(site, opts), opts)
|
|
6466
6774
|
);
|
|
6775
|
+
cli.command(
|
|
6776
|
+
"announce [site]",
|
|
6777
|
+
"Draft the monthly-report announcement email for maintenance sites (all, or one) for approval."
|
|
6778
|
+
).action(
|
|
6779
|
+
async (site, opts) => runOrExit(() => runAnnounceCommand(site, opts), opts)
|
|
6780
|
+
);
|
|
6467
6781
|
cli.command("report [site]", "Draft or send maintenance/testing reports.").option("--due", "Scan all Websites and draft overdue reports.").option(
|
|
6468
6782
|
"--preview",
|
|
6469
6783
|
"Single-site dry run; writes reports/<slug>/draft.html, never touches Airtable."
|