@reddoorla/maintenance 0.7.0 → 0.8.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 +7 -1
- package/dist/cli/bin.js +327 -96
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +6 -0
- package/dist/cli/commands/audit.js +223 -5
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +35 -9
- package/dist/index.js +156 -79
- package/dist/index.js.map +1 -1
- package/dist/reports/maintenance-email/assets/blurredTests.jpg +0 -0
- package/dist/reports/maintenance-email/assets/check.png +0 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -92,6 +92,35 @@ declare function localPath(path: string, opts?: LocalPathOptions): InventoryProv
|
|
|
92
92
|
|
|
93
93
|
declare function fromJsonFile(path: string): InventoryProvider;
|
|
94
94
|
|
|
95
|
+
type AirtableConfig = {
|
|
96
|
+
apiKey: string;
|
|
97
|
+
baseId: string;
|
|
98
|
+
};
|
|
99
|
+
type AirtableBase = ReturnType<typeof openBase>;
|
|
100
|
+
declare function openBase(cfg: AirtableConfig): airtable_lib_airtable_base_js.AirtableBase;
|
|
101
|
+
|
|
102
|
+
type AirtableInventoryOptions = {
|
|
103
|
+
/**
|
|
104
|
+
* Local workdir to compute each site's path as `{workdir}/{slug}`.
|
|
105
|
+
* Defaults to REDDOOR_FLEET_WORKDIR env var if not provided.
|
|
106
|
+
* Airtable doesn't store local checkout paths, so this is required.
|
|
107
|
+
*/
|
|
108
|
+
workdir?: string;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Read sites from the Airtable Websites table as an InventoryProvider.
|
|
112
|
+
* Each row becomes one Site; `path` is computed as `{workdir}/{slug}`.
|
|
113
|
+
* Sites where BOTH maintenance freq AND testing freq are "None" are excluded
|
|
114
|
+
* (they're inactive — no scheduled audits or reports).
|
|
115
|
+
*
|
|
116
|
+
* Note: `repoUrl` is set to the production URL (Websites.url). For sites
|
|
117
|
+
* cloned via `--workdir` semantics this is wrong — the convention should be
|
|
118
|
+
* tightened (e.g. add a `repo` field to Websites) when fleet-clone-from-
|
|
119
|
+
* airtable becomes a real flow. For local audits where the site is already
|
|
120
|
+
* checked out at `path`, the `repoUrl` is unused.
|
|
121
|
+
*/
|
|
122
|
+
declare function fromAirtableBase(base: AirtableBase, opts?: AirtableInventoryOptions): InventoryProvider;
|
|
123
|
+
|
|
95
124
|
type ReportType = "Maintenance" | "Testing";
|
|
96
125
|
type LighthouseScores = {
|
|
97
126
|
performance: number;
|
|
@@ -124,18 +153,13 @@ type ReportData = {
|
|
|
124
153
|
headerImageCid: string;
|
|
125
154
|
};
|
|
126
155
|
|
|
127
|
-
type AirtableConfig = {
|
|
128
|
-
apiKey: string;
|
|
129
|
-
baseId: string;
|
|
130
|
-
};
|
|
131
|
-
type AirtableBase = ReturnType<typeof openBase>;
|
|
132
|
-
declare function openBase(cfg: AirtableConfig): airtable_lib_airtable_base_js.AirtableBase;
|
|
133
|
-
|
|
134
156
|
type Frequency = "None" | "Monthly" | "Quarterly" | "Yearly";
|
|
157
|
+
type Status = "in development" | "launch period" | "maintenance" | "hosting" | "probably not our problem" | "deprecated";
|
|
135
158
|
type WebsiteRow = {
|
|
136
159
|
id: string;
|
|
137
160
|
name: string;
|
|
138
161
|
url: string;
|
|
162
|
+
status: Status | null;
|
|
139
163
|
pointOfContact: string | null;
|
|
140
164
|
maintenanceFreq: Frequency;
|
|
141
165
|
testingFreq: Frequency;
|
|
@@ -151,11 +175,13 @@ type WebsiteRow = {
|
|
|
151
175
|
filename: string;
|
|
152
176
|
type: string;
|
|
153
177
|
} | null;
|
|
154
|
-
/** Lighthouse "current state" snapshot,
|
|
178
|
+
/** Lighthouse "current state" snapshot, kept fresh by `audit lighthouse --write-airtable`. */
|
|
155
179
|
pScore: number | null;
|
|
156
180
|
rScore: number | null;
|
|
157
181
|
bpScore: number | null;
|
|
158
182
|
seoScore: number | null;
|
|
183
|
+
/** ISO timestamp set by `audit lighthouse --write-airtable` when scores were last refreshed. */
|
|
184
|
+
lastLighthouseAuditAt: string | null;
|
|
159
185
|
};
|
|
160
186
|
|
|
161
187
|
type DeliveryStatus = "pending" | "delivered" | "bounced" | "complained";
|
|
@@ -268,4 +294,4 @@ type DueItem = {
|
|
|
268
294
|
*/
|
|
269
295
|
declare function findDueReports(websites: WebsiteRow[], reports: ReportRow[], today: Date): DueItem[];
|
|
270
296
|
|
|
271
|
-
export { ALL_AUDIT_NAMES, ALL_RECIPE_NAMES, AuditName, AuditResult, type BumpDepsOptions, type ConvertToPnpmOptions, type DraftOptions, type DraftResult, type DueItem, type HeaderImage, InventoryProvider, type LighthouseScores, type LocalPathOptions, type OnboardAudit, type OnboardOptions, type OrchestrateOptions, RecipeName, RecipeResult, type RenderResult, type ReportData, type ReportType, Site, type UpgradeSvelte4to5Options, a11yAudit, bumpDeps, convertToPnpm, depsAudit, draftReportForSite, findDueReports, fromJsonFile, isRecipeName, lighthouseAudit, lintAudit, localPath, onboard, renderReportHtml, runAudits, runAuditsAcross, securityAudit, sendApprovedReports, svelteCodemods, upgradeSvelte4to5 };
|
|
297
|
+
export { ALL_AUDIT_NAMES, ALL_RECIPE_NAMES, type AirtableInventoryOptions, AuditName, AuditResult, type BumpDepsOptions, type ConvertToPnpmOptions, type DraftOptions, type DraftResult, type DueItem, type HeaderImage, InventoryProvider, type LighthouseScores, type LocalPathOptions, type OnboardAudit, type OnboardOptions, type OrchestrateOptions, RecipeName, RecipeResult, type RenderResult, type ReportData, type ReportType, Site, type UpgradeSvelte4to5Options, a11yAudit, bumpDeps, convertToPnpm, depsAudit, draftReportForSite, findDueReports, fromAirtableBase, fromJsonFile, isRecipeName, lighthouseAudit, lintAudit, localPath, onboard, renderReportHtml, runAudits, runAuditsAcross, securityAudit, sendApprovedReports, svelteCodemods, upgradeSvelte4to5 };
|
package/dist/index.js
CHANGED
|
@@ -1769,8 +1769,8 @@ import { fileURLToPath } from "url";
|
|
|
1769
1769
|
import { dirname, join as join15 } from "path";
|
|
1770
1770
|
function selfPackageVersion(callerImportMetaUrl) {
|
|
1771
1771
|
try {
|
|
1772
|
-
const
|
|
1773
|
-
const raw = readFileSync(join15(
|
|
1772
|
+
const here2 = dirname(fileURLToPath(callerImportMetaUrl));
|
|
1773
|
+
const raw = readFileSync(join15(here2, "..", "..", "package.json"), "utf-8");
|
|
1774
1774
|
const pkg = JSON.parse(raw);
|
|
1775
1775
|
return pkg.version ?? "0.0.0";
|
|
1776
1776
|
} catch {
|
|
@@ -1924,16 +1924,106 @@ function fromJsonFile(path) {
|
|
|
1924
1924
|
};
|
|
1925
1925
|
}
|
|
1926
1926
|
|
|
1927
|
+
// src/reports/airtable/websites.ts
|
|
1928
|
+
var WEBSITES_TABLE = "Websites";
|
|
1929
|
+
function siteSlug(name) {
|
|
1930
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1931
|
+
}
|
|
1932
|
+
function mapRow(rec) {
|
|
1933
|
+
const f = rec.fields;
|
|
1934
|
+
const attachments = f["Header image"] ?? [];
|
|
1935
|
+
const header = attachments[0] ?? null;
|
|
1936
|
+
return {
|
|
1937
|
+
id: rec.id,
|
|
1938
|
+
name: String(f["Name"] ?? ""),
|
|
1939
|
+
url: String(f["url"] ?? ""),
|
|
1940
|
+
status: f["Status"] ?? null,
|
|
1941
|
+
pointOfContact: f["point of contact"] ?? null,
|
|
1942
|
+
maintenanceFreq: f["maintenence freq"] ?? "None",
|
|
1943
|
+
testingFreq: f["testing freq"] ?? "None",
|
|
1944
|
+
maintenanceDay: f["maintenance day"] ?? null,
|
|
1945
|
+
testingDay: f["testing day"] ?? null,
|
|
1946
|
+
ga4PropertyId: f["GA4 property ID"] ?? null,
|
|
1947
|
+
reportRecipientsTo: f["Report recipients (To)"] ?? null,
|
|
1948
|
+
reportRecipientsCc: f["Report recipients (CC)"] ?? null,
|
|
1949
|
+
headerImage: header,
|
|
1950
|
+
pScore: f["pScore"] ?? null,
|
|
1951
|
+
rScore: f["rScore"] ?? null,
|
|
1952
|
+
bpScore: f["bpScore"] ?? null,
|
|
1953
|
+
seoScore: f["seoScore"] ?? null,
|
|
1954
|
+
lastLighthouseAuditAt: f["Last lighthouse audit at"] ?? null
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
async function listWebsites(base) {
|
|
1958
|
+
const out = [];
|
|
1959
|
+
await base(WEBSITES_TABLE).select({ pageSize: 100 }).eachPage((records, fetchNextPage) => {
|
|
1960
|
+
for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));
|
|
1961
|
+
fetchNextPage();
|
|
1962
|
+
});
|
|
1963
|
+
return out;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// src/inventory/airtable.ts
|
|
1967
|
+
function fromAirtableBase(base, opts = {}) {
|
|
1968
|
+
return async () => {
|
|
1969
|
+
const workdir = opts.workdir ?? process.env.REDDOOR_FLEET_WORKDIR;
|
|
1970
|
+
if (!workdir) {
|
|
1971
|
+
throw new Error(
|
|
1972
|
+
"fromAirtableBase requires `workdir` option or REDDOOR_FLEET_WORKDIR env (sites need a local path)"
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
const websites = await listWebsites(base);
|
|
1976
|
+
return websites.filter((w) => w.maintenanceFreq !== "None" || w.testingFreq !== "None").map((w) => {
|
|
1977
|
+
const slug = siteSlug(w.name);
|
|
1978
|
+
const site = {
|
|
1979
|
+
path: `${workdir}/${slug}`,
|
|
1980
|
+
name: slug,
|
|
1981
|
+
meta: { airtableRowId: w.id, displayName: w.name }
|
|
1982
|
+
};
|
|
1983
|
+
if (w.url) site.repoUrl = w.url;
|
|
1984
|
+
return site;
|
|
1985
|
+
});
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1927
1989
|
// src/reports/draft.ts
|
|
1928
1990
|
import { mkdir, writeFile as writeFile9 } from "fs/promises";
|
|
1929
|
-
import { dirname as
|
|
1991
|
+
import { dirname as dirname3 } from "path";
|
|
1930
1992
|
|
|
1931
1993
|
// src/reports/render.ts
|
|
1932
1994
|
import mjml2html from "mjml";
|
|
1933
1995
|
|
|
1996
|
+
// src/reports/maintenance-email/assets/index.ts
|
|
1997
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1998
|
+
import { dirname as dirname2, join as join17 } from "path";
|
|
1999
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2000
|
+
var here = dirname2(fileURLToPath2(import.meta.url));
|
|
2001
|
+
var CHECK_CID = "rd-check-png";
|
|
2002
|
+
var BLURRED_CID = "rd-blurred-tests-jpg";
|
|
2003
|
+
async function loadBundledImages() {
|
|
2004
|
+
const [check, blurred] = await Promise.all([
|
|
2005
|
+
readFile10(join17(here, "check.png")),
|
|
2006
|
+
readFile10(join17(here, "blurredTests.jpg"))
|
|
2007
|
+
]);
|
|
2008
|
+
return {
|
|
2009
|
+
check: {
|
|
2010
|
+
bytes: new Uint8Array(check),
|
|
2011
|
+
contentType: "image/png",
|
|
2012
|
+
cid: CHECK_CID,
|
|
2013
|
+
filename: "check.png"
|
|
2014
|
+
},
|
|
2015
|
+
blurred: {
|
|
2016
|
+
bytes: new Uint8Array(blurred),
|
|
2017
|
+
contentType: "image/jpeg",
|
|
2018
|
+
cid: BLURRED_CID,
|
|
2019
|
+
filename: "blurredTests.jpg"
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1934
2024
|
// src/reports/maintenance-email/template.ts
|
|
1935
|
-
var CHECK_PNG =
|
|
1936
|
-
var BLURRED_TESTS =
|
|
2025
|
+
var CHECK_PNG = `cid:${CHECK_CID}`;
|
|
2026
|
+
var BLURRED_TESTS = `cid:${BLURRED_CID}`;
|
|
1937
2027
|
function fmtDate(d) {
|
|
1938
2028
|
if (!d) return "";
|
|
1939
2029
|
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
@@ -2104,43 +2194,6 @@ async function renderReportHtml(data) {
|
|
|
2104
2194
|
return { html: out.html, warnings: out.errors ?? [] };
|
|
2105
2195
|
}
|
|
2106
2196
|
|
|
2107
|
-
// src/reports/airtable/websites.ts
|
|
2108
|
-
var WEBSITES_TABLE = "Websites";
|
|
2109
|
-
function siteSlug(name) {
|
|
2110
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
2111
|
-
}
|
|
2112
|
-
function mapRow(rec) {
|
|
2113
|
-
const f = rec.fields;
|
|
2114
|
-
const attachments = f["Header image"] ?? [];
|
|
2115
|
-
const header = attachments[0] ?? null;
|
|
2116
|
-
return {
|
|
2117
|
-
id: rec.id,
|
|
2118
|
-
name: String(f["Name"] ?? ""),
|
|
2119
|
-
url: String(f["url"] ?? ""),
|
|
2120
|
-
pointOfContact: f["point of contact"] ?? null,
|
|
2121
|
-
maintenanceFreq: f["maintenence freq"] ?? "None",
|
|
2122
|
-
testingFreq: f["testing freq"] ?? "None",
|
|
2123
|
-
maintenanceDay: f["maintenance day"] ?? null,
|
|
2124
|
-
testingDay: f["testing day"] ?? null,
|
|
2125
|
-
ga4PropertyId: f["GA4 property ID"] ?? null,
|
|
2126
|
-
reportRecipientsTo: f["Report recipients (To)"] ?? null,
|
|
2127
|
-
reportRecipientsCc: f["Report recipients (CC)"] ?? null,
|
|
2128
|
-
headerImage: header,
|
|
2129
|
-
pScore: f["pScore"] ?? null,
|
|
2130
|
-
rScore: f["rScore"] ?? null,
|
|
2131
|
-
bpScore: f["bpScore"] ?? null,
|
|
2132
|
-
seoScore: f["seoScore"] ?? null
|
|
2133
|
-
};
|
|
2134
|
-
}
|
|
2135
|
-
async function listWebsites(base) {
|
|
2136
|
-
const out = [];
|
|
2137
|
-
await base(WEBSITES_TABLE).select({ pageSize: 100 }).eachPage((records, fetchNextPage) => {
|
|
2138
|
-
for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));
|
|
2139
|
-
fetchNextPage();
|
|
2140
|
-
});
|
|
2141
|
-
return out;
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
2197
|
// src/reports/airtable/reports.ts
|
|
2145
2198
|
var REPORTS_TABLE = "Reports";
|
|
2146
2199
|
function mapRow2(rec) {
|
|
@@ -2242,6 +2295,40 @@ async function stampSent(base, recordId, sentAt, messageId) {
|
|
|
2242
2295
|
]);
|
|
2243
2296
|
}
|
|
2244
2297
|
|
|
2298
|
+
// src/reports/airtable/attachments.ts
|
|
2299
|
+
async function fetchAttachmentBytes(url) {
|
|
2300
|
+
const res = await fetch(url);
|
|
2301
|
+
if (!res.ok) {
|
|
2302
|
+
throw new Error(
|
|
2303
|
+
`Failed to fetch Airtable attachment ${res.status} ${res.statusText} (url=${url})`
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
2307
|
+
const ab = await res.arrayBuffer();
|
|
2308
|
+
return { bytes: new Uint8Array(ab), contentType };
|
|
2309
|
+
}
|
|
2310
|
+
async function uploadAttachment(recordId, fieldName, body, filename, contentType) {
|
|
2311
|
+
const apiKey = process.env.AIRTABLE_PAT;
|
|
2312
|
+
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
2313
|
+
if (!apiKey || !baseId) {
|
|
2314
|
+
throw new Error("AIRTABLE_PAT and AIRTABLE_BASE_ID must be set");
|
|
2315
|
+
}
|
|
2316
|
+
const base64 = typeof body === "string" ? Buffer.from(body, "utf-8").toString("base64") : Buffer.from(body).toString("base64");
|
|
2317
|
+
const payload = { contentType, file: base64, filename };
|
|
2318
|
+
const url = `https://content.airtable.com/v0/${baseId}/${recordId}/${encodeURIComponent(fieldName)}/uploadAttachment`;
|
|
2319
|
+
const res = await fetch(url, {
|
|
2320
|
+
method: "POST",
|
|
2321
|
+
headers: {
|
|
2322
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2323
|
+
"Content-Type": "application/json"
|
|
2324
|
+
},
|
|
2325
|
+
body: JSON.stringify(payload)
|
|
2326
|
+
});
|
|
2327
|
+
if (!res.ok) {
|
|
2328
|
+
throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2245
2332
|
// src/reports/draft.ts
|
|
2246
2333
|
function scoresFromWebsite(siteRow) {
|
|
2247
2334
|
const { pScore, rScore, bpScore, seoScore } = siteRow;
|
|
@@ -2280,7 +2367,7 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
2280
2367
|
});
|
|
2281
2368
|
if (options.previewOnly) {
|
|
2282
2369
|
const path = options.previewPath ?? `reports/${slug}/draft.html`;
|
|
2283
|
-
await mkdir(
|
|
2370
|
+
await mkdir(dirname3(path), { recursive: true });
|
|
2284
2371
|
await writeFile9(path, html, "utf-8");
|
|
2285
2372
|
return { reportRow: null, htmlPath: path, html };
|
|
2286
2373
|
}
|
|
@@ -2296,7 +2383,8 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
2296
2383
|
lighthouse: scores,
|
|
2297
2384
|
lastTestedDate
|
|
2298
2385
|
});
|
|
2299
|
-
|
|
2386
|
+
const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
2387
|
+
await uploadAttachment(created.id, "Rendered HTML", html, htmlFilename, "text/html");
|
|
2300
2388
|
await setDraftReady(base, created.id, true);
|
|
2301
2389
|
return { reportRow: created, htmlPath: null, html };
|
|
2302
2390
|
}
|
|
@@ -2306,28 +2394,6 @@ async function derivePeriodStart(base, siteRow, reportType, today) {
|
|
|
2306
2394
|
const latest = sameType[sameType.length - 1];
|
|
2307
2395
|
return latest ? new Date(latest) : daysAgo(today, 30);
|
|
2308
2396
|
}
|
|
2309
|
-
async function uploadHtmlAttachment(recordId, html, slug, periodEnd) {
|
|
2310
|
-
const apiKey = process.env.AIRTABLE_PAT;
|
|
2311
|
-
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
2312
|
-
const filename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
2313
|
-
const body = {
|
|
2314
|
-
contentType: "text/html",
|
|
2315
|
-
file: Buffer.from(html, "utf-8").toString("base64"),
|
|
2316
|
-
filename
|
|
2317
|
-
};
|
|
2318
|
-
const url = `https://content.airtable.com/v0/${baseId}/${recordId}/Rendered%20HTML/uploadAttachment`;
|
|
2319
|
-
const res = await fetch(url, {
|
|
2320
|
-
method: "POST",
|
|
2321
|
-
headers: {
|
|
2322
|
-
Authorization: `Bearer ${apiKey}`,
|
|
2323
|
-
"Content-Type": "application/json"
|
|
2324
|
-
},
|
|
2325
|
-
body: JSON.stringify(body)
|
|
2326
|
-
});
|
|
2327
|
-
if (!res.ok) {
|
|
2328
|
-
throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
2397
|
|
|
2332
2398
|
// src/reports/airtable/client.ts
|
|
2333
2399
|
import Airtable from "airtable";
|
|
@@ -2342,19 +2408,6 @@ function openBase(cfg) {
|
|
|
2342
2408
|
return new Airtable({ apiKey: cfg.apiKey }).base(cfg.baseId);
|
|
2343
2409
|
}
|
|
2344
2410
|
|
|
2345
|
-
// src/reports/airtable/attachments.ts
|
|
2346
|
-
async function fetchAttachmentBytes(url) {
|
|
2347
|
-
const res = await fetch(url);
|
|
2348
|
-
if (!res.ok) {
|
|
2349
|
-
throw new Error(
|
|
2350
|
-
`Failed to fetch Airtable attachment ${res.status} ${res.statusText} (url=${url})`
|
|
2351
|
-
);
|
|
2352
|
-
}
|
|
2353
|
-
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
2354
|
-
const ab = await res.arrayBuffer();
|
|
2355
|
-
return { bytes: new Uint8Array(ab), contentType };
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
2411
|
// src/reports/send/resend.ts
|
|
2359
2412
|
import { Resend } from "resend";
|
|
2360
2413
|
function defaultResendClient() {
|
|
@@ -2419,6 +2472,7 @@ async function sendOne(client, base, site, report) {
|
|
|
2419
2472
|
throw new Error(`Report ${report.reportId} has no Lighthouse scores`);
|
|
2420
2473
|
}
|
|
2421
2474
|
const { bytes, contentType } = await fetchAttachmentBytes(site.headerImage.url);
|
|
2475
|
+
const bundled = await loadBundledImages();
|
|
2422
2476
|
const slug = siteSlug(site.name);
|
|
2423
2477
|
const cidName = `${slug}-header`;
|
|
2424
2478
|
const { html } = await renderReportHtml({
|
|
@@ -2471,6 +2525,21 @@ async function sendOne(client, base, site, report) {
|
|
|
2471
2525
|
content: Buffer.from(bytes).toString("base64"),
|
|
2472
2526
|
contentType,
|
|
2473
2527
|
inlineContentId: cidName
|
|
2528
|
+
},
|
|
2529
|
+
// Bundled images referenced via cid:rd-check-png / cid:rd-blurred-tests-jpg
|
|
2530
|
+
// in the template. Attached inline so the email is self-contained — no
|
|
2531
|
+
// external CDN dependency, no image-blocked broken icons in webmail.
|
|
2532
|
+
{
|
|
2533
|
+
filename: bundled.check.filename,
|
|
2534
|
+
content: Buffer.from(bundled.check.bytes).toString("base64"),
|
|
2535
|
+
contentType: bundled.check.contentType,
|
|
2536
|
+
inlineContentId: bundled.check.cid
|
|
2537
|
+
},
|
|
2538
|
+
{
|
|
2539
|
+
filename: bundled.blurred.filename,
|
|
2540
|
+
content: Buffer.from(bundled.blurred.bytes).toString("base64"),
|
|
2541
|
+
contentType: bundled.blurred.contentType,
|
|
2542
|
+
inlineContentId: bundled.blurred.cid
|
|
2474
2543
|
}
|
|
2475
2544
|
],
|
|
2476
2545
|
// Stable across retries of the same row — if Airtable stamping fails after a
|
|
@@ -2508,6 +2577,12 @@ function isProbablyEmail(s) {
|
|
|
2508
2577
|
}
|
|
2509
2578
|
|
|
2510
2579
|
// src/reports/due.ts
|
|
2580
|
+
var ELIGIBLE_STATUSES = /* @__PURE__ */ new Set([
|
|
2581
|
+
"in development",
|
|
2582
|
+
"launch period",
|
|
2583
|
+
"maintenance",
|
|
2584
|
+
"hosting"
|
|
2585
|
+
]);
|
|
2511
2586
|
var MONTHS = {
|
|
2512
2587
|
Monthly: 1,
|
|
2513
2588
|
Quarterly: 3,
|
|
@@ -2537,6 +2612,7 @@ function findDueReports(websites, reports, today) {
|
|
|
2537
2612
|
const out = [];
|
|
2538
2613
|
const todayStart = startOfDay(today);
|
|
2539
2614
|
for (const site of websites) {
|
|
2615
|
+
if (site.status !== null && !ELIGIBLE_STATUSES.has(site.status)) continue;
|
|
2540
2616
|
for (const type of ["Maintenance", "Testing"]) {
|
|
2541
2617
|
const freq = type === "Maintenance" ? site.maintenanceFreq : site.testingFreq;
|
|
2542
2618
|
if (freq === "None") continue;
|
|
@@ -2564,6 +2640,7 @@ export {
|
|
|
2564
2640
|
depsAudit,
|
|
2565
2641
|
draftReportForSite,
|
|
2566
2642
|
findDueReports,
|
|
2643
|
+
fromAirtableBase,
|
|
2567
2644
|
fromJsonFile,
|
|
2568
2645
|
isRecipeName,
|
|
2569
2646
|
lighthouseAudit,
|