@reddoorla/maintenance 0.86.0 → 0.88.2
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/airtable-QG5ICI3Y.js +9 -0
- package/dist/{announce-MXTUQ6C7.js → announce-GKD5YJNB.js} +49 -28
- package/dist/announce-GKD5YJNB.js.map +1 -0
- package/dist/attachments-NLZIJ5NU.js +9 -0
- package/dist/{blux-ZGDSHI3U.js → blux-DN4MI532.js} +5 -5
- package/dist/{bump-deps-EC74CPLS.js → bump-deps-KGH5UYSF.js} +4 -4
- package/dist/{chunk-N45M3FDK.js → chunk-6I6L3CMX.js} +4 -4
- package/dist/{chunk-KTH5ZODA.js → chunk-7BZUOZPU.js} +2 -2
- package/dist/{chunk-2EVEFOZY.js → chunk-7FFUBF4I.js} +6 -3
- package/dist/chunk-7FFUBF4I.js.map +1 -0
- package/dist/{chunk-LDWZSLSU.js → chunk-7NYN2P6I.js} +7 -4
- package/dist/{chunk-LDWZSLSU.js.map → chunk-7NYN2P6I.js.map} +1 -1
- package/dist/{chunk-X3B723J4.js → chunk-7TMWBXX7.js} +184 -25
- package/dist/chunk-7TMWBXX7.js.map +1 -0
- package/dist/{chunk-3VQWSE3F.js → chunk-7UAYMAKR.js} +4 -4
- package/dist/{chunk-W7Z3BGCI.js → chunk-A2UNJIKH.js} +15 -7
- package/dist/chunk-A2UNJIKH.js.map +1 -0
- package/dist/{chunk-F5KM4LEQ.js → chunk-ANJNTAMM.js} +4 -70
- package/dist/chunk-ANJNTAMM.js.map +1 -0
- package/dist/{chunk-2ZPT7GLV.js → chunk-B23FFWU4.js} +4 -4
- package/dist/chunk-B23FFWU4.js.map +1 -0
- package/dist/{chunk-DJZWYIPP.js → chunk-BGTYPVLT.js} +1 -1
- package/dist/chunk-BGTYPVLT.js.map +1 -0
- package/dist/chunk-BRYXFH77.js +70 -0
- package/dist/chunk-BRYXFH77.js.map +1 -0
- package/dist/{chunk-2NDRHXMR.js → chunk-DGZWFI2G.js} +18 -141
- package/dist/chunk-DGZWFI2G.js.map +1 -0
- package/dist/{chunk-ACBRLLFT.js → chunk-F3JWOPTG.js} +7 -3
- package/dist/chunk-F3JWOPTG.js.map +1 -0
- package/dist/chunk-GQDDWYU2.js +372 -0
- package/dist/chunk-GQDDWYU2.js.map +1 -0
- package/dist/{chunk-Z7AXYKQB.js → chunk-IY6256GK.js} +1 -1
- package/dist/{chunk-BYK66EWO.js → chunk-J3WEZGIN.js} +52 -46
- package/dist/chunk-J3WEZGIN.js.map +1 -0
- package/dist/{chunk-BNM5POSJ.js → chunk-JRHIFA5R.js} +2 -2
- package/dist/{chunk-ZKXSX4LO.js → chunk-K6SLF6GI.js} +6 -6
- package/dist/{chunk-CQR4LIM7.js → chunk-KJGKZLCT.js} +7 -3
- package/dist/{chunk-CQR4LIM7.js.map → chunk-KJGKZLCT.js.map} +1 -1
- package/dist/{chunk-PPE5KZJK.js → chunk-KMCZA7CH.js} +13 -3
- package/dist/chunk-KMCZA7CH.js.map +1 -0
- package/dist/{chunk-3SF7D4DY.js → chunk-MFHS7IQ2.js} +36 -1
- package/dist/{chunk-3SF7D4DY.js.map → chunk-MFHS7IQ2.js.map} +1 -1
- package/dist/{chunk-SNSTJLQW.js → chunk-NO74DXDP.js} +48 -2
- package/dist/chunk-NO74DXDP.js.map +1 -0
- package/dist/{chunk-C6B4RHXR.js → chunk-O3GT46R2.js} +8 -2
- package/dist/chunk-O3GT46R2.js.map +1 -0
- package/dist/{chunk-ITIOFN7A.js → chunk-P6AORG32.js} +29 -10
- package/dist/chunk-P6AORG32.js.map +1 -0
- package/dist/{chunk-SLUQ7R6I.js → chunk-PR343K7F.js} +5 -5
- package/dist/{chunk-P2FE5AI6.js → chunk-Q47DU255.js} +4 -4
- package/dist/{chunk-EVBCK2KC.js → chunk-RARREDQE.js} +2 -1
- package/dist/{chunk-EVBCK2KC.js.map → chunk-RARREDQE.js.map} +1 -1
- package/dist/chunk-RUPPWAD6.js +7 -0
- package/dist/chunk-RUPPWAD6.js.map +1 -0
- package/dist/{chunk-VSBFM7DS.js → chunk-SJOXWJ5Y.js} +3 -3
- package/dist/{chunk-TERE5SDF.js → chunk-ULHPALI5.js} +4 -4
- package/dist/{chunk-XUGSZP7Z.js → chunk-W45FZEGE.js} +2 -2
- package/dist/{chunk-JHGHLKFS.js → chunk-WS6NU475.js} +2 -2
- package/dist/chunk-XP5WM2LV.js +146 -0
- package/dist/chunk-XP5WM2LV.js.map +1 -0
- package/dist/{chunk-ILUNZHYV.js → chunk-YFAXOD7Z.js} +10 -6
- package/dist/chunk-YFAXOD7Z.js.map +1 -0
- package/dist/{chunk-6TWWY7ZU.js → chunk-Z3JXLULD.js} +2 -2
- package/dist/cli/bin.js +34 -29
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +4 -0
- package/dist/cli/commands/audit.js +25 -18
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-JYKPVCJX.js +11 -0
- package/dist/configs/playwright-a11y.js +1 -1
- package/dist/{convert-to-pnpm-6L6P74WR.js → convert-to-pnpm-NWB5FCQ2.js} +4 -4
- package/dist/{db-B35OGV3G.js → db-V6UIS433.js} +124 -29
- package/dist/db-V6UIS433.js.map +1 -0
- package/dist/{digest-BL6UJYKH.js → digest-2JEAEJK6.js} +60 -35
- package/dist/digest-2JEAEJK6.js.map +1 -0
- package/dist/digest-state-CRZRNGDI.js +25 -0
- package/dist/digest-state-CRZRNGDI.js.map +1 -0
- package/dist/digest-state-F37XUCS5.js +13 -0
- package/dist/{dump-P6EBSIOH.js → dump-EPL7AZFR.js} +38 -13
- package/dist/dump-EPL7AZFR.js.map +1 -0
- package/dist/{email-VTTTKKL5.js → email-SL7464YI.js} +16 -6
- package/dist/{email-VTTTKKL5.js.map → email-SL7464YI.js.map} +1 -1
- package/dist/{ensure-site-DFTB7MN3.js → ensure-site-FU6527OU.js} +19 -11
- package/dist/ensure-site-FU6527OU.js.map +1 -0
- package/dist/fleet-state-R5LU7IC3.js +41 -0
- package/dist/{forms-notify-target-75U6OU7L.js → forms-notify-target-SGDGXXK5.js} +10 -7
- package/dist/forms-notify-target-SGDGXXK5.js.map +1 -0
- package/dist/{github-signals-IJUPGCUX.js → github-signals-GWGCKEE2.js} +13 -12
- package/dist/{github-signals-IJUPGCUX.js.map → github-signals-GWGCKEE2.js.map} +1 -1
- package/dist/{header-image-AWLLQA4L.js → header-image-7A7XLVEU.js} +8 -8
- package/dist/{header-images-335L2AKB.js → header-images-SQBCE5OD.js} +21 -1
- package/dist/header-images-SQBCE5OD.js.map +1 -0
- package/dist/{health-endpoint-WJC2UMYS.js → health-endpoint-UVX6R5N5.js} +4 -4
- package/dist/health-mirror-GCDN7BJL.js +46 -0
- package/dist/health-mirror-GCDN7BJL.js.map +1 -0
- package/dist/{import-airtable-HE43FB4H.js → import-airtable-OHVY344G.js} +7 -5
- package/dist/index.d.ts +149 -0
- package/dist/index.js +245 -108
- package/dist/index.js.map +1 -1
- package/dist/{init-NJEOEO7I.js → init-FGTYNGUI.js} +11 -11
- package/dist/{launch-XNAX427U.js → launch-5H7LPEGP.js} +59 -29
- package/dist/launch-5H7LPEGP.js.map +1 -0
- package/dist/{lighthouse-airtable-LBVIYYP5.js → lighthouse-airtable-3GUWHETW.js} +3 -3
- package/dist/migrate-YITCXBLS.js +7 -0
- package/dist/{notify-C645PGD5.js → notify-4HP4GZ2H.js} +5 -5
- package/dist/{onboard-GK4PHPNI.js → onboard-BKCYBAVW.js} +4 -4
- package/dist/{orchestrate-77UO4BYJ.js → orchestrate-JNKCWU5K.js} +14 -13
- package/dist/parity-NBYZUBFU.js +14 -0
- package/dist/pdf-N5QLQ5ZQ.js +38 -0
- package/dist/pdf-N5QLQ5ZQ.js.map +1 -0
- package/dist/{pipeline-S6CVHDGB.js → pipeline-WYTXSLHX.js} +3 -3
- package/dist/{preflight-6WBNU2GZ.js → preflight-IMTYDDD4.js} +15 -14
- package/dist/{preflight-6WBNU2GZ.js.map → preflight-IMTYDDD4.js.map} +1 -1
- package/dist/{prismic-ci-3RHOZUUT.js → prismic-ci-MY2U6NN7.js} +7 -7
- package/dist/{prismic-models-QBGUT2Q3.js → prismic-models-NJN5JORF.js} +15 -10
- package/dist/prismic-models-NJN5JORF.js.map +1 -0
- package/dist/prospect/types.d.ts +281 -0
- package/dist/prospect/types.js +1 -0
- package/dist/{prospect-audit-IUNCJ2LV.js → prospect-audit-JMBRVXSG.js} +32 -13
- package/dist/prospect-audit-JMBRVXSG.js.map +1 -0
- package/dist/recipes/sync-configs.js +2 -2
- package/dist/render-VX72AESY.js +10 -0
- package/dist/render-VX72AESY.js.map +1 -0
- package/dist/render-from-row-DOYJDO2L.js +17 -0
- package/dist/render-from-row-DOYJDO2L.js.map +1 -0
- package/dist/{renovate-dispatch-QJY75ODR.js → renovate-dispatch-6GQWTRKX.js} +10 -10
- package/dist/{renovate-dispatch-QJY75ODR.js.map → renovate-dispatch-6GQWTRKX.js.map} +1 -1
- package/dist/{replay-C2LXMJZP.js → replay-4S6ENYCH.js} +3 -3
- package/dist/{report-4EVNDHOZ.js → report-TALY5P3L.js} +63 -21
- package/dist/report-TALY5P3L.js.map +1 -0
- package/dist/report-mirror-AAK3MXHY.js +55 -0
- package/dist/report-mirror-AAK3MXHY.js.map +1 -0
- package/dist/rerender-3FM6DNRH.js +32 -0
- package/dist/rerender-3FM6DNRH.js.map +1 -0
- package/dist/schema-75PJECAE.js +18 -0
- package/dist/schema-75PJECAE.js.map +1 -0
- package/dist/{self-updating-6DWI2Z56.js → self-updating-NTCCWJYZ.js} +4 -4
- package/dist/{selftest-RC36MTKP.js → selftest-NZTR4SM2.js} +26 -24
- package/dist/{selftest-RC36MTKP.js.map → selftest-NZTR4SM2.js.map} +1 -1
- package/dist/site-mirror-Q763PLGG.js +62 -0
- package/dist/site-mirror-Q763PLGG.js.map +1 -0
- package/dist/{smoke-suite-VYY2LXM2.js → smoke-suite-4JFZZYVW.js} +4 -4
- package/dist/{spam-classifier-XLBFCO7G.js → spam-classifier-T5H4VTP6.js} +4 -2
- package/dist/spam-classifier-T5H4VTP6.js.map +1 -0
- package/dist/{submissions-Q7DRFOCD.js → submissions-TEHPBSZJ.js} +5 -5
- package/dist/{svelte-codemods-25J2FO22.js → svelte-codemods-QAPDZC6F.js} +4 -4
- package/dist/{sync-34HNZZ56.js → sync-GBFV45WX.js} +6 -4
- package/dist/{sync-34HNZZ56.js.map → sync-GBFV45WX.js.map} +1 -1
- package/dist/{sync-configs-3SE6PSCO.js → sync-configs-34JIA6KZ.js} +6 -6
- package/dist/{upgrade-WBRQ62FX.js → upgrade-IR2OO3KP.js} +4 -4
- package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
- package/dist/{webhook-VOGOW3W6.js → webhook-XM6EV4XT.js} +2 -2
- package/dist/{websites-SXWTP72S.js → websites-D6WYFAES.js} +2 -2
- package/dist/websites-D6WYFAES.js.map +1 -0
- package/dist/write-audits-to-airtable-OBHGR7DQ.js +17 -0
- package/dist/write-audits-to-airtable-OBHGR7DQ.js.map +1 -0
- package/package.json +5 -1
- package/dist/airtable-G7WBQQMI.js +0 -9
- package/dist/announce-MXTUQ6C7.js.map +0 -1
- package/dist/chunk-2EVEFOZY.js.map +0 -1
- package/dist/chunk-2NDRHXMR.js.map +0 -1
- package/dist/chunk-2ZPT7GLV.js.map +0 -1
- package/dist/chunk-ACBRLLFT.js.map +0 -1
- package/dist/chunk-BYK66EWO.js.map +0 -1
- package/dist/chunk-C6B4RHXR.js.map +0 -1
- package/dist/chunk-DJZWYIPP.js.map +0 -1
- package/dist/chunk-F5KM4LEQ.js.map +0 -1
- package/dist/chunk-ILUNZHYV.js.map +0 -1
- package/dist/chunk-ITIOFN7A.js.map +0 -1
- package/dist/chunk-PPE5KZJK.js.map +0 -1
- package/dist/chunk-SNSTJLQW.js.map +0 -1
- package/dist/chunk-W7Z3BGCI.js.map +0 -1
- package/dist/chunk-X3B723J4.js.map +0 -1
- package/dist/client-AUE3XUPO.js +0 -11
- package/dist/db-B35OGV3G.js.map +0 -1
- package/dist/digest-BL6UJYKH.js.map +0 -1
- package/dist/dump-P6EBSIOH.js.map +0 -1
- package/dist/ensure-site-DFTB7MN3.js.map +0 -1
- package/dist/forms-notify-target-75U6OU7L.js.map +0 -1
- package/dist/header-images-335L2AKB.js.map +0 -1
- package/dist/health-mirror-4B2L7T2Y.js +0 -75
- package/dist/health-mirror-4B2L7T2Y.js.map +0 -1
- package/dist/launch-XNAX427U.js.map +0 -1
- package/dist/migrate-GQH237A6.js +0 -7
- package/dist/parity-MWS24A3H.js +0 -12
- package/dist/prismic-models-QBGUT2Q3.js.map +0 -1
- package/dist/prospect-audit-IUNCJ2LV.js.map +0 -1
- package/dist/render-AQAVQDPZ.js +0 -10
- package/dist/report-4EVNDHOZ.js.map +0 -1
- package/dist/write-audits-to-airtable-MBEILLPU.js +0 -14
- /package/dist/{airtable-G7WBQQMI.js.map → airtable-QG5ICI3Y.js.map} +0 -0
- /package/dist/{client-AUE3XUPO.js.map → attachments-NLZIJ5NU.js.map} +0 -0
- /package/dist/{blux-ZGDSHI3U.js.map → blux-DN4MI532.js.map} +0 -0
- /package/dist/{bump-deps-EC74CPLS.js.map → bump-deps-KGH5UYSF.js.map} +0 -0
- /package/dist/{chunk-N45M3FDK.js.map → chunk-6I6L3CMX.js.map} +0 -0
- /package/dist/{chunk-KTH5ZODA.js.map → chunk-7BZUOZPU.js.map} +0 -0
- /package/dist/{chunk-3VQWSE3F.js.map → chunk-7UAYMAKR.js.map} +0 -0
- /package/dist/{chunk-Z7AXYKQB.js.map → chunk-IY6256GK.js.map} +0 -0
- /package/dist/{chunk-BNM5POSJ.js.map → chunk-JRHIFA5R.js.map} +0 -0
- /package/dist/{chunk-ZKXSX4LO.js.map → chunk-K6SLF6GI.js.map} +0 -0
- /package/dist/{chunk-SLUQ7R6I.js.map → chunk-PR343K7F.js.map} +0 -0
- /package/dist/{chunk-P2FE5AI6.js.map → chunk-Q47DU255.js.map} +0 -0
- /package/dist/{chunk-VSBFM7DS.js.map → chunk-SJOXWJ5Y.js.map} +0 -0
- /package/dist/{chunk-TERE5SDF.js.map → chunk-ULHPALI5.js.map} +0 -0
- /package/dist/{chunk-XUGSZP7Z.js.map → chunk-W45FZEGE.js.map} +0 -0
- /package/dist/{chunk-JHGHLKFS.js.map → chunk-WS6NU475.js.map} +0 -0
- /package/dist/{chunk-6TWWY7ZU.js.map → chunk-Z3JXLULD.js.map} +0 -0
- /package/dist/{import-airtable-HE43FB4H.js.map → client-JYKPVCJX.js.map} +0 -0
- /package/dist/{convert-to-pnpm-6L6P74WR.js.map → convert-to-pnpm-NWB5FCQ2.js.map} +0 -0
- /package/dist/{lighthouse-airtable-LBVIYYP5.js.map → digest-state-F37XUCS5.js.map} +0 -0
- /package/dist/{migrate-GQH237A6.js.map → fleet-state-R5LU7IC3.js.map} +0 -0
- /package/dist/{header-image-AWLLQA4L.js.map → header-image-7A7XLVEU.js.map} +0 -0
- /package/dist/{health-endpoint-WJC2UMYS.js.map → health-endpoint-UVX6R5N5.js.map} +0 -0
- /package/dist/{notify-C645PGD5.js.map → import-airtable-OHVY344G.js.map} +0 -0
- /package/dist/{init-NJEOEO7I.js.map → init-FGTYNGUI.js.map} +0 -0
- /package/dist/{orchestrate-77UO4BYJ.js.map → lighthouse-airtable-3GUWHETW.js.map} +0 -0
- /package/dist/{parity-MWS24A3H.js.map → migrate-YITCXBLS.js.map} +0 -0
- /package/dist/{pipeline-S6CVHDGB.js.map → notify-4HP4GZ2H.js.map} +0 -0
- /package/dist/{onboard-GK4PHPNI.js.map → onboard-BKCYBAVW.js.map} +0 -0
- /package/dist/{render-AQAVQDPZ.js.map → orchestrate-JNKCWU5K.js.map} +0 -0
- /package/dist/{spam-classifier-XLBFCO7G.js.map → parity-NBYZUBFU.js.map} +0 -0
- /package/dist/{websites-SXWTP72S.js.map → pipeline-WYTXSLHX.js.map} +0 -0
- /package/dist/{prismic-ci-3RHOZUUT.js.map → prismic-ci-MY2U6NN7.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-MBEILLPU.js.map → prospect/types.js.map} +0 -0
- /package/dist/{replay-C2LXMJZP.js.map → replay-4S6ENYCH.js.map} +0 -0
- /package/dist/{self-updating-6DWI2Z56.js.map → self-updating-NTCCWJYZ.js.map} +0 -0
- /package/dist/{smoke-suite-VYY2LXM2.js.map → smoke-suite-4JFZZYVW.js.map} +0 -0
- /package/dist/{submissions-Q7DRFOCD.js.map → submissions-TEHPBSZJ.js.map} +0 -0
- /package/dist/{svelte-codemods-25J2FO22.js.map → svelte-codemods-QAPDZC6F.js.map} +0 -0
- /package/dist/{sync-configs-3SE6PSCO.js.map → sync-configs-34JIA6KZ.js.map} +0 -0
- /package/dist/{upgrade-WBRQ62FX.js.map → upgrade-IR2OO3KP.js.map} +0 -0
- /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
- /package/dist/{webhook-VOGOW3W6.js.map → webhook-XM6EV4XT.js.map} +0 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SITE_FIELDS,
|
|
3
|
+
healthColumnFor,
|
|
4
|
+
mapReportRecord,
|
|
5
|
+
mapWebsiteRecord,
|
|
6
|
+
scheduleColumnFor,
|
|
7
|
+
siteValueFor
|
|
8
|
+
} from "./chunk-P6AORG32.js";
|
|
9
|
+
import {
|
|
10
|
+
parseAutoEvidence,
|
|
11
|
+
toReportType
|
|
12
|
+
} from "./chunk-F3JWOPTG.js";
|
|
13
|
+
import {
|
|
14
|
+
MAINTENANCE_CHECKLIST,
|
|
15
|
+
TESTING_CHECKLIST
|
|
16
|
+
} from "./chunk-VAD43MOR.js";
|
|
17
|
+
import {
|
|
18
|
+
canonicalizeStatus,
|
|
19
|
+
parseNotifyRouting,
|
|
20
|
+
parseSecurityAdvisories,
|
|
21
|
+
toFrequency,
|
|
22
|
+
toPrismicModelsVerdict,
|
|
23
|
+
toVerdict,
|
|
24
|
+
trimToNull
|
|
25
|
+
} from "./chunk-7NYN2P6I.js";
|
|
26
|
+
|
|
27
|
+
// src/db/fleet-state.ts
|
|
28
|
+
import { sql } from "kysely";
|
|
29
|
+
var str = (v) => typeof v === "string" ? v : null;
|
|
30
|
+
var num = (v) => typeof v === "number" ? v : null;
|
|
31
|
+
var bool = (v) => typeof v === "number" ? v !== 0 : null;
|
|
32
|
+
function parseAwc(raw) {
|
|
33
|
+
if (typeof raw !== "string" || raw.trim() === "") return [];
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
if (!Array.isArray(parsed)) return [];
|
|
37
|
+
return parsed.filter((x) => typeof x === "string").map((s) => s.trim()).filter(Boolean);
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function rowFromJoined(r) {
|
|
43
|
+
const name = String(r.name ?? "");
|
|
44
|
+
const headerFilename = str(r.header_image_filename);
|
|
45
|
+
const headerType = str(r.header_image_type);
|
|
46
|
+
return {
|
|
47
|
+
id: String(r.id),
|
|
48
|
+
name,
|
|
49
|
+
url: str(r.url) ?? "",
|
|
50
|
+
// `sites.status` holds the RAW Airtable cell (the importer stores it verbatim
|
|
51
|
+
// so the hourly parity check compares raw-to-raw). Canonicalizing HERE — the
|
|
52
|
+
// twin of mapRow's seam — is what keeps the #558 equivalence instrument green.
|
|
53
|
+
status: canonicalizeStatus(r.status),
|
|
54
|
+
statusRaw: str(r.status),
|
|
55
|
+
pointOfContact: str(r.point_of_contact),
|
|
56
|
+
maintenanceFreq: toFrequency(r.maintenance_freq, `${name} maintenance`),
|
|
57
|
+
testingFreq: toFrequency(r.testing_freq, `${name} testing`),
|
|
58
|
+
maintenanceFreqRaw: str(r.maintenance_freq),
|
|
59
|
+
testingFreqRaw: str(r.testing_freq),
|
|
60
|
+
maintenanceDay: str(r.maintenance_day),
|
|
61
|
+
testingDay: str(r.testing_day),
|
|
62
|
+
ga4PropertyId: str(r.ga4_property_id),
|
|
63
|
+
searchQuery: str(r.search_query),
|
|
64
|
+
searchConsoleProperty: str(r.search_console_property),
|
|
65
|
+
analyticsSoftFailAt: str(r.analytics_soft_fail_at),
|
|
66
|
+
gitRepo: str(r.git_repo),
|
|
67
|
+
reportRecipientsTo: str(r.report_recipients_to),
|
|
68
|
+
reportRecipientsCc: str(r.report_recipients_cc),
|
|
69
|
+
acceptedWatchConditions: parseAwc(r.accepted_watch_conditions),
|
|
70
|
+
headerImage: headerFilename !== null && headerType !== null ? { url: "", filename: headerFilename, type: headerType } : null,
|
|
71
|
+
pScore: num(r.p_score),
|
|
72
|
+
rScore: num(r.r_score),
|
|
73
|
+
bpScore: num(r.bp_score),
|
|
74
|
+
seoScore: num(r.seo_score),
|
|
75
|
+
lastLighthouseAuditAt: str(r.lighthouse_at),
|
|
76
|
+
a11yViolations: num(r.a11y_violations),
|
|
77
|
+
depsDrifted: num(r.deps_drifted),
|
|
78
|
+
depsMajorBehind: num(r.deps_major_behind),
|
|
79
|
+
depsOutdated: num(r.deps_outdated),
|
|
80
|
+
depsMajorOutdated: num(r.deps_major_outdated),
|
|
81
|
+
securityVulnsCritical: num(r.vulns_critical),
|
|
82
|
+
securityVulnsHigh: num(r.vulns_high),
|
|
83
|
+
securityVulnsModerate: num(r.vulns_moderate),
|
|
84
|
+
securityVulnsLow: num(r.vulns_low),
|
|
85
|
+
securityAutoFixAttempts: num(r.auto_fix_attempts),
|
|
86
|
+
lastSecurityAuditAt: str(r.security_audit_at),
|
|
87
|
+
securityAdvisories: parseSecurityAdvisories(r.security_advisories),
|
|
88
|
+
certDaysRemaining: num(r.cert_days_remaining),
|
|
89
|
+
domainCheckedAt: str(r.domain_checked_at),
|
|
90
|
+
netlifyId: trimToNull(r.netlify_id),
|
|
91
|
+
deployStatus: str(r.deploy_status),
|
|
92
|
+
lastDeployAt: str(r.last_deploy_at),
|
|
93
|
+
deployLogUrl: str(r.deploy_log_url),
|
|
94
|
+
deployCheckedAt: str(r.deploy_checked_at),
|
|
95
|
+
functionHealth: toVerdict(r.function_health),
|
|
96
|
+
cmsReachable: toVerdict(r.cms_reachable),
|
|
97
|
+
turnstileWidget: toVerdict(r.turnstile_widget),
|
|
98
|
+
functionHealthCheckedAt: str(r.function_health_checked_at),
|
|
99
|
+
crossbrowserOk: bool(r.crossbrowser_ok),
|
|
100
|
+
mobileOk: bool(r.mobile_ok),
|
|
101
|
+
linksOk: bool(r.links_ok),
|
|
102
|
+
brokenLinks: num(r.broken_links),
|
|
103
|
+
browserCheckedAt: str(r.browser_checked_at),
|
|
104
|
+
reachableOk: toVerdict(r.uptime_reachable),
|
|
105
|
+
titleMetaOk: toVerdict(r.titles_meta_ok),
|
|
106
|
+
copyIntro: trimToNull(r.copy_intro),
|
|
107
|
+
copyContact: trimToNull(r.copy_contact),
|
|
108
|
+
copyFooter: trimToNull(r.copy_footer),
|
|
109
|
+
launchedAt: str(r.launched_at),
|
|
110
|
+
newsletterWebhook: trimToNull(r.newsletter_webhook),
|
|
111
|
+
notifyRouting: parseNotifyRouting(r.notify_routing),
|
|
112
|
+
notifyRoutingRaw: trimToNull(r.notify_routing),
|
|
113
|
+
mailchimpApiKey: trimToNull(r.mailchimp_api_key),
|
|
114
|
+
mailchimpAudienceId: trimToNull(r.mailchimp_audience_id),
|
|
115
|
+
requireTurnstile: typeof r.require_turnstile === "number" ? r.require_turnstile !== 0 : false,
|
|
116
|
+
renovateFailingCis: num(r.renovate_failing_cis),
|
|
117
|
+
defaultBranchCi: str(r.default_branch_ci),
|
|
118
|
+
lastCommitAt: str(r.last_commit_at),
|
|
119
|
+
githubSignalsAt: str(r.github_signals_at),
|
|
120
|
+
smokeOk: toVerdict(r.smoke_ok),
|
|
121
|
+
lastSmokeAt: str(r.last_smoke_at),
|
|
122
|
+
formE2eOk: toVerdict(r.form_e2e_ok),
|
|
123
|
+
formE2eCheckedAt: str(r.form_e2e_checked_at),
|
|
124
|
+
prismicModels: toPrismicModelsVerdict(r.prismic_models),
|
|
125
|
+
prismicModelsCheckedAt: str(r.prismic_models_checked_at),
|
|
126
|
+
prismicModelsDrift: str(r.prismic_models_drift),
|
|
127
|
+
prismicAckUntil: str(r.prismic_ack_until),
|
|
128
|
+
nextMaintenanceAt: str(r.next_maintenance_at),
|
|
129
|
+
nextTestingAt: str(r.next_testing_at)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
var SITE_COLUMNS = [
|
|
133
|
+
"sites.id",
|
|
134
|
+
"sites.slug",
|
|
135
|
+
"sites.name",
|
|
136
|
+
"sites.url",
|
|
137
|
+
"sites.status",
|
|
138
|
+
"sites.point_of_contact",
|
|
139
|
+
"sites.maintenance_freq",
|
|
140
|
+
"sites.testing_freq",
|
|
141
|
+
"sites.maintenance_day",
|
|
142
|
+
"sites.testing_day",
|
|
143
|
+
"sites.ga4_property_id",
|
|
144
|
+
"sites.search_query",
|
|
145
|
+
"sites.search_console_property",
|
|
146
|
+
"sites.git_repo",
|
|
147
|
+
"sites.netlify_id",
|
|
148
|
+
"sites.report_recipients_to",
|
|
149
|
+
"sites.report_recipients_cc",
|
|
150
|
+
"sites.copy_intro",
|
|
151
|
+
"sites.copy_contact",
|
|
152
|
+
"sites.copy_footer",
|
|
153
|
+
"sites.newsletter_webhook",
|
|
154
|
+
"sites.mailchimp_api_key",
|
|
155
|
+
"sites.mailchimp_audience_id",
|
|
156
|
+
"sites.notify_routing",
|
|
157
|
+
"sites.require_turnstile",
|
|
158
|
+
"sites.accepted_watch_conditions",
|
|
159
|
+
"sites.prismic_ack_until",
|
|
160
|
+
"sites.launched_at",
|
|
161
|
+
"sites.header_image_filename",
|
|
162
|
+
"sites.header_image_type",
|
|
163
|
+
"sites.header_image_generated_at",
|
|
164
|
+
"sites.legacy"
|
|
165
|
+
];
|
|
166
|
+
function joined(db) {
|
|
167
|
+
return db.selectFrom("sites").leftJoin("site_health", "site_health.site_id", "sites.id").leftJoin("site_schedule", "site_schedule.site_id", "sites.id").select(SITE_COLUMNS).selectAll(["site_health", "site_schedule"]);
|
|
168
|
+
}
|
|
169
|
+
async function mirrorSiteField(db, siteId, airtableColumn, value) {
|
|
170
|
+
await mirrorSiteFields(db, siteId, { [airtableColumn]: value });
|
|
171
|
+
}
|
|
172
|
+
async function mirrorSiteFields(db, siteId, fields) {
|
|
173
|
+
const patch = {};
|
|
174
|
+
for (const [airtableColumn, value] of Object.entries(fields)) {
|
|
175
|
+
const col = SITE_FIELDS[airtableColumn];
|
|
176
|
+
if (!col)
|
|
177
|
+
throw new Error(`mirrorSiteFields: importer claims no sites column for '${airtableColumn}'`);
|
|
178
|
+
patch[col] = siteValueFor(col, value);
|
|
179
|
+
}
|
|
180
|
+
if (Object.keys(patch).length === 0) return true;
|
|
181
|
+
const res = await db.updateTable("sites").set(patch).where("id", "=", siteId).executeTakeFirst();
|
|
182
|
+
return res.numUpdatedRows > 0n;
|
|
183
|
+
}
|
|
184
|
+
async function mirrorSiteInsert(db, rec, computedAt) {
|
|
185
|
+
const { site, health, schedule } = mapWebsiteRecord(rec, computedAt);
|
|
186
|
+
await db.insertInto("sites").values(site).onConflict((oc) => oc.column("id").doUpdateSet(site)).execute();
|
|
187
|
+
await db.insertInto("site_health").values(health).onConflict((oc) => oc.column("site_id").doUpdateSet(health)).execute();
|
|
188
|
+
await db.insertInto("site_schedule").values(schedule).onConflict((oc) => oc.column("site_id").doUpdateSet(schedule)).execute();
|
|
189
|
+
}
|
|
190
|
+
async function mirrorHealthFields(db, siteId, fields) {
|
|
191
|
+
const patch = {};
|
|
192
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
193
|
+
const m = healthColumnFor(field);
|
|
194
|
+
if (!m)
|
|
195
|
+
throw new Error(`mirrorHealthFields: importer claims no site_health column for '${field}'`);
|
|
196
|
+
patch[m.col] = m.coerce(value);
|
|
197
|
+
}
|
|
198
|
+
if (Object.keys(patch).length === 0) return true;
|
|
199
|
+
const res = await db.updateTable("site_health").set(patch).where("site_id", "=", siteId).executeTakeFirst();
|
|
200
|
+
return res.numUpdatedRows > 0n;
|
|
201
|
+
}
|
|
202
|
+
async function mirrorScheduleFields(db, siteId, fields, computedAt) {
|
|
203
|
+
const patch = {};
|
|
204
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
205
|
+
const m = scheduleColumnFor(field);
|
|
206
|
+
if (!m)
|
|
207
|
+
throw new Error(
|
|
208
|
+
`mirrorScheduleFields: importer claims no site_schedule column for '${field}'`
|
|
209
|
+
);
|
|
210
|
+
patch[m.col] = m.coerce(value);
|
|
211
|
+
}
|
|
212
|
+
if (Object.keys(patch).length === 0) return true;
|
|
213
|
+
patch.computed_at = computedAt;
|
|
214
|
+
const res = await db.updateTable("site_schedule").set(patch).where("site_id", "=", siteId).executeTakeFirst();
|
|
215
|
+
return res.numUpdatedRows > 0n;
|
|
216
|
+
}
|
|
217
|
+
async function getSiteBySlug(db, slug) {
|
|
218
|
+
const r = await joined(db).where("sites.slug", "=", slug).executeTakeFirst();
|
|
219
|
+
return r ? rowFromJoined(r) : null;
|
|
220
|
+
}
|
|
221
|
+
async function getSiteById(db, id) {
|
|
222
|
+
const r = await joined(db).where("sites.id", "=", id).executeTakeFirst();
|
|
223
|
+
return r ? rowFromJoined(r) : null;
|
|
224
|
+
}
|
|
225
|
+
async function listSites(db) {
|
|
226
|
+
const rows = await joined(db).orderBy("sites.name").execute();
|
|
227
|
+
return rows.map((r) => rowFromJoined(r));
|
|
228
|
+
}
|
|
229
|
+
var CHECKLIST_FIELD_BY_KEY = new Map(
|
|
230
|
+
[...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map((i) => [i.key, i.field])
|
|
231
|
+
);
|
|
232
|
+
function checklistFromStored(raw) {
|
|
233
|
+
let stored = {};
|
|
234
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(raw);
|
|
237
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
238
|
+
stored = parsed;
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const out = {};
|
|
243
|
+
for (const [key, field] of CHECKLIST_FIELD_BY_KEY) out[field] = Boolean(stored[key]);
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
function reportRowFromDb(r, hasRenderedHtml) {
|
|
247
|
+
const p = r.lighthouse_performance;
|
|
248
|
+
const a = r.lighthouse_accessibility;
|
|
249
|
+
const b = r.lighthouse_best_practices;
|
|
250
|
+
const seo = r.lighthouse_seo;
|
|
251
|
+
const lighthouse = typeof p === "number" && typeof a === "number" && typeof b === "number" && typeof seo === "number" ? { performance: p, accessibility: a, bestPractices: b, seo } : null;
|
|
252
|
+
return {
|
|
253
|
+
id: r.id,
|
|
254
|
+
reportId: r.report_id ?? "",
|
|
255
|
+
siteId: r.site_id ?? "",
|
|
256
|
+
reportType: toReportType(r.report_type ?? void 0),
|
|
257
|
+
period: r.period,
|
|
258
|
+
periodStart: r.period_start,
|
|
259
|
+
periodEnd: r.period_end,
|
|
260
|
+
completedOn: r.completed_on,
|
|
261
|
+
lighthouse,
|
|
262
|
+
gaUsersCurrent: r.ga_users_current,
|
|
263
|
+
gaUsersPrevious: r.ga_users_previous,
|
|
264
|
+
searchFoundPage1: bool(r.search_found_page1),
|
|
265
|
+
searchPosition: r.search_position,
|
|
266
|
+
lastTestedDate: r.last_tested_date,
|
|
267
|
+
commentary: r.commentary,
|
|
268
|
+
subjectOverride: r.subject_override,
|
|
269
|
+
draftReady: r.draft_ready !== 0,
|
|
270
|
+
approvedToSend: r.approved_to_send !== 0,
|
|
271
|
+
sentAt: r.sent_at,
|
|
272
|
+
approvedAt: r.approved_at,
|
|
273
|
+
approvedBy: r.approved_by,
|
|
274
|
+
deliveryStatus: r.delivery_status ?? "pending",
|
|
275
|
+
// The body lives IN the row (rendered_html) — the link points at the
|
|
276
|
+
// dashboard's own preview route instead of an EXPIRING Airtable signed URL.
|
|
277
|
+
// Strictly better for the operator: the old link 404'd once the URL aged out.
|
|
278
|
+
renderedHtmlAttachment: hasRenderedHtml ? { url: `/api/reports/${r.id}/preview`, filename: `${r.report_id ?? r.id}.html` } : null,
|
|
279
|
+
resendMessageId: r.resend_message_id,
|
|
280
|
+
checklist: checklistFromStored(r.checklist),
|
|
281
|
+
autoEvidence: parseAutoEvidence(r.checklist_auto_evidence),
|
|
282
|
+
sendOverride: r.send_override !== 0,
|
|
283
|
+
overrideReason: r.override_reason,
|
|
284
|
+
overrideBy: r.override_by,
|
|
285
|
+
overrideAt: r.override_at
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
var REPORT_LIST_COLUMNS = [
|
|
289
|
+
"id",
|
|
290
|
+
"site_id",
|
|
291
|
+
"report_id",
|
|
292
|
+
"report_type",
|
|
293
|
+
"period",
|
|
294
|
+
"period_start",
|
|
295
|
+
"period_end",
|
|
296
|
+
"completed_on",
|
|
297
|
+
"lighthouse_performance",
|
|
298
|
+
"lighthouse_accessibility",
|
|
299
|
+
"lighthouse_best_practices",
|
|
300
|
+
"lighthouse_seo",
|
|
301
|
+
"ga_users_current",
|
|
302
|
+
"ga_users_previous",
|
|
303
|
+
"search_found_page1",
|
|
304
|
+
"search_position",
|
|
305
|
+
"last_tested_date",
|
|
306
|
+
"commentary",
|
|
307
|
+
"subject_override",
|
|
308
|
+
"draft_ready",
|
|
309
|
+
"approved_to_send",
|
|
310
|
+
"approved_at",
|
|
311
|
+
"approved_by",
|
|
312
|
+
"send_override",
|
|
313
|
+
"override_reason",
|
|
314
|
+
"override_by",
|
|
315
|
+
"override_at",
|
|
316
|
+
"sent_at",
|
|
317
|
+
"delivery_status",
|
|
318
|
+
"resend_message_id",
|
|
319
|
+
"checklist",
|
|
320
|
+
"checklist_auto_evidence"
|
|
321
|
+
];
|
|
322
|
+
var HAS_RENDERED_HTML = sql`(rendered_html is not null)`.as("has_rendered_html");
|
|
323
|
+
async function listAllReports(db) {
|
|
324
|
+
const rows = await db.selectFrom("reports").select(REPORT_LIST_COLUMNS).select(HAS_RENDERED_HTML).orderBy("period_start", "desc").orderBy("id").execute();
|
|
325
|
+
return rows.map((r) => reportRowFromDb(r, r.has_rendered_html !== 0));
|
|
326
|
+
}
|
|
327
|
+
async function listReportsForSite(db, siteId) {
|
|
328
|
+
const rows = await db.selectFrom("reports").select(REPORT_LIST_COLUMNS).select(HAS_RENDERED_HTML).where("site_id", "=", siteId).orderBy("period_start", "desc").orderBy("id").execute();
|
|
329
|
+
return rows.map((r) => reportRowFromDb(r, r.has_rendered_html !== 0));
|
|
330
|
+
}
|
|
331
|
+
async function mirrorReportPatch(db, reportId, patch) {
|
|
332
|
+
if (Object.keys(patch).length === 0) return;
|
|
333
|
+
await db.updateTable("reports").set(patch).where("id", "=", reportId).execute();
|
|
334
|
+
}
|
|
335
|
+
async function mirrorReportInsert(db, rec) {
|
|
336
|
+
const row = mapReportRecord(rec, null);
|
|
337
|
+
const { rendered_html: _rh, ...rowSansHtml } = row;
|
|
338
|
+
void _rh;
|
|
339
|
+
await db.insertInto("reports").values(row).onConflict((oc) => oc.column("id").doUpdateSet(rowSansHtml)).execute();
|
|
340
|
+
}
|
|
341
|
+
async function storeRenderedHtml(db, reportId, html) {
|
|
342
|
+
await db.updateTable("reports").set({ rendered_html: html }).where("id", "=", reportId).execute();
|
|
343
|
+
}
|
|
344
|
+
async function getReportById(db, id) {
|
|
345
|
+
const r = await db.selectFrom("reports").selectAll().where("id", "=", id).executeTakeFirst();
|
|
346
|
+
return r ? reportRowFromDb(r, r.rendered_html !== null) : null;
|
|
347
|
+
}
|
|
348
|
+
async function getReportHtml(db, id) {
|
|
349
|
+
const r = await db.selectFrom("reports").select(["rendered_html", "report_id"]).where("id", "=", id).executeTakeFirst();
|
|
350
|
+
if (!r || r.rendered_html === null) return null;
|
|
351
|
+
return { html: r.rendered_html, reportId: r.report_id };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export {
|
|
355
|
+
mirrorSiteField,
|
|
356
|
+
mirrorSiteFields,
|
|
357
|
+
mirrorSiteInsert,
|
|
358
|
+
mirrorHealthFields,
|
|
359
|
+
mirrorScheduleFields,
|
|
360
|
+
getSiteBySlug,
|
|
361
|
+
getSiteById,
|
|
362
|
+
listSites,
|
|
363
|
+
REPORT_LIST_COLUMNS,
|
|
364
|
+
listAllReports,
|
|
365
|
+
listReportsForSite,
|
|
366
|
+
mirrorReportPatch,
|
|
367
|
+
mirrorReportInsert,
|
|
368
|
+
storeRenderedHtml,
|
|
369
|
+
getReportById,
|
|
370
|
+
getReportHtml
|
|
371
|
+
};
|
|
372
|
+
//# sourceMappingURL=chunk-GQDDWYU2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/fleet-state.ts"],"sourcesContent":["/** Phase 2 of the Airtable → Turso migration (#539): the fleet-state READ layer.\n *\n * Returns the exact `WebsiteRow` shape the Airtable module returns, so every\n * repoint is an import-only swap at a composition root — the same trick\n * listSubmissionsForSite used for the hybrid-db cutover. The raw values live\n * in `sites`/`site_health`/`site_schedule` (written by the importer / hourly\n * sync); coercion to `WebsiteRow` happens HERE, reusing the Airtable module's\n * own exported coercers (`toVerdict`, `toFrequency`, `parseNotifyRouting`,\n * `parseSecurityAdvisories`, `trimToNull`) so there is one truth for each.\n *\n * The equivalence instrument (tests/db/fleet-state.test.ts) pins this module\n * to `mapRow` field-by-field: for a fixture record, `mapRow(rec)` must deep-\n * equal the row read back through here after an import. A new WebsiteRow\n * field fails that test until this module carries it.\n *\n * `headerImage` is the ONE deliberate exception (design D5): Airtable stopped\n * being its source — the bytes belong in `sites.header_image*`. Those columns\n * ARE now written: the header-image CLI dual-writes on every generation and the\n * one-shot backfill copied the rest, so as of 2026-08-25 production carries a\n * BLOB for 12 of the 13 maintained sites (the 13th, LA Homelessness Youth, has\n * no header image in Airtable either — its reports are blocked at approve for\n * exactly that). An earlier version of this comment said the columns were empty\n * fleet-wide; that was true when written and is no longer.\n *\n * The `url` is \"\" because the bytes live in the row itself, not behind a signed\n * URL — which is why `url` is NOT a usable handle. A consumer that needs the\n * image calls `loadHeaderImage(db, siteId)` for the bytes; the send path still\n * fetches the Airtable attachment, and moving it over is its own change.\n */\nimport { sql, type Selectable, type Updateable } from \"kysely\";\nimport type { Db } from \"./client.js\";\nimport type { ReportsTable, SiteHealthTable, SiteScheduleTable, SitesTable } from \"./schema.js\";\nimport {\n SITE_FIELDS,\n siteValueFor,\n healthColumnFor,\n scheduleColumnFor,\n mapReportRecord,\n mapWebsiteRecord,\n type RawRecord,\n} from \"./import-airtable.js\";\nimport type { AirtableCellValue } from \"../reports/airtable/websites.js\";\nimport {\n toReportType,\n parseAutoEvidence,\n type ReportRow,\n type DeliveryStatus,\n} from \"../reports/airtable/reports.js\";\nimport { MAINTENANCE_CHECKLIST, TESTING_CHECKLIST } from \"../reports/checklist.js\";\nimport type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { canonicalizeStatus } from \"../reports/airtable/site-status.js\";\nimport {\n parseNotifyRouting,\n parseSecurityAdvisories,\n toFrequency,\n toPrismicModelsVerdict,\n toVerdict,\n trimToNull,\n} from \"../reports/airtable/websites.js\";\n\ntype JoinedRow = Record<string, unknown>;\n\nconst str = (v: unknown): string | null => (typeof v === \"string\" ? v : null);\nconst num = (v: unknown): number | null => (typeof v === \"number\" ? v : null);\n/** Stored 1/0/null (importer's b01n) → boolean | null. */\nconst bool = (v: unknown): boolean | null => (typeof v === \"number\" ? v !== 0 : null);\n\n/** Stored JSON array (importer-normalized) → the trimmed string[] mapRow yields. */\nfunction parseAwc(raw: unknown): string[] {\n if (typeof raw !== \"string\" || raw.trim() === \"\") return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed\n .filter((x): x is string => typeof x === \"string\")\n .map((s) => s.trim())\n .filter(Boolean);\n } catch {\n return [];\n }\n}\n\nfunction rowFromJoined(r: JoinedRow): WebsiteRow {\n const name = String(r.name ?? \"\");\n const headerFilename = str(r.header_image_filename);\n const headerType = str(r.header_image_type);\n return {\n id: String(r.id),\n name,\n url: str(r.url) ?? \"\",\n // `sites.status` holds the RAW Airtable cell (the importer stores it verbatim\n // so the hourly parity check compares raw-to-raw). Canonicalizing HERE — the\n // twin of mapRow's seam — is what keeps the #558 equivalence instrument green.\n status: canonicalizeStatus(r.status),\n statusRaw: str(r.status),\n pointOfContact: str(r.point_of_contact),\n maintenanceFreq: toFrequency(r.maintenance_freq, `${name} maintenance`),\n testingFreq: toFrequency(r.testing_freq, `${name} testing`),\n maintenanceFreqRaw: str(r.maintenance_freq),\n testingFreqRaw: str(r.testing_freq),\n maintenanceDay: str(r.maintenance_day),\n testingDay: str(r.testing_day),\n ga4PropertyId: str(r.ga4_property_id),\n searchQuery: str(r.search_query),\n searchConsoleProperty: str(r.search_console_property),\n analyticsSoftFailAt: str(r.analytics_soft_fail_at),\n gitRepo: str(r.git_repo),\n reportRecipientsTo: str(r.report_recipients_to),\n reportRecipientsCc: str(r.report_recipients_cc),\n acceptedWatchConditions: parseAwc(r.accepted_watch_conditions),\n headerImage:\n headerFilename !== null && headerType !== null\n ? { url: \"\", filename: headerFilename, type: headerType }\n : null,\n pScore: num(r.p_score),\n rScore: num(r.r_score),\n bpScore: num(r.bp_score),\n seoScore: num(r.seo_score),\n lastLighthouseAuditAt: str(r.lighthouse_at),\n a11yViolations: num(r.a11y_violations),\n depsDrifted: num(r.deps_drifted),\n depsMajorBehind: num(r.deps_major_behind),\n depsOutdated: num(r.deps_outdated),\n depsMajorOutdated: num(r.deps_major_outdated),\n securityVulnsCritical: num(r.vulns_critical),\n securityVulnsHigh: num(r.vulns_high),\n securityVulnsModerate: num(r.vulns_moderate),\n securityVulnsLow: num(r.vulns_low),\n securityAutoFixAttempts: num(r.auto_fix_attempts),\n lastSecurityAuditAt: str(r.security_audit_at),\n securityAdvisories: parseSecurityAdvisories(r.security_advisories),\n certDaysRemaining: num(r.cert_days_remaining),\n domainCheckedAt: str(r.domain_checked_at),\n netlifyId: trimToNull(r.netlify_id),\n deployStatus: str(r.deploy_status),\n lastDeployAt: str(r.last_deploy_at),\n deployLogUrl: str(r.deploy_log_url),\n deployCheckedAt: str(r.deploy_checked_at),\n functionHealth: toVerdict(r.function_health),\n cmsReachable: toVerdict(r.cms_reachable),\n turnstileWidget: toVerdict(r.turnstile_widget),\n functionHealthCheckedAt: str(r.function_health_checked_at),\n crossbrowserOk: bool(r.crossbrowser_ok),\n mobileOk: bool(r.mobile_ok),\n linksOk: bool(r.links_ok),\n brokenLinks: num(r.broken_links),\n browserCheckedAt: str(r.browser_checked_at),\n reachableOk: toVerdict(r.uptime_reachable),\n titleMetaOk: toVerdict(r.titles_meta_ok),\n copyIntro: trimToNull(r.copy_intro),\n copyContact: trimToNull(r.copy_contact),\n copyFooter: trimToNull(r.copy_footer),\n launchedAt: str(r.launched_at),\n newsletterWebhook: trimToNull(r.newsletter_webhook),\n notifyRouting: parseNotifyRouting(r.notify_routing),\n notifyRoutingRaw: trimToNull(r.notify_routing),\n mailchimpApiKey: trimToNull(r.mailchimp_api_key),\n mailchimpAudienceId: trimToNull(r.mailchimp_audience_id),\n requireTurnstile: typeof r.require_turnstile === \"number\" ? r.require_turnstile !== 0 : false,\n renovateFailingCis: num(r.renovate_failing_cis),\n defaultBranchCi: str(r.default_branch_ci),\n lastCommitAt: str(r.last_commit_at),\n githubSignalsAt: str(r.github_signals_at),\n smokeOk: toVerdict(r.smoke_ok),\n lastSmokeAt: str(r.last_smoke_at),\n formE2eOk: toVerdict(r.form_e2e_ok),\n formE2eCheckedAt: str(r.form_e2e_checked_at),\n prismicModels: toPrismicModelsVerdict(r.prismic_models),\n prismicModelsCheckedAt: str(r.prismic_models_checked_at),\n prismicModelsDrift: str(r.prismic_models_drift),\n prismicAckUntil: str(r.prismic_ack_until),\n nextMaintenanceAt: str(r.next_maintenance_at),\n nextTestingAt: str(r.next_testing_at),\n };\n}\n\n/** Every sites column EXCEPT the header-image BLOB. Since the 2026-08-24\n * backfill, header_image holds 0.7–3.5 MB JPEGs — a selectAll would haul\n * megabytes into every site lookup (per LEAD on the ingest path, ×44 on the\n * fleet list) and Turso bills the bytes. WebsiteRow only needs the metadata\n * columns; the bytes have their own dedicated readers when a consumer wants\n * them. Kept in lockstep with SitesTable by the blob-exclusion test. */\nconst SITE_COLUMNS = [\n \"sites.id\",\n \"sites.slug\",\n \"sites.name\",\n \"sites.url\",\n \"sites.status\",\n \"sites.point_of_contact\",\n \"sites.maintenance_freq\",\n \"sites.testing_freq\",\n \"sites.maintenance_day\",\n \"sites.testing_day\",\n \"sites.ga4_property_id\",\n \"sites.search_query\",\n \"sites.search_console_property\",\n \"sites.git_repo\",\n \"sites.netlify_id\",\n \"sites.report_recipients_to\",\n \"sites.report_recipients_cc\",\n \"sites.copy_intro\",\n \"sites.copy_contact\",\n \"sites.copy_footer\",\n \"sites.newsletter_webhook\",\n \"sites.mailchimp_api_key\",\n \"sites.mailchimp_audience_id\",\n \"sites.notify_routing\",\n \"sites.require_turnstile\",\n \"sites.accepted_watch_conditions\",\n \"sites.prismic_ack_until\",\n \"sites.launched_at\",\n \"sites.header_image_filename\",\n \"sites.header_image_type\",\n \"sites.header_image_generated_at\",\n \"sites.legacy\",\n] as const;\n\n/** The three-table join every read composes from. site_health/site_schedule\n * rows are upserted alongside sites by the importer, but LEFT JOIN anyway —\n * a site row must never vanish from a surface because a health row is absent. */\nfunction joined(db: Db) {\n return db\n .selectFrom(\"sites\")\n .leftJoin(\"site_health\", \"site_health.site_id\", \"sites.id\")\n .leftJoin(\"site_schedule\", \"site_schedule.site_id\", \"sites.id\")\n .select(SITE_COLUMNS)\n .selectAll([\"site_health\", \"site_schedule\"]);\n}\n\n/** Write-through for the site-detail editor (Phase 2): after the Airtable\n * write (still the Phase 2 source of truth), mirror the same cell into\n * `sites` so a Turso-reading page renders the edit immediately instead of\n * waiting for the next hourly sync. The column mapping is the IMPORTER's own\n * map — one truth — and the value gets the importer's empty-clears-to-null\n * semantics. Throws on a column the importer doesn't claim (the lockstep\n * test makes that unreachable for the editor's allowlist); the caller treats\n * a mirror failure as non-fatal — the hourly sync converges it.\n *\n * Handles the non-text columns too (`Require Turnstile` is a checkbox,\n * `Accepted Watch Conditions` a multi-select): the coercion is NOT repeated\n * here, it is delegated to the importer's own `siteValueFor`. That delegation\n * is the whole safety property — parity compares raw-to-raw, so a mirror that\n * stored `\"true\"` where the importer stores `1` would red every hourly run. */\nexport async function mirrorSiteField(\n db: Db,\n siteId: string,\n airtableColumn: string,\n value: AirtableCellValue,\n): Promise<void> {\n await mirrorSiteFields(db, siteId, { [airtableColumn]: value });\n}\n\n/** The multi-column form, and where the work actually happens (#539 Phase 5).\n *\n * `updateLaunched` is why it exists: it flips `Status` AND stamps `Launched at`\n * in one Airtable update, and mirroring those as two separate UPDATEs would\n * open a window where Turso says a site is maintained but never launched.\n *\n * Same contract as {@link mirrorHealthFields}, deliberately — it takes the\n * EXACT FieldSet just written to Airtable (the writers return it, so the mirror\n * cannot carry a different payload), and returns whether a `sites` row matched:\n * a site the hourly sync hasn't imported yet updates 0 rows → false, so the\n * caller can count it honestly instead of claiming it mirrored. An empty\n * FieldSet runs no SQL and returns true — nothing to mirror is not a miss. */\nexport async function mirrorSiteFields(\n db: Db,\n siteId: string,\n fields: Record<string, unknown>,\n): Promise<boolean> {\n const patch: Record<string, string | number | null> = {};\n for (const [airtableColumn, value] of Object.entries(fields)) {\n const col = SITE_FIELDS[airtableColumn];\n if (!col)\n throw new Error(`mirrorSiteFields: importer claims no sites column for '${airtableColumn}'`);\n // Empty text still clears to null (the importer's `s()` does the same); the\n // non-text columns get their own coercion inside siteValueFor.\n patch[col] = siteValueFor(col, value);\n }\n if (Object.keys(patch).length === 0) return true;\n const res = await db\n .updateTable(\"sites\")\n .set(patch as Updateable<SitesTable>)\n .where(\"id\", \"=\", siteId)\n .executeTakeFirst();\n // kysely/libSQL reports numUpdatedRows as a BigInt — compare in BigInt.\n return res.numUpdatedRows > 0n;\n}\n\n/** Mirror a NEWLY CREATED Airtable Websites record into Turso (#539 Phase 5).\n *\n * `ensure-site` CREATES a row, and every other site mirror is an UPDATE, which\n * does nothing at all for a row that does not exist yet — so a site\n * bootstrapped at 09:05 was invisible until the 09:20 sync, and every mirror\n * the rest of the bootstrap fired reported `mirrored=missed` with nothing to\n * update.\n *\n * Maps with the IMPORTER's own `mapWebsiteRecord`, which is also exactly what\n * parity diffs against: that delegation is what makes the mirrored rows\n * parity-clean by construction rather than by a column list someone has to\n * remember to extend.\n *\n * All THREE rows, not just `sites`. Parity reverse-checks `site_health` and\n * `site_schedule` per site and reports a missing one as `(row) ABSENT`, and a\n * later `mirrorHealthFields` would return `missed` forever with no row to hit.\n *\n * Upserts rather than inserts because `ensure-site` is re-run to RESUME a\n * bootstrap. The header_image* columns survive that by construction —\n * `mapWebsiteRecord` does not carry them (Airtable stopped being their source,\n * design D5), so the conflict branch cannot blank a stored plate whose bytes\n * live in no other store. */\nexport async function mirrorSiteInsert(db: Db, rec: RawRecord, computedAt: string): Promise<void> {\n const { site, health, schedule } = mapWebsiteRecord(rec, computedAt);\n await db\n .insertInto(\"sites\")\n .values(site)\n .onConflict((oc) => oc.column(\"id\").doUpdateSet(site))\n .execute();\n await db\n .insertInto(\"site_health\")\n .values(health)\n .onConflict((oc) => oc.column(\"site_id\").doUpdateSet(health))\n .execute();\n await db\n .insertInto(\"site_schedule\")\n .values(schedule)\n .onConflict((oc) => oc.column(\"site_id\").doUpdateSet(schedule))\n .execute();\n}\n\n/** Write-through mirror for the nightly health writers (#539 Phase 3). Takes\n * the EXACT FieldSet just written to Airtable (updateAuditFields /\n * updateGitHubSignals return it), so the mirror can never carry a different\n * payload than the Airtable write it shadows. Resolution + coercion come from\n * the importer's healthColumnFor — one truth; an Airtable column no\n * site_health column claims throws (same contract as mirrorSiteField).\n * Partial by design: absent fields stay untouched, matching\n * updateGitHubSignals' deliberate omission of a null lastCommitAt. Returns\n * whether a site_health row matched: a site the hourly sync hasn't imported\n * yet updates 0 rows → false, so the caller can count it honestly\n * (mirror_missed) instead of claiming it mirrored — it still converges on the\n * next sync, like every mirror. An empty FieldSet runs no SQL and returns\n * true: nothing to mirror is not a miss. */\nexport async function mirrorHealthFields(\n db: Db,\n siteId: string,\n fields: Record<string, unknown>,\n): Promise<boolean> {\n // Per-column value types (number vs text) are guaranteed by healthColumnFor's\n // coercion — the numeric/text split lives there, once — so the patch builds\n // untyped and casts at the .set() boundary (the importer's own idiom).\n const patch: Record<string, string | number | null> = {};\n for (const [field, value] of Object.entries(fields)) {\n const m = healthColumnFor(field);\n if (!m)\n throw new Error(`mirrorHealthFields: importer claims no site_health column for '${field}'`);\n patch[m.col] = m.coerce(value);\n }\n if (Object.keys(patch).length === 0) return true;\n const res = await db\n .updateTable(\"site_health\")\n .set(patch as Updateable<SiteHealthTable>)\n .where(\"site_id\", \"=\", siteId)\n .executeTakeFirst();\n // kysely/libSQL reports numUpdatedRows as a BigInt — compare in BigInt.\n return res.numUpdatedRows > 0n;\n}\n\n/** The site_schedule twin of {@link mirrorHealthFields}, for the nightly\n * next-due write-back. `computedAt` stamps when THIS computation ran — the\n * hourly sync overwrites it with its own import stamp, same as every mirrored\n * value. Empty fields → full no-op (no lone computed_at stamp for a write\n * that carried nothing). Same return contract as mirrorHealthFields: false\n * when the UPDATE matched no site_schedule row (site not yet imported), true\n * otherwise — including the empty-fields no-op. */\nexport async function mirrorScheduleFields(\n db: Db,\n siteId: string,\n fields: Record<string, unknown>,\n computedAt: string,\n): Promise<boolean> {\n const patch: Record<string, string | null> = {};\n for (const [field, value] of Object.entries(fields)) {\n const m = scheduleColumnFor(field);\n if (!m)\n throw new Error(\n `mirrorScheduleFields: importer claims no site_schedule column for '${field}'`,\n );\n patch[m.col] = m.coerce(value);\n }\n if (Object.keys(patch).length === 0) return true;\n patch.computed_at = computedAt;\n const res = await db\n .updateTable(\"site_schedule\")\n .set(patch as Updateable<SiteScheduleTable>)\n .where(\"site_id\", \"=\", siteId)\n .executeTakeFirst();\n return res.numUpdatedRows > 0n;\n}\n\n/** Same contract as the Airtable getWebsiteBySlug: slug is siteSlug(Name),\n * precomputed into the UNIQUE sites.slug column at import time. */\nexport async function getSiteBySlug(db: Db, slug: string): Promise<WebsiteRow | null> {\n const r = await joined(db).where(\"sites.slug\", \"=\", slug).executeTakeFirst();\n return r ? rowFromJoined(r as JoinedRow) : null;\n}\n\n/** By Airtable rec id (the PK) — approve-report's lookup shape. */\nexport async function getSiteById(db: Db, id: string): Promise<WebsiteRow | null> {\n const r = await joined(db).where(\"sites.id\", \"=\", id).executeTakeFirst();\n return r ? rowFromJoined(r as JoinedRow) : null;\n}\n\n/** Same contract as the Airtable listWebsites: every site, one row each.\n * Name-ordered for determinism (Airtable returned table order; no consumer\n * is order-sensitive — the cockpit groups and sorts itself). */\nexport async function listSites(db: Db): Promise<WebsiteRow[]> {\n const rows = await joined(db).orderBy(\"sites.name\").execute();\n return rows.map((r) => rowFromJoined(r as JoinedRow));\n}\n\n// ————————————————————————— reports —————————————————————————\n\n/** stable checklist key → Airtable column name. The importer stores stable keys\n * (mapReportRecord); `ReportRow.checklist` exposes Airtable column names — one\n * derived map, built from the checklist definitions themselves. */\nconst CHECKLIST_FIELD_BY_KEY: ReadonlyMap<string, string> = new Map(\n [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map((i) => [i.key, i.field]),\n);\n\nfunction checklistFromStored(raw: string | null): Record<string, boolean> {\n let stored: Record<string, unknown> = {};\n if (typeof raw === \"string\" && raw.trim() !== \"\") {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed))\n stored = parsed as Record<string, unknown>;\n } catch {\n // fall through to all-false — a bad blob must not throw the page\n }\n }\n const out: Record<string, boolean> = {};\n for (const [key, field] of CHECKLIST_FIELD_BY_KEY) out[field] = Boolean(stored[key]);\n return out;\n}\n\n/**\n * `hasRenderedHtml` is passed separately rather than read off `r`, because the\n * list read deliberately does not fetch the body (see REPORT_LIST_COLUMNS) and\n * only ever needed its null-ness. Taking it as an argument means a list row and a\n * full row are both honest inputs, and nothing here can start depending on body\n * CONTENT without the type changing to say so.\n */\nfunction reportRowFromDb(\n r: Omit<Selectable<ReportsTable>, \"rendered_html\">,\n hasRenderedHtml: boolean,\n): ReportRow {\n const p = r.lighthouse_performance;\n const a = r.lighthouse_accessibility;\n const b = r.lighthouse_best_practices;\n const seo = r.lighthouse_seo;\n const lighthouse =\n typeof p === \"number\" &&\n typeof a === \"number\" &&\n typeof b === \"number\" &&\n typeof seo === \"number\"\n ? { performance: p, accessibility: a, bestPractices: b, seo }\n : null;\n return {\n id: r.id,\n reportId: r.report_id ?? \"\",\n siteId: r.site_id ?? \"\",\n reportType: toReportType(r.report_type ?? undefined),\n period: r.period,\n periodStart: r.period_start,\n periodEnd: r.period_end,\n completedOn: r.completed_on,\n lighthouse,\n gaUsersCurrent: r.ga_users_current,\n gaUsersPrevious: r.ga_users_previous,\n searchFoundPage1: bool(r.search_found_page1),\n searchPosition: r.search_position,\n lastTestedDate: r.last_tested_date,\n commentary: r.commentary,\n subjectOverride: r.subject_override,\n draftReady: r.draft_ready !== 0,\n approvedToSend: r.approved_to_send !== 0,\n sentAt: r.sent_at,\n approvedAt: r.approved_at,\n approvedBy: r.approved_by,\n deliveryStatus: (r.delivery_status ?? \"pending\") as DeliveryStatus,\n // The body lives IN the row (rendered_html) — the link points at the\n // dashboard's own preview route instead of an EXPIRING Airtable signed URL.\n // Strictly better for the operator: the old link 404'd once the URL aged out.\n renderedHtmlAttachment: hasRenderedHtml\n ? { url: `/api/reports/${r.id}/preview`, filename: `${r.report_id ?? r.id}.html` }\n : null,\n resendMessageId: r.resend_message_id,\n checklist: checklistFromStored(r.checklist),\n autoEvidence: parseAutoEvidence(r.checklist_auto_evidence),\n sendOverride: r.send_override !== 0,\n overrideReason: r.override_reason,\n overrideBy: r.override_by,\n overrideAt: r.override_at,\n };\n}\n\n/** Every reports column EXCEPT the rendered_html body — the exact hazard\n * SITE_COLUMNS guards against on `sites`, on the table where it currently costs\n * more. Live, all 16 reports carry a body: 1.17 MB total, avg 73 KB. A list read\n * wants the body only to know WHETHER there is one, so `has_rendered_html`\n * computes that in SQL and the bytes stay in the database. Kept in lockstep with\n * ReportsTable by the body-exclusion test. */\nexport const REPORT_LIST_COLUMNS = [\n \"id\",\n \"site_id\",\n \"report_id\",\n \"report_type\",\n \"period\",\n \"period_start\",\n \"period_end\",\n \"completed_on\",\n \"lighthouse_performance\",\n \"lighthouse_accessibility\",\n \"lighthouse_best_practices\",\n \"lighthouse_seo\",\n \"ga_users_current\",\n \"ga_users_previous\",\n \"search_found_page1\",\n \"search_position\",\n \"last_tested_date\",\n \"commentary\",\n \"subject_override\",\n \"draft_ready\",\n \"approved_to_send\",\n \"approved_at\",\n \"approved_by\",\n \"send_override\",\n \"override_reason\",\n \"override_by\",\n \"override_at\",\n \"sent_at\",\n \"delivery_status\",\n \"resend_message_id\",\n \"checklist\",\n \"checklist_auto_evidence\",\n] as const;\n\n/** The one thing a list read still needs from the body: whether there IS one.\n * Computed in SQLite so the bytes never cross the wire. */\nconst HAS_RENDERED_HTML = sql<number>`(rendered_html is not null)`.as(\"has_rendered_html\");\n\n/** Same contract as the Airtable listAllReports. Newest-period-first (Airtable\n * returned manual table order; every consumer filters/sorts itself). */\nexport async function listAllReports(db: Db): Promise<ReportRow[]> {\n const rows = await db\n .selectFrom(\"reports\")\n .select(REPORT_LIST_COLUMNS)\n .select(HAS_RENDERED_HTML)\n .orderBy(\"period_start\", \"desc\")\n .orderBy(\"id\")\n .execute();\n return rows.map((r) => reportRowFromDb(r, r.has_rendered_html !== 0));\n}\n\n/** Same contract as the Airtable listReportsForSite — served by idx_reports_site.\n * Body-free for the same reason as listAllReports: the site-detail page renders\n * a link, never the HTML. */\nexport async function listReportsForSite(db: Db, siteId: string): Promise<ReportRow[]> {\n const rows = await db\n .selectFrom(\"reports\")\n .select(REPORT_LIST_COLUMNS)\n .select(HAS_RENDERED_HTML)\n .where(\"site_id\", \"=\", siteId)\n .orderBy(\"period_start\", \"desc\")\n .orderBy(\"id\")\n .execute();\n return rows.map((r) => reportRowFromDb(r, r.has_rendered_html !== 0));\n}\n\n/** Columns a report writer mirrors after its Airtable write (same pattern as\n * mirrorSiteField): the approve/override flow, the resend-webhook's delivery\n * status, and — since #539 Phase 5 — the drafting path's queue flag and a\n * re-run's refreshed scores.\n *\n * `rendered_html` stays deliberately OUT even though the drafting path now\n * writes it: bodies go through `storeRenderedHtml`, so a request handler\n * holding a patch cannot accidentally write megabytes of HTML. `sent_at` and\n * `resend_message_id` stay out because nothing but the send batch writes them\n * and the hourly sync converges those. */\nexport type ReportMirrorPatch = Partial<\n Pick<\n ReportsTable,\n | \"approved_to_send\"\n | \"approved_at\"\n | \"approved_by\"\n | \"send_override\"\n | \"override_reason\"\n | \"override_by\"\n | \"override_at\"\n | \"delivery_status\"\n // Phase 4 report review: the console edits commentary and re-renders the\n // page immediately after the write, so the mirror has to carry it or the\n // operator sees their own save as a no-op until the next hourly sync.\n | \"commentary\"\n // Phase 5 drafting path. `draft_ready` is the queue flag `queueDraft`\n // writes — for the new draft AND for every row it supersedes, so without it\n // the console shows a site with two queued reports until the next sync.\n | \"draft_ready\"\n // A re-run (announce/launch reuse) refreshes an existing row's scores so the\n // eventually-sent email is not stale; the console reads the same numbers.\n | \"lighthouse_performance\"\n | \"lighthouse_accessibility\"\n | \"lighthouse_best_practices\"\n | \"lighthouse_seo\"\n | \"ga_users_current\"\n | \"ga_users_previous\"\n | \"search_found_page1\"\n | \"search_position\"\n | \"completed_on\"\n >\n>;\n\n/** Mirror an Airtable report write into Turso so the page re-render after an\n * approve/override/bounce shows the new state immediately instead of after\n * the next hourly sync. Callers treat a failure as non-fatal (the sync\n * converges it); an empty patch is a no-op, never invalid SQL. */\nexport async function mirrorReportPatch(\n db: Db,\n reportId: string,\n patch: ReportMirrorPatch,\n): Promise<void> {\n if (Object.keys(patch).length === 0) return;\n await db.updateTable(\"reports\").set(patch).where(\"id\", \"=\", reportId).execute();\n}\n\n/** Mirror a NEWLY CREATED Airtable Reports record into Turso (#539 Phase 5).\n *\n * Every other report mirror is an UPDATE, which silently does nothing for a row\n * that does not exist yet — so a draft created at 09:05 was invisible to the\n * Turso-backed console until the 09:20 sync. Takes the raw record Airtable\n * echoed back from the create, and maps it with the IMPORTER's own\n * `mapReportRecord`: parity diffs Turso against exactly that function, so\n * delegating is what makes the mirrored row parity-clean by construction rather\n * than by a column list someone has to remember to extend.\n *\n * Upsert, not insert, and the conflict branch drops `rendered_html` for the\n * same reason the importer's does: the body is written later by a separate\n * sharp-bearing step, so a re-mirror carrying null would blank a render that\n * had already succeeded. */\nexport async function mirrorReportInsert(db: Db, rec: RawRecord): Promise<void> {\n const row = mapReportRecord(rec, null);\n const { rendered_html: _rh, ...rowSansHtml } = row;\n void _rh;\n await db\n .insertInto(\"reports\")\n .values(row)\n .onConflict((oc) => oc.column(\"id\").doUpdateSet(rowSansHtml))\n .execute();\n}\n\n/**\n * Store a freshly rendered report body, replacing whatever was there.\n *\n * Deliberately NOT part of `ReportMirrorPatch`: that patch is the request path's\n * write-through (approve, override, delivery status), and a rendered body is\n * produced by a batch job with sharp, never by a dashboard POST. Keeping it out\n * means a handler cannot accidentally write megabytes of HTML from a request.\n *\n * It REPLACES unconditionally. The importer's `when-missing` mode skips a report\n * that already has a body — correct for an import, wrong here: a refresh whose\n * entire purpose is showing the newest commentary must overwrite.\n */\nexport async function storeRenderedHtml(db: Db, reportId: string, html: string): Promise<void> {\n await db.updateTable(\"reports\").set({ rendered_html: html }).where(\"id\", \"=\", reportId).execute();\n}\n\n/** By rec id (the PK) — approve-report's read. */\nexport async function getReportById(db: Db, id: string): Promise<ReportRow | null> {\n const r = await db.selectFrom(\"reports\").selectAll().where(\"id\", \"=\", id).executeTakeFirst();\n return r ? reportRowFromDb(r, r.rendered_html !== null) : null;\n}\n\n/** The preview route's read: the stored rendered body, or null when the report\n * has none (imported while its signed URL was expired, or predates rendering). */\nexport async function getReportHtml(\n db: Db,\n id: string,\n): Promise<{ html: string; reportId: string | null } | null> {\n const r = await db\n .selectFrom(\"reports\")\n .select([\"rendered_html\", \"report_id\"])\n .where(\"id\", \"=\", id)\n .executeTakeFirst();\n if (!r || r.rendered_html === null) return null;\n return { html: r.rendered_html, reportId: r.report_id };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
queueDraft
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import {
|
|
5
|
-
generateHeaderImage
|
|
6
|
-
} from "./chunk-5UINJNAJ.js";
|
|
7
|
-
import {
|
|
8
|
-
renderReportHtml,
|
|
9
|
-
resolveCopy
|
|
10
|
-
} from "./chunk-CQR4LIM7.js";
|
|
11
|
-
import {
|
|
12
|
-
uploadAttachment
|
|
13
|
-
} from "./chunk-Z7AXYKQB.js";
|
|
3
|
+
} from "./chunk-YFAXOD7Z.js";
|
|
14
4
|
import {
|
|
15
5
|
createDraft,
|
|
16
6
|
listReportsForSite
|
|
17
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-F3JWOPTG.js";
|
|
18
8
|
import {
|
|
19
9
|
checklistFor,
|
|
20
10
|
gatingFields
|
|
21
11
|
} from "./chunk-VAD43MOR.js";
|
|
22
12
|
import {
|
|
23
|
-
|
|
24
|
-
} from "./chunk-
|
|
13
|
+
uploadAttachment
|
|
14
|
+
} from "./chunk-IY6256GK.js";
|
|
15
|
+
import {
|
|
16
|
+
generateHeaderImage
|
|
17
|
+
} from "./chunk-5UINJNAJ.js";
|
|
18
|
+
import {
|
|
19
|
+
renderReportHtml,
|
|
20
|
+
resolveCopy
|
|
21
|
+
} from "./chunk-KJGKZLCT.js";
|
|
25
22
|
import {
|
|
26
23
|
siteSlug,
|
|
27
24
|
updateAnalyticsHealth
|
|
28
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-7NYN2P6I.js";
|
|
26
|
+
import {
|
|
27
|
+
isNetlifyAppUrl
|
|
28
|
+
} from "./chunk-RARREDQE.js";
|
|
29
29
|
import {
|
|
30
30
|
defaultCredentialsPath
|
|
31
31
|
} from "./chunk-6VAL7XJT.js";
|
|
@@ -583,22 +583,23 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
583
583
|
if (base === null) throw new Error("base required when previewOnly=false");
|
|
584
584
|
if (readGaConfig() !== null && Boolean(siteRow.ga4PropertyId || siteRow.searchQuery)) {
|
|
585
585
|
try {
|
|
586
|
-
await updateAnalyticsHealth(
|
|
586
|
+
const fields = await updateAnalyticsHealth(
|
|
587
587
|
base,
|
|
588
588
|
siteRow.id,
|
|
589
589
|
softFailures.length > 0 ? today.toISOString() : null
|
|
590
590
|
);
|
|
591
|
+
await options.siteMirror?.health(siteRow.id, fields);
|
|
591
592
|
} catch (e) {
|
|
592
593
|
console.warn(`\u26A0 analytics-health write skipped for ${siteRow.name}: ${e.message}`);
|
|
593
594
|
}
|
|
594
595
|
}
|
|
595
596
|
if (options.completeRowId) {
|
|
596
|
-
await uploadDraftHtml(options.completeRowId, slug, periodEnd, html);
|
|
597
|
-
const outcome2 = await queueDraft(
|
|
598
|
-
|
|
599
|
-
siteId: siteRow.id,
|
|
600
|
-
|
|
601
|
-
|
|
597
|
+
await uploadDraftHtml(options.completeRowId, slug, periodEnd, html, options.reportMirror);
|
|
598
|
+
const outcome2 = await queueDraft(
|
|
599
|
+
base,
|
|
600
|
+
{ id: options.completeRowId, siteId: siteRow.id, reportType },
|
|
601
|
+
options.reportMirror
|
|
602
|
+
);
|
|
602
603
|
return {
|
|
603
604
|
reportRow: options.existingRow ?? null,
|
|
604
605
|
htmlPath: null,
|
|
@@ -614,28 +615,32 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
614
615
|
const checklistTicks = [...evidence.entries()].filter(([, e]) => e.result === "pass").map(([field]) => field);
|
|
615
616
|
const autoEvidence = Object.fromEntries(evidence);
|
|
616
617
|
const reportId = `${siteRow.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
|
|
617
|
-
const created = await createDraft(
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
618
|
+
const created = await createDraft(
|
|
619
|
+
base,
|
|
620
|
+
{
|
|
621
|
+
reportId,
|
|
622
|
+
siteId: siteRow.id,
|
|
623
|
+
reportType,
|
|
624
|
+
period: options.period ?? periodEnd.toISOString().slice(0, 7),
|
|
625
|
+
periodStart,
|
|
626
|
+
periodEnd,
|
|
627
|
+
completedOn,
|
|
628
|
+
lighthouse: scores,
|
|
629
|
+
lastTestedDate,
|
|
630
|
+
...gaUsers ? { gaUsersCurrent: gaUsers.current, gaUsersPrevious: gaUsers.previous } : {},
|
|
631
|
+
...search ? { searchFoundPage1: search.foundOnPage1 } : {},
|
|
632
|
+
...search?.foundOnPage1 && search.position !== null ? { searchPosition: search.position } : {},
|
|
633
|
+
checklistTicks,
|
|
634
|
+
autoEvidence
|
|
635
|
+
},
|
|
636
|
+
options.reportMirror?.created
|
|
637
|
+
);
|
|
638
|
+
await uploadDraftHtml(created.id, slug, periodEnd, html, options.reportMirror);
|
|
639
|
+
const outcome = await queueDraft(
|
|
640
|
+
base,
|
|
641
|
+
{ id: created.id, siteId: siteRow.id, reportType },
|
|
642
|
+
options.reportMirror
|
|
643
|
+
);
|
|
639
644
|
return {
|
|
640
645
|
reportRow: created,
|
|
641
646
|
htmlPath: null,
|
|
@@ -647,9 +652,10 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
647
652
|
supersededIds: outcome.supersededIds
|
|
648
653
|
};
|
|
649
654
|
}
|
|
650
|
-
async function uploadDraftHtml(rowId, slug, periodEnd, html) {
|
|
655
|
+
async function uploadDraftHtml(rowId, slug, periodEnd, html, mirror) {
|
|
651
656
|
const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
652
657
|
await uploadAttachment(rowId, "Rendered HTML", html, htmlFilename, "text/html");
|
|
658
|
+
await mirror?.body(rowId, html);
|
|
653
659
|
}
|
|
654
660
|
var NO_ENRICHMENT = { value: null, softFailed: false };
|
|
655
661
|
async function fetchGaUsers(siteRow, periodStart, periodEnd) {
|
|
@@ -735,4 +741,4 @@ export {
|
|
|
735
741
|
fetchGaUsers,
|
|
736
742
|
fetchSearch
|
|
737
743
|
};
|
|
738
|
-
//# sourceMappingURL=chunk-
|
|
744
|
+
//# sourceMappingURL=chunk-J3WEZGIN.js.map
|