@reddoorla/maintenance 0.40.0 → 0.42.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 +282 -16
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +24 -0
- 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 +35 -1
- package/dist/index.js +133 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/bin.js
CHANGED
|
@@ -118,6 +118,7 @@ __export(websites_exports, {
|
|
|
118
118
|
isDashboardVisible: () => isDashboardVisible,
|
|
119
119
|
listWebsites: () => listWebsites,
|
|
120
120
|
mapRow: () => mapRow,
|
|
121
|
+
parseNotifyRouting: () => parseNotifyRouting,
|
|
121
122
|
siteSlug: () => siteSlug,
|
|
122
123
|
updateA11yCounts: () => updateA11yCounts,
|
|
123
124
|
updateAuditFields: () => updateAuditFields,
|
|
@@ -135,6 +136,28 @@ function trimToNull(raw) {
|
|
|
135
136
|
const trimmed = raw.trim();
|
|
136
137
|
return trimmed.length > 0 ? trimmed : null;
|
|
137
138
|
}
|
|
139
|
+
function parseNotifyRouting(raw) {
|
|
140
|
+
if (typeof raw !== "string") return null;
|
|
141
|
+
const trimmed = raw.trim();
|
|
142
|
+
if (!trimmed) return null;
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = JSON.parse(trimmed);
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
150
|
+
const o = parsed;
|
|
151
|
+
if (typeof o.field !== "string" || !o.field.trim()) return null;
|
|
152
|
+
if (!o.routes || typeof o.routes !== "object" || Array.isArray(o.routes)) return null;
|
|
153
|
+
const routing = {
|
|
154
|
+
field: o.field,
|
|
155
|
+
routes: o.routes
|
|
156
|
+
};
|
|
157
|
+
if (o.default !== void 0) routing.default = o.default;
|
|
158
|
+
if (Array.isArray(o.cc)) routing.cc = o.cc.filter((x) => typeof x === "string");
|
|
159
|
+
return routing;
|
|
160
|
+
}
|
|
138
161
|
function isDashboardVisible(site) {
|
|
139
162
|
return site.status !== null && ACTIVE_STATUSES.has(site.status);
|
|
140
163
|
}
|
|
@@ -177,6 +200,7 @@ function mapRow(rec) {
|
|
|
177
200
|
copyFooter: trimToNull(f["Copy \u2014 Footer"]),
|
|
178
201
|
launchedAt: f["Launched at"] ?? null,
|
|
179
202
|
newsletterWebhook: trimToNull(f["Newsletter Webhook"]),
|
|
203
|
+
notifyRouting: parseNotifyRouting(f["Notify Routing"]),
|
|
180
204
|
mailchimpApiKey: trimToNull(f["Mailchimp API Key"]),
|
|
181
205
|
mailchimpAudienceId: trimToNull(f["Mailchimp Audience ID"]),
|
|
182
206
|
renovateFailingCis: f["Renovate Failing CIs"] ?? null,
|
|
@@ -627,6 +651,7 @@ async function createDraft(base, input) {
|
|
|
627
651
|
if (input.searchFoundPage1 !== void 0) fields["Search found page 1"] = input.searchFoundPage1;
|
|
628
652
|
if (input.searchPosition !== void 0) fields["Search position"] = input.searchPosition;
|
|
629
653
|
if (input.period !== void 0) fields["Period"] = input.period;
|
|
654
|
+
if (input.subjectOverride !== void 0) fields["Subject override"] = input.subjectOverride;
|
|
630
655
|
const created = await base(REPORTS_TABLE).create([{ fields }]);
|
|
631
656
|
const rec = created[0];
|
|
632
657
|
if (!rec) throw new Error("Airtable create returned no records");
|
|
@@ -693,7 +718,7 @@ var init_reports = __esm({
|
|
|
693
718
|
"src/reports/airtable/reports.ts"() {
|
|
694
719
|
"use strict";
|
|
695
720
|
REPORTS_TABLE = "Reports";
|
|
696
|
-
REPORT_TYPES = ["Maintenance", "Testing", "Launch"];
|
|
721
|
+
REPORT_TYPES = ["Maintenance", "Testing", "Launch", "Announcement"];
|
|
697
722
|
}
|
|
698
723
|
});
|
|
699
724
|
|
|
@@ -753,7 +778,15 @@ var init_copy = __esm({
|
|
|
753
778
|
"Hosting, DNS, and SSL configured",
|
|
754
779
|
"Continuous integration + automatic dependency updates",
|
|
755
780
|
"Analytics and uptime monitoring"
|
|
756
|
-
]
|
|
781
|
+
],
|
|
782
|
+
announceHeading: "YOUR MONTHLY REPORT",
|
|
783
|
+
announceBody: "We've set up ongoing monitoring and maintenance for your site. Each month we quietly check that everything's healthy and up to date \u2014 and now you'll get a short report so you can see it at a glance.",
|
|
784
|
+
announceMonitorItems: ["Performance", "Accessibility", "Security", "Uptime"],
|
|
785
|
+
announcePreviewLabel: "A snapshot of your latest scores:",
|
|
786
|
+
announceImprovementResend: "Your contact forms now deliver straight to your inbox through reliable infrastructure, so no inquiry slips through the cracks.",
|
|
787
|
+
announceImprovementSvelte5: "We've modernized your site to the latest framework \u2014 it's faster, more secure, and built to last.",
|
|
788
|
+
announceCadence: "You'll receive this every month. There's nothing you need to do.",
|
|
789
|
+
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."
|
|
757
790
|
};
|
|
758
791
|
}
|
|
759
792
|
});
|
|
@@ -1090,10 +1123,115 @@ var init_template2 = __esm({
|
|
|
1090
1123
|
}
|
|
1091
1124
|
});
|
|
1092
1125
|
|
|
1126
|
+
// src/reports/announcement-email/template.ts
|
|
1127
|
+
function buildAnnouncementMjml(data) {
|
|
1128
|
+
const copy = data.copy ?? DEFAULT_COPY;
|
|
1129
|
+
const previewText = "Your monthly report from Reddoor";
|
|
1130
|
+
const improvementItems = [];
|
|
1131
|
+
if (data.improvements?.resendForms) improvementItems.push(copy.announceImprovementResend);
|
|
1132
|
+
if (data.improvements?.svelte5) improvementItems.push(copy.announceImprovementSvelte5);
|
|
1133
|
+
const improvementsSection = improvementItems.length > 0 ? `
|
|
1134
|
+
<mj-section background-color="white">
|
|
1135
|
+
<mj-column>
|
|
1136
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">RECENT IMPROVEMENTS</mj-text>
|
|
1137
|
+
${improvementItems.map(
|
|
1138
|
+
(item) => `
|
|
1139
|
+
<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>`
|
|
1140
|
+
).join("")}
|
|
1141
|
+
</mj-column>
|
|
1142
|
+
</mj-section>` : "";
|
|
1143
|
+
const monitorRows = copy.announceMonitorItems.map(
|
|
1144
|
+
(item) => `
|
|
1145
|
+
<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>`
|
|
1146
|
+
).join("");
|
|
1147
|
+
const scoreRows = SCORE_PREVIEW.map(
|
|
1148
|
+
({ label, key }) => `
|
|
1149
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="300" padding-top="25px">${label}</mj-text>
|
|
1150
|
+
<mj-text color="${RED2}" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse[key]}</mj-text>`
|
|
1151
|
+
).join("");
|
|
1152
|
+
const contactRows = copy.contact.map(
|
|
1153
|
+
(line) => `
|
|
1154
|
+
<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
|
|
1155
|
+
).join("");
|
|
1156
|
+
const footerAddressRows = copy.footerAddress.map(
|
|
1157
|
+
(line) => `
|
|
1158
|
+
<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>`
|
|
1159
|
+
).join("");
|
|
1160
|
+
return `<mjml>
|
|
1161
|
+
<mj-head>
|
|
1162
|
+
<mj-attributes>
|
|
1163
|
+
<mj-text font-family="helvetica, sans-serif" padding-left="5px" padding-right="5px" />
|
|
1164
|
+
<mj-section padding-left="11%" padding-right="11%"/>
|
|
1165
|
+
<mj-image padding="0px" />
|
|
1166
|
+
</mj-attributes>
|
|
1167
|
+
<mj-preview>${escapeXml(previewText)}</mj-preview>
|
|
1168
|
+
${headerStyleBlock(data)}
|
|
1169
|
+
</mj-head>
|
|
1170
|
+
<mj-body background-color="white">
|
|
1171
|
+
<mj-section background-color="#F4F4F4" padding-top="0px" padding-bottom="0px" padding-left="0px" padding-right="0px">
|
|
1172
|
+
<mj-column>${headerImageTag(data)}</mj-column>
|
|
1173
|
+
</mj-section>
|
|
1174
|
+
<mj-section background-color="white">
|
|
1175
|
+
<mj-column>
|
|
1176
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="75px">${escapeXml(copy.announceHeading)}</mj-text>
|
|
1177
|
+
<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>
|
|
1178
|
+
<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>
|
|
1179
|
+
</mj-column>
|
|
1180
|
+
</mj-section>
|
|
1181
|
+
${improvementsSection}
|
|
1182
|
+
<mj-section background-color="white">
|
|
1183
|
+
<mj-column>
|
|
1184
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">WHAT WE MONITOR</mj-text>
|
|
1185
|
+
${monitorRows}
|
|
1186
|
+
</mj-column>
|
|
1187
|
+
</mj-section>
|
|
1188
|
+
<mj-section background-color="#F4F4F4">
|
|
1189
|
+
<mj-column>
|
|
1190
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="55px">${escapeXml(copy.announcePreviewLabel)}</mj-text>
|
|
1191
|
+
${scoreRows}
|
|
1192
|
+
</mj-column>
|
|
1193
|
+
</mj-section>
|
|
1194
|
+
<mj-section background-color="white">
|
|
1195
|
+
<mj-column>
|
|
1196
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="36px">${escapeXml(copy.announceCadence)}</mj-text>
|
|
1197
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="8px">${escapeXml(copy.announceOpenDoor)}</mj-text>
|
|
1198
|
+
</mj-column>
|
|
1199
|
+
</mj-section>
|
|
1200
|
+
<mj-section background-color="white">
|
|
1201
|
+
<mj-column padding-top="36px">
|
|
1202
|
+
<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>
|
|
1203
|
+
${contactRows}
|
|
1204
|
+
<mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
|
|
1205
|
+
<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>
|
|
1206
|
+
<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>
|
|
1207
|
+
<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>
|
|
1208
|
+
${footerAddressRows}
|
|
1209
|
+
</mj-column>
|
|
1210
|
+
</mj-section>
|
|
1211
|
+
</mj-body>
|
|
1212
|
+
</mjml>`;
|
|
1213
|
+
}
|
|
1214
|
+
var RED2, GREY2, SCORE_PREVIEW;
|
|
1215
|
+
var init_template3 = __esm({
|
|
1216
|
+
"src/reports/announcement-email/template.ts"() {
|
|
1217
|
+
"use strict";
|
|
1218
|
+
init_copy();
|
|
1219
|
+
init_template();
|
|
1220
|
+
RED2 = "#C00";
|
|
1221
|
+
GREY2 = "#757575";
|
|
1222
|
+
SCORE_PREVIEW = [
|
|
1223
|
+
{ label: "Performance", key: "performance" },
|
|
1224
|
+
{ label: "Readability", key: "accessibility" },
|
|
1225
|
+
{ label: "Best Practices", key: "bestPractices" },
|
|
1226
|
+
{ label: "Site Structure", key: "seo" }
|
|
1227
|
+
];
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1093
1231
|
// src/reports/render.ts
|
|
1094
1232
|
import mjml2html from "mjml";
|
|
1095
1233
|
async function renderReportHtml(data) {
|
|
1096
|
-
const mjml = data.reportType === "Launch" ? buildLaunchMjml(data) : buildMjml(data);
|
|
1234
|
+
const mjml = data.reportType === "Launch" ? buildLaunchMjml(data) : data.reportType === "Announcement" ? buildAnnouncementMjml(data) : buildMjml(data);
|
|
1097
1235
|
const out = await mjml2html(mjml, { validationLevel: "strict" });
|
|
1098
1236
|
return { html: out.html, warnings: out.errors ?? [] };
|
|
1099
1237
|
}
|
|
@@ -1102,6 +1240,7 @@ var init_render = __esm({
|
|
|
1102
1240
|
"use strict";
|
|
1103
1241
|
init_template();
|
|
1104
1242
|
init_template2();
|
|
1243
|
+
init_template3();
|
|
1105
1244
|
}
|
|
1106
1245
|
});
|
|
1107
1246
|
|
|
@@ -1395,16 +1534,16 @@ __export(digest_exports, {
|
|
|
1395
1534
|
runDigest: () => runDigest
|
|
1396
1535
|
});
|
|
1397
1536
|
function readySection(items) {
|
|
1398
|
-
const heading = `<h2 style="color:${
|
|
1537
|
+
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>`;
|
|
1399
1538
|
if (items.length === 0) {
|
|
1400
|
-
return `${heading}<p style="color:${
|
|
1539
|
+
return `${heading}<p style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;margin:0">Nothing waiting on you.</p>`;
|
|
1401
1540
|
}
|
|
1402
1541
|
const rows = items.map((it) => {
|
|
1403
1542
|
const safeUrl = it.dashboardUrl.startsWith("https://") ? it.dashboardUrl : void 0;
|
|
1404
1543
|
const link = safeUrl ? `<a href="${escapeHtml(safeUrl)}" style="${ANCHOR_STYLE}">review & approve</a>` : `review & approve`;
|
|
1405
1544
|
return `
|
|
1406
1545
|
<tr>
|
|
1407
|
-
<td style="color:${
|
|
1546
|
+
<td style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px">
|
|
1408
1547
|
<strong style="color:#222">${escapeHtml(it.siteName)}</strong> \u2014 ${escapeHtml(it.reportType)} (${escapeHtml(it.period)})
|
|
1409
1548
|
\u2014 ${link}
|
|
1410
1549
|
</td>
|
|
@@ -1414,15 +1553,15 @@ function readySection(items) {
|
|
|
1414
1553
|
}
|
|
1415
1554
|
function attentionBadge(status) {
|
|
1416
1555
|
if (status === "new")
|
|
1417
|
-
return `<strong style="color:${
|
|
1556
|
+
return `<strong style="color:${RED3};font-family:helvetica,sans-serif">NEW</strong> `;
|
|
1418
1557
|
if (status === "worse")
|
|
1419
|
-
return `<strong style="color:${
|
|
1558
|
+
return `<strong style="color:${RED3};font-family:helvetica,sans-serif">WORSE</strong> `;
|
|
1420
1559
|
return "";
|
|
1421
1560
|
}
|
|
1422
1561
|
function attentionSection(items) {
|
|
1423
|
-
const heading = `<h2 style="color:${
|
|
1562
|
+
const heading = `<h2 style="color:${RED3};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px">Needs attention</h2>`;
|
|
1424
1563
|
if (items.length === 0) {
|
|
1425
|
-
return `${heading}<p style="color:${
|
|
1564
|
+
return `${heading}<p style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;margin:0">All clear \u2014 nothing needs attention.</p>`;
|
|
1426
1565
|
}
|
|
1427
1566
|
const bySite = /* @__PURE__ */ new Map();
|
|
1428
1567
|
for (const it of items) {
|
|
@@ -1439,7 +1578,7 @@ function attentionSection(items) {
|
|
|
1439
1578
|
const titleHtml = safeUrl ? `<a href="${escapeHtml(safeUrl)}" style="${ANCHOR_STYLE}">${escapeHtml(it.title)}</a>` : escapeHtml(it.title);
|
|
1440
1579
|
return `
|
|
1441
1580
|
<tr>
|
|
1442
|
-
<td style="color:${
|
|
1581
|
+
<td style="color:${GREY3};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px">${attentionBadge(it.status)}${titleHtml}</td>
|
|
1443
1582
|
</tr>`;
|
|
1444
1583
|
}).join("");
|
|
1445
1584
|
return `
|
|
@@ -1564,7 +1703,7 @@ function renderDigestHtml(sections) {
|
|
|
1564
1703
|
<table width="600" style="border-collapse:collapse">
|
|
1565
1704
|
<tr>
|
|
1566
1705
|
<td>
|
|
1567
|
-
<h1 style="color:${
|
|
1706
|
+
<h1 style="color:${RED3};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px">Your fleet today</h1>
|
|
1568
1707
|
${readySection(sections.readyForYourYes)}
|
|
1569
1708
|
${attentionSection(sections.needsAttention)}
|
|
1570
1709
|
</td>
|
|
@@ -1576,7 +1715,7 @@ function renderDigestHtml(sections) {
|
|
|
1576
1715
|
</body>
|
|
1577
1716
|
</html>`;
|
|
1578
1717
|
}
|
|
1579
|
-
var
|
|
1718
|
+
var GREY3, RED3, ANCHOR_STYLE, SEVERITY_ORDER, FROM_ADDRESS, DIGEST_OPERATOR_FALLBACK;
|
|
1580
1719
|
var init_digest = __esm({
|
|
1581
1720
|
"src/reports/digest.ts"() {
|
|
1582
1721
|
"use strict";
|
|
@@ -1588,9 +1727,9 @@ var init_digest = __esm({
|
|
|
1588
1727
|
init_digest_collectors();
|
|
1589
1728
|
init_digest_state();
|
|
1590
1729
|
init_html();
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
ANCHOR_STYLE = `color:${
|
|
1730
|
+
GREY3 = "#757575";
|
|
1731
|
+
RED3 = "#C00";
|
|
1732
|
+
ANCHOR_STYLE = `color:${RED3};font-family:helvetica,sans-serif`;
|
|
1594
1733
|
SEVERITY_ORDER = { critical: 0, warning: 1 };
|
|
1595
1734
|
FROM_ADDRESS = "Reddoor Reports <reports@reddoorla.com>";
|
|
1596
1735
|
DIGEST_OPERATOR_FALLBACK = "info@reddoorla.com";
|
|
@@ -6187,6 +6326,127 @@ async function runLaunchCommand(site, opts) {
|
|
|
6187
6326
|
return { output: formatResult9(result), code: result.complete ? 0 : 1 };
|
|
6188
6327
|
}
|
|
6189
6328
|
|
|
6329
|
+
// src/recipes/announce.ts
|
|
6330
|
+
init_client();
|
|
6331
|
+
init_websites();
|
|
6332
|
+
init_reports();
|
|
6333
|
+
init_attachments();
|
|
6334
|
+
init_render();
|
|
6335
|
+
init_copy();
|
|
6336
|
+
async function announce(deps) {
|
|
6337
|
+
const base = deps?.base ?? openBase(readAirtableConfig());
|
|
6338
|
+
const now = deps?.now ?? /* @__PURE__ */ new Date();
|
|
6339
|
+
const websites = await listWebsites(base);
|
|
6340
|
+
let targets = websites.filter((w) => w.status === "maintenance");
|
|
6341
|
+
if (deps?.site) {
|
|
6342
|
+
const wanted = siteSlug(deps.site);
|
|
6343
|
+
targets = targets.filter((w) => siteSlug(w.name) === wanted);
|
|
6344
|
+
}
|
|
6345
|
+
const period = now.toISOString().slice(0, 7);
|
|
6346
|
+
const results = [];
|
|
6347
|
+
for (const w of targets) {
|
|
6348
|
+
try {
|
|
6349
|
+
const scores = scoresFromRow(w);
|
|
6350
|
+
if (scores === null) {
|
|
6351
|
+
results.push({ site: w.name, status: "skipped-no-scores" });
|
|
6352
|
+
continue;
|
|
6353
|
+
}
|
|
6354
|
+
let report;
|
|
6355
|
+
let statusKind;
|
|
6356
|
+
const existing = await findReportByPeriod(base, w.id, "Announcement", period);
|
|
6357
|
+
if (existing) {
|
|
6358
|
+
await updateReportScores(base, existing.id, scores, now);
|
|
6359
|
+
report = existing;
|
|
6360
|
+
statusKind = "reused";
|
|
6361
|
+
} else {
|
|
6362
|
+
report = await createDraft(base, draftInputFor2(w, scores, now, period));
|
|
6363
|
+
statusKind = "drafted";
|
|
6364
|
+
}
|
|
6365
|
+
const slug = siteSlug(w.name);
|
|
6366
|
+
const { html } = await renderReportHtml({
|
|
6367
|
+
siteName: w.name,
|
|
6368
|
+
siteUrl: w.url,
|
|
6369
|
+
reportType: "Announcement",
|
|
6370
|
+
completedOn: now,
|
|
6371
|
+
lighthouse: scores,
|
|
6372
|
+
lastTestedDate: null,
|
|
6373
|
+
commentary: null,
|
|
6374
|
+
copy: resolveCopy(w),
|
|
6375
|
+
headerImageCid: `${slug}-header`,
|
|
6376
|
+
// Default-on fleet-wide for v1: both recent-improvement callouts render for every
|
|
6377
|
+
// site. Operator review (the draft never auto-sends) is the relevance backstop;
|
|
6378
|
+
// per-site conditioning of these flags is a future lever, not a v1 requirement.
|
|
6379
|
+
improvements: { resendForms: true, svelte5: true }
|
|
6380
|
+
});
|
|
6381
|
+
try {
|
|
6382
|
+
await uploadAttachment(
|
|
6383
|
+
report.id,
|
|
6384
|
+
"Rendered HTML",
|
|
6385
|
+
html,
|
|
6386
|
+
`${slug}-${now.toISOString().slice(0, 10)}.html`,
|
|
6387
|
+
"text/html"
|
|
6388
|
+
);
|
|
6389
|
+
} catch (uploadErr) {
|
|
6390
|
+
console.warn(
|
|
6391
|
+
`\u26A0 Announcement preview upload skipped for ${w.name}: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
|
|
6392
|
+
);
|
|
6393
|
+
}
|
|
6394
|
+
await setDraftReady(base, report.id, true);
|
|
6395
|
+
const recipientMissing = !(w.reportRecipientsTo && w.reportRecipientsTo.trim());
|
|
6396
|
+
results.push({ site: w.name, status: statusKind, reportId: report.id, recipientMissing });
|
|
6397
|
+
} catch (err) {
|
|
6398
|
+
results.push({
|
|
6399
|
+
site: w.name,
|
|
6400
|
+
status: "error",
|
|
6401
|
+
message: err instanceof Error ? err.message : String(err)
|
|
6402
|
+
});
|
|
6403
|
+
}
|
|
6404
|
+
}
|
|
6405
|
+
return { results };
|
|
6406
|
+
}
|
|
6407
|
+
function scoresFromRow(w) {
|
|
6408
|
+
if (w.pScore === null || w.rScore === null || w.bpScore === null || w.seoScore === null) {
|
|
6409
|
+
return null;
|
|
6410
|
+
}
|
|
6411
|
+
return {
|
|
6412
|
+
performance: w.pScore,
|
|
6413
|
+
accessibility: w.rScore,
|
|
6414
|
+
bestPractices: w.bpScore,
|
|
6415
|
+
seo: w.seoScore
|
|
6416
|
+
};
|
|
6417
|
+
}
|
|
6418
|
+
function draftInputFor2(w, scores, now, period) {
|
|
6419
|
+
return {
|
|
6420
|
+
reportId: `${w.name} \u2014 Announcement \u2014 ${now.toISOString().slice(0, 10)}`,
|
|
6421
|
+
siteId: w.id,
|
|
6422
|
+
reportType: "Announcement",
|
|
6423
|
+
period,
|
|
6424
|
+
periodStart: now,
|
|
6425
|
+
periodEnd: now,
|
|
6426
|
+
completedOn: now,
|
|
6427
|
+
lighthouse: scores,
|
|
6428
|
+
lastTestedDate: null,
|
|
6429
|
+
subjectOverride: `Your new monthly report for ${w.name}`
|
|
6430
|
+
};
|
|
6431
|
+
}
|
|
6432
|
+
|
|
6433
|
+
// src/cli/commands/announce.ts
|
|
6434
|
+
function formatSiteResult(r) {
|
|
6435
|
+
if (r.status === "skipped-no-scores") return `[${r.site}] skipped-no-scores`;
|
|
6436
|
+
if (r.status === "error") return `[${r.site}] error: ${r.message}`;
|
|
6437
|
+
const note = r.recipientMissing ? " \u26A0 recipient missing" : "";
|
|
6438
|
+
return `[${r.site}] ${r.status}${note}`;
|
|
6439
|
+
}
|
|
6440
|
+
function formatAnnounceResult(result) {
|
|
6441
|
+
if (result.results.length === 0) return "No maintenance sites to announce.";
|
|
6442
|
+
return result.results.map(formatSiteResult).join("\n");
|
|
6443
|
+
}
|
|
6444
|
+
async function runAnnounceCommand(site, _opts) {
|
|
6445
|
+
const result = await announce(site ? { site } : {});
|
|
6446
|
+
const hadError = result.results.some((r) => r.status === "error");
|
|
6447
|
+
return { output: formatAnnounceResult(result), code: hadError ? 1 : 0 };
|
|
6448
|
+
}
|
|
6449
|
+
|
|
6190
6450
|
// src/cli/commands/github-signals.ts
|
|
6191
6451
|
init_client();
|
|
6192
6452
|
init_websites();
|
|
@@ -6440,6 +6700,12 @@ cli.command(
|
|
|
6440
6700
|
).action(
|
|
6441
6701
|
async (site, opts) => runOrExit(() => runLaunchCommand(site, opts), opts)
|
|
6442
6702
|
);
|
|
6703
|
+
cli.command(
|
|
6704
|
+
"announce [site]",
|
|
6705
|
+
"Draft the monthly-report announcement email for maintenance sites (all, or one) for approval."
|
|
6706
|
+
).action(
|
|
6707
|
+
async (site, opts) => runOrExit(() => runAnnounceCommand(site, opts), opts)
|
|
6708
|
+
);
|
|
6443
6709
|
cli.command("report [site]", "Draft or send maintenance/testing reports.").option("--due", "Scan all Websites and draft overdue reports.").option(
|
|
6444
6710
|
"--preview",
|
|
6445
6711
|
"Single-site dry run; writes reports/<slug>/draft.html, never touches Airtable."
|