@reddoorla/maintenance 0.85.2 → 0.87.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 +8 -0
- package/dist/airtable-ZBUPQJSC.js +9 -0
- package/dist/{announce-GIAYN67S.js → announce-I2Z4JN3I.js} +58 -30
- package/dist/announce-I2Z4JN3I.js.map +1 -0
- package/dist/attachments-NLZIJ5NU.js +9 -0
- package/dist/{bump-deps-SWJB6H5V.js → bump-deps-CGVELRLK.js} +9 -9
- package/dist/{chunk-SDUJIANJ.js → chunk-2NJQLSEU.js} +3 -3
- package/dist/chunk-2NQUCS7O.js +243 -0
- package/dist/chunk-2NQUCS7O.js.map +1 -0
- package/dist/{chunk-ZTPPCHDB.js → chunk-3G25KIWW.js} +4 -4
- package/dist/{chunk-DTTIFYAM.js → chunk-3JQ2FO3D.js} +44 -405
- package/dist/chunk-3JQ2FO3D.js.map +1 -0
- package/dist/chunk-3SF7D4DY.js +359 -0
- package/dist/chunk-3SF7D4DY.js.map +1 -0
- package/dist/{chunk-37S6L6G4.js → chunk-5UINJNAJ.js} +72 -2
- package/dist/chunk-5UINJNAJ.js.map +1 -0
- package/dist/{chunk-THEFTZ7F.js → chunk-62CMMGEO.js} +5 -5
- package/dist/{chunk-FDIFTIXC.js → chunk-6TWWY7ZU.js} +2 -2
- package/dist/{chunk-PBWDDDQH.js → chunk-7BZUOZPU.js} +2 -2
- package/dist/{chunk-577HCGZO.js → chunk-7NYN2P6I.js} +44 -26
- package/dist/chunk-7NYN2P6I.js.map +1 -0
- package/dist/{chunk-UYCAF77E.js → chunk-7RJ2HMMJ.js} +3 -3
- package/dist/{chunk-Q276MZ7E.js → chunk-7UAYMAKR.js} +2 -2
- package/dist/{chunk-MGR2H7F4.js → chunk-BBUSGEES.js} +58 -45
- package/dist/chunk-BBUSGEES.js.map +1 -0
- package/dist/chunk-BNM5POSJ.js +284 -0
- package/dist/chunk-BNM5POSJ.js.map +1 -0
- package/dist/{chunk-R6FPFMOS.js → chunk-BQRFQSGY.js} +3 -3
- package/dist/{chunk-NWB4O5EY.js → chunk-C6CLRKPV.js} +5 -5
- package/dist/chunk-C6CLRKPV.js.map +1 -0
- package/dist/{chunk-7J53SUT6.js → chunk-CROKUHJB.js} +4 -4
- package/dist/chunk-DEWJMMG3.js +274 -0
- package/dist/chunk-DEWJMMG3.js.map +1 -0
- package/dist/{chunk-SEEA6RXF.js → chunk-DJZWYIPP.js} +2 -2
- package/dist/chunk-DJZWYIPP.js.map +1 -0
- package/dist/chunk-EVBCK2KC.js +68 -0
- package/dist/chunk-EVBCK2KC.js.map +1 -0
- package/dist/chunk-F3JWOPTG.js +202 -0
- package/dist/chunk-F3JWOPTG.js.map +1 -0
- package/dist/{chunk-GZNBCHAZ.js → chunk-HEWSRXYV.js} +4 -4
- package/dist/chunk-HHMMUR4P.js +335 -0
- package/dist/chunk-HHMMUR4P.js.map +1 -0
- package/dist/chunk-HSI6Q7MI.js +1238 -0
- package/dist/chunk-HSI6Q7MI.js.map +1 -0
- package/dist/{chunk-MPBXOJ3N.js → chunk-IY6256GK.js} +33 -2
- package/dist/chunk-IY6256GK.js.map +1 -0
- package/dist/{chunk-LS6QPANO.js → chunk-IYSEC543.js} +2 -2
- package/dist/{chunk-DC5H7KEQ.js → chunk-JHGHLKFS.js} +2 -2
- package/dist/{chunk-4DHQCN5H.js → chunk-LBYOLBW7.js} +17 -2
- package/dist/chunk-LBYOLBW7.js.map +1 -0
- package/dist/{chunk-6QRLRT3P.js → chunk-MT2JINSS.js} +16 -265
- package/dist/chunk-MT2JINSS.js.map +1 -0
- package/dist/{chunk-TP5M2NQB.js → chunk-NCMZMLY7.js} +3 -3
- package/dist/{chunk-COZPL62X.js → chunk-NDWYJLRF.js} +24 -139
- package/dist/chunk-NDWYJLRF.js.map +1 -0
- package/dist/{chunk-3YXDSCM6.js → chunk-NXNPDCF3.js} +29 -11
- package/dist/chunk-NXNPDCF3.js.map +1 -0
- package/dist/chunk-NYUYNYQL.js +48 -0
- package/dist/chunk-NYUYNYQL.js.map +1 -0
- package/dist/chunk-P6AORG32.js +415 -0
- package/dist/chunk-P6AORG32.js.map +1 -0
- package/dist/chunk-PTDMVGKL.js +31 -0
- package/dist/chunk-PTDMVGKL.js.map +1 -0
- package/dist/{chunk-D4FVAIEJ.js → chunk-PZR4EBPY.js} +5 -2
- package/dist/chunk-PZR4EBPY.js.map +1 -0
- package/dist/{chunk-LTB6KVJW.js → chunk-SDDPUAWH.js} +13 -11
- package/dist/{chunk-LTB6KVJW.js.map → chunk-SDDPUAWH.js.map} +1 -1
- package/dist/{chunk-WYYQK4D3.js → chunk-TD2NNXLZ.js} +13 -13
- package/dist/{chunk-ZMIPG6TR.js → chunk-TVL5MVUF.js} +4 -4
- package/dist/chunk-UDRHAQT7.js +172 -0
- package/dist/chunk-UDRHAQT7.js.map +1 -0
- package/dist/chunk-VAD43MOR.js +72 -0
- package/dist/chunk-VAD43MOR.js.map +1 -0
- package/dist/chunk-VSBFM7DS.js +229 -0
- package/dist/chunk-VSBFM7DS.js.map +1 -0
- package/dist/{chunk-VRWUL4TR.js → chunk-XHHMM5HX.js} +2 -2
- package/dist/{chunk-RJB3QUSP.js → chunk-XIDUYLSR.js} +3 -3
- package/dist/{chunk-QG6OSL4M.js → chunk-XUGSZP7Z.js} +2 -2
- package/dist/{chunk-S56JQJ4F.js → chunk-Y53WOQIJ.js} +3 -3
- package/dist/{chunk-62VF32IC.js → chunk-YFAXOD7Z.js} +10 -6
- package/dist/chunk-YFAXOD7Z.js.map +1 -0
- package/dist/{chunk-ICXV25N7.js → chunk-Z3KYFUN6.js} +3 -3
- package/dist/chunk-ZBDZTJUO.js +129 -0
- package/dist/chunk-ZBDZTJUO.js.map +1 -0
- package/dist/chunk-ZGBEIWSC.js +146 -0
- package/dist/chunk-ZGBEIWSC.js.map +1 -0
- package/dist/cli/bin.js +51 -28
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +30 -1
- package/dist/cli/commands/audit.js +69 -33
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-AUE3XUPO.js +11 -0
- package/dist/{convert-to-pnpm-AT52NWZB.js → convert-to-pnpm-QFJOEP5V.js} +9 -9
- package/dist/db-UXNIZ2RG.js +217 -0
- package/dist/db-UXNIZ2RG.js.map +1 -0
- package/dist/{digest-UVHYZUYE.js → digest-FW6QMZZJ.js} +26 -22
- package/dist/{digest-UVHYZUYE.js.map → digest-FW6QMZZJ.js.map} +1 -1
- package/dist/dump-P6EBSIOH.js +68 -0
- package/dist/dump-P6EBSIOH.js.map +1 -0
- package/dist/email-KJXASVH5.js +195 -0
- package/dist/email-KJXASVH5.js.map +1 -0
- package/dist/{ensure-site-S25MDDHW.js → ensure-site-4DCEBAPM.js} +25 -16
- package/dist/ensure-site-4DCEBAPM.js.map +1 -0
- package/dist/fleet-events-cF2Om88w.d.ts +24 -0
- package/dist/fleet-state-7TJ2GYT2.js +39 -0
- package/dist/{forms-notify-target-TWN4EH6K.js → forms-notify-target-2EAWCWBW.js} +42 -133
- package/dist/forms-notify-target-2EAWCWBW.js.map +1 -0
- package/dist/{github-signals-G4ILZAJ6.js → github-signals-E2WPYTBK.js} +42 -21
- package/dist/github-signals-E2WPYTBK.js.map +1 -0
- package/dist/{header-image-T7XD37JL.js → header-image-6IC53W27.js} +36 -13
- package/dist/header-image-6IC53W27.js.map +1 -0
- package/dist/header-images-SQBCE5OD.js +88 -0
- package/dist/header-images-SQBCE5OD.js.map +1 -0
- package/dist/{health-endpoint-242WG2OJ.js → health-endpoint-RFXL2F4A.js} +9 -9
- package/dist/health-mirror-L5JBEJVA.js +41 -0
- package/dist/health-mirror-L5JBEJVA.js.map +1 -0
- package/dist/import-airtable-OHVY344G.js +37 -0
- package/dist/index.d.ts +324 -21
- package/dist/index.js +215 -76
- package/dist/index.js.map +1 -1
- package/dist/{init-JN26NTWF.js → init-XRAQZYID.js} +21 -18
- package/dist/{init-JN26NTWF.js.map → init-XRAQZYID.js.map} +1 -1
- package/dist/{launch-JA7Z4CIM.js → launch-IX4Y5JJC.js} +65 -30
- package/dist/launch-IX4Y5JJC.js.map +1 -0
- package/dist/lighthouse-VHI77PFB.js +11 -0
- package/dist/{lighthouse-airtable-BIPM26SZ.js → lighthouse-airtable-3GUWHETW.js} +3 -3
- package/dist/mailchimp-XYCTEEJY.js +72 -0
- package/dist/mailchimp-XYCTEEJY.js.map +1 -0
- package/dist/migrate-GQH237A6.js +7 -0
- package/dist/migrate-GQH237A6.js.map +1 -0
- package/dist/notify-C645PGD5.js +22 -0
- package/dist/notify-C645PGD5.js.map +1 -0
- package/dist/{onboard-53CITIM7.js → onboard-O4IAHJMO.js} +9 -9
- package/dist/orchestrate-YLWQ6KK2.js +31 -0
- package/dist/orchestrate-YLWQ6KK2.js.map +1 -0
- package/dist/parity-56KA2DGR.js +12 -0
- package/dist/parity-56KA2DGR.js.map +1 -0
- package/dist/pipeline-3AANZFCR.js +12 -0
- package/dist/pipeline-3AANZFCR.js.map +1 -0
- package/dist/{preflight-3XPDUHFJ.js → preflight-TNORKGCX.js} +17 -13
- package/dist/{preflight-3XPDUHFJ.js.map → preflight-TNORKGCX.js.map} +1 -1
- package/dist/{prismic-ci-UTQ6LRR3.js → prismic-ci-G2AK53VL.js} +12 -12
- package/dist/{prismic-models-PVES6R5V.js → prismic-models-O6Z3OK3A.js} +18 -13
- package/dist/prismic-models-O6Z3OK3A.js.map +1 -0
- package/dist/prospect/types.d.ts +267 -0
- package/dist/prospect/types.js +1 -0
- package/dist/prospect/types.js.map +1 -0
- package/dist/prospect-audit-KSXUNQ5J.js +174 -0
- package/dist/prospect-audit-KSXUNQ5J.js.map +1 -0
- package/dist/prospect-audits-3PMIO73D.js +19 -0
- package/dist/prospect-audits-3PMIO73D.js.map +1 -0
- package/dist/{protection-audit-64PLLBLI.js → protection-audit-SJO53MEQ.js} +3 -3
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/render-IFOGFB5T.js +10 -0
- package/dist/render-IFOGFB5T.js.map +1 -0
- package/dist/render-from-row-BWSOIYDS.js +17 -0
- package/dist/render-from-row-BWSOIYDS.js.map +1 -0
- package/dist/{renovate-dispatch-ICJGSFCE.js → renovate-dispatch-6GQWTRKX.js} +12 -12
- package/dist/{renovate-dispatch-ICJGSFCE.js.map → renovate-dispatch-6GQWTRKX.js.map} +1 -1
- package/dist/replay-C2LXMJZP.js +66 -0
- package/dist/replay-C2LXMJZP.js.map +1 -0
- package/dist/{report-6CKSWJNG.js → report-R3E3Y7HR.js} +100 -32
- package/dist/report-R3E3Y7HR.js.map +1 -0
- package/dist/report-mirror-4MNORYQ4.js +49 -0
- package/dist/report-mirror-4MNORYQ4.js.map +1 -0
- package/dist/reports/header-image/assets/headline-announcement.png +0 -0
- package/dist/reports/header-image/assets/headline-launch.png +0 -0
- package/dist/reports/header-image/assets/headline-maintenance.png +0 -0
- package/dist/reports/header-image/assets/headline-testing.png +0 -0
- package/dist/reports/header-image/assets/plate-clean.png +0 -0
- package/dist/rerender-3FM6DNRH.js +32 -0
- package/dist/rerender-3FM6DNRH.js.map +1 -0
- package/dist/{self-updating-PPQVV6UA.js → self-updating-UCIWESAX.js} +9 -9
- package/dist/{selftest-CQ6NOX52.js → selftest-VGHWPN72.js} +31 -24
- package/dist/selftest-VGHWPN72.js.map +1 -0
- package/dist/site-mirror-NIREX5ME.js +52 -0
- package/dist/site-mirror-NIREX5ME.js.map +1 -0
- package/dist/smoke-B2GQOSIZ.js +20 -0
- package/dist/smoke-B2GQOSIZ.js.map +1 -0
- package/dist/{smoke-suite-TMCSOYRK.js → smoke-suite-U3UKD3NS.js} +9 -9
- package/dist/{submissions-GJ6ITBRH.js → submissions-IQEJX6FK.js} +3 -3
- package/dist/{svelte-codemods-ZOBDHIES.js → svelte-codemods-GOKXTBHW.js} +9 -9
- package/dist/sync-CTUVXMCK.js +44 -0
- package/dist/sync-CTUVXMCK.js.map +1 -0
- package/dist/{sync-configs-CJXUX6MD.js → sync-configs-SPTLFJCC.js} +9 -9
- package/dist/{upgrade-WLX27UPC.js → upgrade-W6ZR3FJH.js} +9 -9
- package/dist/webhook-VOGOW3W6.js +38 -0
- package/dist/webhook-VOGOW3W6.js.map +1 -0
- package/dist/{websites-SADZR4XQ.js → websites-D6WYFAES.js} +6 -2
- package/dist/websites-D6WYFAES.js.map +1 -0
- package/dist/{write-audits-to-airtable-7PH75YWY.js → write-audits-to-airtable-CMKHGJNQ.js} +4 -4
- package/dist/write-audits-to-airtable-CMKHGJNQ.js.map +1 -0
- package/package.json +9 -2
- package/dist/airtable-HRKSA7V4.js +0 -9
- package/dist/announce-GIAYN67S.js.map +0 -1
- package/dist/chunk-37S6L6G4.js.map +0 -1
- package/dist/chunk-3YXDSCM6.js.map +0 -1
- package/dist/chunk-4DHQCN5H.js.map +0 -1
- package/dist/chunk-4SOCPK7A.js +0 -108
- package/dist/chunk-4SOCPK7A.js.map +0 -1
- package/dist/chunk-577HCGZO.js.map +0 -1
- package/dist/chunk-62VF32IC.js.map +0 -1
- package/dist/chunk-6QRLRT3P.js.map +0 -1
- package/dist/chunk-COZPL62X.js.map +0 -1
- package/dist/chunk-D4FVAIEJ.js.map +0 -1
- package/dist/chunk-DTTIFYAM.js.map +0 -1
- package/dist/chunk-MGR2H7F4.js.map +0 -1
- package/dist/chunk-MPBXOJ3N.js.map +0 -1
- package/dist/chunk-NWB4O5EY.js.map +0 -1
- package/dist/chunk-SEEA6RXF.js.map +0 -1
- package/dist/chunk-U5SZ4FOP.js +0 -26
- package/dist/chunk-U5SZ4FOP.js.map +0 -1
- package/dist/client-X5WUJWDE.js +0 -11
- package/dist/db-JSIZYT23.js +0 -23
- package/dist/db-JSIZYT23.js.map +0 -1
- package/dist/ensure-site-S25MDDHW.js.map +0 -1
- package/dist/forms-notify-target-TWN4EH6K.js.map +0 -1
- package/dist/github-signals-G4ILZAJ6.js.map +0 -1
- package/dist/header-image-T7XD37JL.js.map +0 -1
- package/dist/launch-JA7Z4CIM.js.map +0 -1
- package/dist/migrate-LJUFGXQZ.js +0 -7
- package/dist/orchestrate-KDNXZQX7.js +0 -27
- package/dist/prismic-models-PVES6R5V.js.map +0 -1
- package/dist/report-6CKSWJNG.js.map +0 -1
- package/dist/reports/header-image/assets/plate.png +0 -0
- package/dist/selftest-CQ6NOX52.js.map +0 -1
- /package/dist/{airtable-HRKSA7V4.js.map → airtable-ZBUPQJSC.js.map} +0 -0
- /package/dist/{client-X5WUJWDE.js.map → attachments-NLZIJ5NU.js.map} +0 -0
- /package/dist/{bump-deps-SWJB6H5V.js.map → bump-deps-CGVELRLK.js.map} +0 -0
- /package/dist/{chunk-SDUJIANJ.js.map → chunk-2NJQLSEU.js.map} +0 -0
- /package/dist/{chunk-ZTPPCHDB.js.map → chunk-3G25KIWW.js.map} +0 -0
- /package/dist/{chunk-THEFTZ7F.js.map → chunk-62CMMGEO.js.map} +0 -0
- /package/dist/{chunk-FDIFTIXC.js.map → chunk-6TWWY7ZU.js.map} +0 -0
- /package/dist/{chunk-PBWDDDQH.js.map → chunk-7BZUOZPU.js.map} +0 -0
- /package/dist/{chunk-UYCAF77E.js.map → chunk-7RJ2HMMJ.js.map} +0 -0
- /package/dist/{chunk-Q276MZ7E.js.map → chunk-7UAYMAKR.js.map} +0 -0
- /package/dist/{chunk-R6FPFMOS.js.map → chunk-BQRFQSGY.js.map} +0 -0
- /package/dist/{chunk-7J53SUT6.js.map → chunk-CROKUHJB.js.map} +0 -0
- /package/dist/{chunk-GZNBCHAZ.js.map → chunk-HEWSRXYV.js.map} +0 -0
- /package/dist/{chunk-LS6QPANO.js.map → chunk-IYSEC543.js.map} +0 -0
- /package/dist/{chunk-DC5H7KEQ.js.map → chunk-JHGHLKFS.js.map} +0 -0
- /package/dist/{chunk-TP5M2NQB.js.map → chunk-NCMZMLY7.js.map} +0 -0
- /package/dist/{chunk-WYYQK4D3.js.map → chunk-TD2NNXLZ.js.map} +0 -0
- /package/dist/{chunk-ZMIPG6TR.js.map → chunk-TVL5MVUF.js.map} +0 -0
- /package/dist/{chunk-VRWUL4TR.js.map → chunk-XHHMM5HX.js.map} +0 -0
- /package/dist/{chunk-RJB3QUSP.js.map → chunk-XIDUYLSR.js.map} +0 -0
- /package/dist/{chunk-QG6OSL4M.js.map → chunk-XUGSZP7Z.js.map} +0 -0
- /package/dist/{chunk-S56JQJ4F.js.map → chunk-Y53WOQIJ.js.map} +0 -0
- /package/dist/{chunk-ICXV25N7.js.map → chunk-Z3KYFUN6.js.map} +0 -0
- /package/dist/{lighthouse-airtable-BIPM26SZ.js.map → client-AUE3XUPO.js.map} +0 -0
- /package/dist/{convert-to-pnpm-AT52NWZB.js.map → convert-to-pnpm-QFJOEP5V.js.map} +0 -0
- /package/dist/{migrate-LJUFGXQZ.js.map → fleet-state-7TJ2GYT2.js.map} +0 -0
- /package/dist/{health-endpoint-242WG2OJ.js.map → health-endpoint-RFXL2F4A.js.map} +0 -0
- /package/dist/{orchestrate-KDNXZQX7.js.map → import-airtable-OHVY344G.js.map} +0 -0
- /package/dist/{websites-SADZR4XQ.js.map → lighthouse-VHI77PFB.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-7PH75YWY.js.map → lighthouse-airtable-3GUWHETW.js.map} +0 -0
- /package/dist/{onboard-53CITIM7.js.map → onboard-O4IAHJMO.js.map} +0 -0
- /package/dist/{prismic-ci-UTQ6LRR3.js.map → prismic-ci-G2AK53VL.js.map} +0 -0
- /package/dist/{protection-audit-64PLLBLI.js.map → protection-audit-SJO53MEQ.js.map} +0 -0
- /package/dist/{self-updating-PPQVV6UA.js.map → self-updating-UCIWESAX.js.map} +0 -0
- /package/dist/{smoke-suite-TMCSOYRK.js.map → smoke-suite-U3UKD3NS.js.map} +0 -0
- /package/dist/{submissions-GJ6ITBRH.js.map → submissions-IQEJX6FK.js.map} +0 -0
- /package/dist/{svelte-codemods-ZOBDHIES.js.map → svelte-codemods-GOKXTBHW.js.map} +0 -0
- /package/dist/{sync-configs-CJXUX6MD.js.map → sync-configs-SPTLFJCC.js.map} +0 -0
- /package/dist/{upgrade-WLX27UPC.js.map → upgrade-W6ZR3FJH.js.map} +0 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALL_CHECKLIST_FIELDS
|
|
3
|
+
} from "./chunk-VAD43MOR.js";
|
|
4
|
+
|
|
5
|
+
// src/reports/airtable/reports.ts
|
|
6
|
+
var REPORTS_TABLE = "Reports";
|
|
7
|
+
var REPORT_TYPES = ["Maintenance", "Testing", "Launch", "Announcement"];
|
|
8
|
+
function toReportType(raw) {
|
|
9
|
+
if (raw && REPORT_TYPES.includes(raw)) return raw;
|
|
10
|
+
if (raw)
|
|
11
|
+
console.warn(`[reports] unknown Report type ${JSON.stringify(raw)} \u2014 treating as Maintenance`);
|
|
12
|
+
return "Maintenance";
|
|
13
|
+
}
|
|
14
|
+
function isPendingApproval(r) {
|
|
15
|
+
return r.draftReady && !r.approvedToSend && r.sentAt === null;
|
|
16
|
+
}
|
|
17
|
+
function mapRow(rec) {
|
|
18
|
+
const f = rec.fields;
|
|
19
|
+
const linkSites = f["Site"] ?? [];
|
|
20
|
+
const html = (f["Rendered HTML"] ?? [])[0] ?? null;
|
|
21
|
+
return {
|
|
22
|
+
id: rec.id,
|
|
23
|
+
reportId: String(f["Report ID"] ?? ""),
|
|
24
|
+
siteId: linkSites[0] ?? "",
|
|
25
|
+
reportType: toReportType(f["Report type"]),
|
|
26
|
+
period: f["Period"] ?? null,
|
|
27
|
+
periodStart: f["Period start"] ?? null,
|
|
28
|
+
periodEnd: f["Period end"] ?? null,
|
|
29
|
+
completedOn: f["Completed on"] ?? null,
|
|
30
|
+
lighthouse: lighthouseFromFields(f),
|
|
31
|
+
gaUsersCurrent: f["GA users (period)"] ?? null,
|
|
32
|
+
gaUsersPrevious: f["GA users (prev period)"] ?? null,
|
|
33
|
+
searchFoundPage1: typeof f["Search found page 1"] === "boolean" ? f["Search found page 1"] : null,
|
|
34
|
+
searchPosition: f["Search position"] ?? null,
|
|
35
|
+
lastTestedDate: f["Last tested date"] ?? null,
|
|
36
|
+
commentary: f["Commentary"] ?? null,
|
|
37
|
+
subjectOverride: f["Subject override"] ?? null,
|
|
38
|
+
draftReady: Boolean(f["Draft ready"]),
|
|
39
|
+
approvedToSend: Boolean(f["Approved to send"]),
|
|
40
|
+
sentAt: f["Sent at"] ?? null,
|
|
41
|
+
approvedAt: f["Approved At"] ?? null,
|
|
42
|
+
approvedBy: f["Approved By"] ?? null,
|
|
43
|
+
deliveryStatus: f["Delivery status"] ?? "pending",
|
|
44
|
+
renderedHtmlAttachment: html,
|
|
45
|
+
resendMessageId: f["Resend message ID"] ?? null,
|
|
46
|
+
checklist: Object.fromEntries(ALL_CHECKLIST_FIELDS.map((name) => [name, Boolean(f[name])])),
|
|
47
|
+
autoEvidence: parseAutoEvidence(f["Checklist auto-evidence"]),
|
|
48
|
+
sendOverride: Boolean(f["Send override"]),
|
|
49
|
+
overrideReason: f["Override reason"] ?? null,
|
|
50
|
+
overrideBy: f["Override by"] ?? null,
|
|
51
|
+
overrideAt: f["Override at"] ?? null
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function parseAutoEvidence(raw) {
|
|
55
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(raw);
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
63
|
+
const out = {};
|
|
64
|
+
for (const [field, v] of Object.entries(parsed)) {
|
|
65
|
+
if (!v || typeof v !== "object") continue;
|
|
66
|
+
const o = v;
|
|
67
|
+
if (o.result !== "pass" && o.result !== "fail" && o.result !== "unknown" && o.result !== "n/a")
|
|
68
|
+
continue;
|
|
69
|
+
out[field] = {
|
|
70
|
+
result: o.result,
|
|
71
|
+
checkedAt: typeof o.checkedAt === "string" ? o.checkedAt : null,
|
|
72
|
+
note: typeof o.note === "string" ? o.note : ""
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
76
|
+
}
|
|
77
|
+
function lighthouseFromFields(f) {
|
|
78
|
+
const p = f["Lighthouse \u2014 Performance"];
|
|
79
|
+
const a = f["Lighthouse \u2014 Accessibility"];
|
|
80
|
+
const b = f["Lighthouse \u2014 Best Practices"];
|
|
81
|
+
const s = f["Lighthouse \u2014 SEO"];
|
|
82
|
+
if (typeof p !== "number" || typeof a !== "number" || typeof b !== "number" || typeof s !== "number")
|
|
83
|
+
return null;
|
|
84
|
+
return { performance: p, accessibility: a, bestPractices: b, seo: s };
|
|
85
|
+
}
|
|
86
|
+
function ymd(d) {
|
|
87
|
+
return d.toISOString().slice(0, 10);
|
|
88
|
+
}
|
|
89
|
+
function escapeFormulaString(s) {
|
|
90
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
91
|
+
}
|
|
92
|
+
async function createDraft(base, input, mirror) {
|
|
93
|
+
const fields = {
|
|
94
|
+
"Report ID": input.reportId,
|
|
95
|
+
Site: [input.siteId],
|
|
96
|
+
"Report type": input.reportType,
|
|
97
|
+
"Period start": ymd(input.periodStart),
|
|
98
|
+
"Period end": ymd(input.periodEnd),
|
|
99
|
+
"Completed on": ymd(input.completedOn),
|
|
100
|
+
"Lighthouse \u2014 Performance": input.lighthouse.performance,
|
|
101
|
+
"Lighthouse \u2014 Accessibility": input.lighthouse.accessibility,
|
|
102
|
+
"Lighthouse \u2014 Best Practices": input.lighthouse.bestPractices,
|
|
103
|
+
"Lighthouse \u2014 SEO": input.lighthouse.seo,
|
|
104
|
+
"Delivery status": "pending"
|
|
105
|
+
};
|
|
106
|
+
if (input.lastTestedDate) fields["Last tested date"] = ymd(input.lastTestedDate);
|
|
107
|
+
if (input.gaUsersCurrent !== void 0) fields["GA users (period)"] = input.gaUsersCurrent;
|
|
108
|
+
if (input.gaUsersPrevious !== void 0) fields["GA users (prev period)"] = input.gaUsersPrevious;
|
|
109
|
+
if (input.searchFoundPage1 !== void 0) fields["Search found page 1"] = input.searchFoundPage1;
|
|
110
|
+
if (input.searchPosition !== void 0) fields["Search position"] = input.searchPosition;
|
|
111
|
+
if (input.period !== void 0) fields["Period"] = input.period;
|
|
112
|
+
if (input.subjectOverride !== void 0) fields["Subject override"] = input.subjectOverride;
|
|
113
|
+
for (const field of input.checklistTicks ?? []) fields[field] = true;
|
|
114
|
+
if (input.autoEvidence && Object.keys(input.autoEvidence).length > 0) {
|
|
115
|
+
fields["Checklist auto-evidence"] = JSON.stringify(input.autoEvidence);
|
|
116
|
+
}
|
|
117
|
+
const created = await base(REPORTS_TABLE).create([{ fields }]);
|
|
118
|
+
const rec = created[0];
|
|
119
|
+
if (!rec) throw new Error("Airtable create returned no records");
|
|
120
|
+
const raw = { id: rec.id, fields: rec.fields };
|
|
121
|
+
if (mirror) await mirror(raw);
|
|
122
|
+
return mapRow(raw);
|
|
123
|
+
}
|
|
124
|
+
async function setDraftReady(base, recordId, ready) {
|
|
125
|
+
await base(REPORTS_TABLE).update([{ id: recordId, fields: { "Draft ready": ready } }]);
|
|
126
|
+
}
|
|
127
|
+
async function updateReportScores(base, recordId, scores, completedOn, enrichment) {
|
|
128
|
+
const fields = {
|
|
129
|
+
"Lighthouse \u2014 Performance": scores.performance,
|
|
130
|
+
"Lighthouse \u2014 Accessibility": scores.accessibility,
|
|
131
|
+
"Lighthouse \u2014 Best Practices": scores.bestPractices,
|
|
132
|
+
"Lighthouse \u2014 SEO": scores.seo
|
|
133
|
+
};
|
|
134
|
+
if (completedOn) fields["Completed on"] = ymd(completedOn);
|
|
135
|
+
if (enrichment?.gaUsersCurrent !== void 0)
|
|
136
|
+
fields["GA users (period)"] = enrichment.gaUsersCurrent;
|
|
137
|
+
if (enrichment?.gaUsersPrevious !== void 0)
|
|
138
|
+
fields["GA users (prev period)"] = enrichment.gaUsersPrevious;
|
|
139
|
+
if (enrichment?.searchFoundPage1 !== void 0)
|
|
140
|
+
fields["Search found page 1"] = enrichment.searchFoundPage1;
|
|
141
|
+
if (enrichment?.searchPosition !== void 0)
|
|
142
|
+
fields["Search position"] = enrichment.searchPosition;
|
|
143
|
+
await base(REPORTS_TABLE).update([{ id: recordId, fields }]);
|
|
144
|
+
}
|
|
145
|
+
async function listSendableReports(base) {
|
|
146
|
+
const out = [];
|
|
147
|
+
await base(REPORTS_TABLE).select({
|
|
148
|
+
filterByFormula: "AND({Draft ready} = TRUE(), {Approved to send} = TRUE(), {Sent at} = BLANK())",
|
|
149
|
+
pageSize: 100
|
|
150
|
+
}).eachPage((records, fetchNextPage) => {
|
|
151
|
+
for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));
|
|
152
|
+
fetchNextPage();
|
|
153
|
+
});
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
async function listAllReports(base) {
|
|
157
|
+
const out = [];
|
|
158
|
+
await base(REPORTS_TABLE).select({ pageSize: 100 }).eachPage((records, fetchNextPage) => {
|
|
159
|
+
for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));
|
|
160
|
+
fetchNextPage();
|
|
161
|
+
});
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
async function listReportsForSite(base, siteId) {
|
|
165
|
+
return (await listAllReports(base)).filter((r) => r.siteId === siteId);
|
|
166
|
+
}
|
|
167
|
+
async function stampSent(base, recordId, sentAt, messageId) {
|
|
168
|
+
const fields = { "Sent at": sentAt.toISOString() };
|
|
169
|
+
if (messageId !== null) fields["Resend message ID"] = messageId;
|
|
170
|
+
await base(REPORTS_TABLE).update([
|
|
171
|
+
{
|
|
172
|
+
id: recordId,
|
|
173
|
+
fields
|
|
174
|
+
}
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
async function findReportByPeriod(base, siteId, reportType, period) {
|
|
178
|
+
const safeType = escapeFormulaString(reportType);
|
|
179
|
+
const safePeriod = escapeFormulaString(period);
|
|
180
|
+
const formula = `AND({Report type} = "${safeType}", {Period} = "${safePeriod}")`;
|
|
181
|
+
const rows = [];
|
|
182
|
+
await base(REPORTS_TABLE).select({ filterByFormula: formula, pageSize: 100 }).eachPage((records, fetchNextPage) => {
|
|
183
|
+
for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));
|
|
184
|
+
fetchNextPage();
|
|
185
|
+
});
|
|
186
|
+
return rows.find((r) => r.siteId === siteId) ?? null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export {
|
|
190
|
+
toReportType,
|
|
191
|
+
isPendingApproval,
|
|
192
|
+
parseAutoEvidence,
|
|
193
|
+
createDraft,
|
|
194
|
+
setDraftReady,
|
|
195
|
+
updateReportScores,
|
|
196
|
+
listSendableReports,
|
|
197
|
+
listAllReports,
|
|
198
|
+
listReportsForSite,
|
|
199
|
+
stampSent,
|
|
200
|
+
findReportByPeriod
|
|
201
|
+
};
|
|
202
|
+
//# sourceMappingURL=chunk-F3JWOPTG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/reports/airtable/reports.ts"],"sourcesContent":["import type { FieldSet, Records } from \"airtable\";\nimport type { AirtableBase } from \"./client.js\";\nimport type { ReportType, LighthouseScores } from \"../types.js\";\nimport { ALL_CHECKLIST_FIELDS } from \"../checklist.js\";\nimport type { EvidenceRecord } from \"../auto-tick.js\";\n\nexport const REPORTS_TABLE = \"Reports\";\n\nconst REPORT_TYPES: readonly ReportType[] = [\"Maintenance\", \"Testing\", \"Launch\", \"Announcement\"];\n\n/** Coerce the Airtable `Report type` (a single-select string) to a known\n * ReportType. A bare `as ReportType` cast is a compile-time lie: if the\n * single-select gains an unexpected option, the bad value flows to render.ts,\n * where `reportType === \"Launch\"` silently falls through to the Maintenance\n * template. Validate at the boundary; warn + default to \"Maintenance\" so an\n * unknown type is VISIBLE in the logs rather than silently mis-templated. */\nexport function toReportType(raw: string | undefined): ReportType {\n if (raw && (REPORT_TYPES as readonly string[]).includes(raw)) return raw as ReportType;\n if (raw)\n console.warn(`[reports] unknown Report type ${JSON.stringify(raw)} — treating as Maintenance`);\n return \"Maintenance\";\n}\n\nexport type DeliveryStatus = \"pending\" | \"delivered\" | \"bounced\" | \"complained\";\n\nexport type ReportRow = {\n id: string;\n reportId: string;\n siteId: string;\n reportType: ReportType;\n /** UTC `YYYY-MM` recurrence key (idempotency for search-before-create). Null on legacy rows. */\n period: string | null;\n periodStart: string | null;\n periodEnd: string | null;\n completedOn: string | null;\n lighthouse: LighthouseScores | null;\n gaUsersCurrent: number | null;\n gaUsersPrevious: number | null;\n searchFoundPage1: boolean | null;\n searchPosition: number | null;\n lastTestedDate: string | null;\n commentary: string | null;\n subjectOverride: string | null;\n draftReady: boolean;\n approvedToSend: boolean;\n sentAt: string | null;\n approvedAt: string | null;\n approvedBy: string | null;\n deliveryStatus: DeliveryStatus;\n renderedHtmlAttachment: { url: string; filename: string } | null;\n /** Read out of the Resend response and stored in a hidden field; needed for webhook reconciliation. */\n resendMessageId: string | null;\n /** The 12 operator-checklist checkboxes, keyed by their Airtable column name (ALL_CHECKLIST_FIELDS);\n * missing/false cells read false. Maintenance/Testing reports gate approve+send on the relevant\n * subset (see src/reports/checklist.ts). */\n checklist: Record<string, boolean>;\n /** Snapshot of the auto-tick evidence at draft time, keyed by checklist field → evidence\n * record. Null when the report predates auto-tick or carried no auto-checked items. This is the\n * health gate's source of truth: `isHealthGateClear`/`gatingHealth` (src/reports/checklist.ts)\n * read these evidence records, NOT the checklist booleans — and also drive the dashboard badges. */\n autoEvidence: Record<string, EvidenceRecord> | null;\n /** Logged send-anyway override (Phase 10). When `sendOverride` is true AND `overrideReason` is\n * non-empty, the health gate is bypassed for THIS report; `overrideBy`/`overrideAt` are the\n * audit trail (parallel to approvedBy/approvedAt). Missing cells read false/null. */\n sendOverride: boolean;\n overrideReason: string | null;\n overrideBy: string | null;\n overrideAt: string | null;\n};\n\n/**\n * The \"Ready for your yes\" gate: Draft ready ∧ ¬Approved to send ∧ Sent at BLANK.\n * The single source of truth for \"pending the operator's approval\" — `listPendingApproval`,\n * `runDigest`'s ready-list, the per-site dashboard, and the fleet cockpit all key off this\n * one predicate so the surfaces can't drift.\n */\nexport function isPendingApproval(r: ReportRow): boolean {\n return r.draftReady && !r.approvedToSend && r.sentAt === null;\n}\n\nexport function mapRow(rec: { id: string; fields: Record<string, unknown> }): ReportRow {\n const f = rec.fields;\n const linkSites = (f[\"Site\"] as string[] | undefined) ?? [];\n const html =\n ((f[\"Rendered HTML\"] as Array<{ url: string; filename: string }> | undefined) ?? [])[0] ?? null;\n return {\n id: rec.id,\n reportId: String(f[\"Report ID\"] ?? \"\"),\n siteId: linkSites[0] ?? \"\",\n reportType: toReportType(f[\"Report type\"] as string | undefined),\n period: (f[\"Period\"] as string | undefined) ?? null,\n periodStart: (f[\"Period start\"] as string | undefined) ?? null,\n periodEnd: (f[\"Period end\"] as string | undefined) ?? null,\n completedOn: (f[\"Completed on\"] as string | undefined) ?? null,\n lighthouse: lighthouseFromFields(f),\n gaUsersCurrent: (f[\"GA users (period)\"] as number | undefined) ?? null,\n gaUsersPrevious: (f[\"GA users (prev period)\"] as number | undefined) ?? null,\n searchFoundPage1:\n typeof f[\"Search found page 1\"] === \"boolean\" ? (f[\"Search found page 1\"] as boolean) : null,\n searchPosition: (f[\"Search position\"] as number | undefined) ?? null,\n lastTestedDate: (f[\"Last tested date\"] as string | undefined) ?? null,\n commentary: (f[\"Commentary\"] as string | undefined) ?? null,\n subjectOverride: (f[\"Subject override\"] as string | undefined) ?? null,\n draftReady: Boolean(f[\"Draft ready\"]),\n approvedToSend: Boolean(f[\"Approved to send\"]),\n sentAt: (f[\"Sent at\"] as string | undefined) ?? null,\n approvedAt: (f[\"Approved At\"] as string | undefined) ?? null,\n approvedBy: (f[\"Approved By\"] as string | undefined) ?? null,\n deliveryStatus: ((f[\"Delivery status\"] as string | undefined) ?? \"pending\") as DeliveryStatus,\n renderedHtmlAttachment: html,\n resendMessageId: (f[\"Resend message ID\"] as string | undefined) ?? null,\n checklist: Object.fromEntries(ALL_CHECKLIST_FIELDS.map((name) => [name, Boolean(f[name])])),\n autoEvidence: parseAutoEvidence(f[\"Checklist auto-evidence\"]),\n sendOverride: Boolean(f[\"Send override\"]),\n overrideReason: (f[\"Override reason\"] as string | undefined) ?? null,\n overrideBy: (f[\"Override by\"] as string | undefined) ?? null,\n overrideAt: (f[\"Override at\"] as string | undefined) ?? null,\n };\n}\n\n/**\n * Parse the `Checklist auto-evidence` JSON field → a field→EvidenceRecord map, or null when\n * absent/malformed. Permissive on inner shape (it's display-only — a bad blob just yields null,\n * never throws), mirroring `parseNotifyRouting`.\n */\nexport function parseAutoEvidence(raw: unknown): Record<string, EvidenceRecord> | null {\n if (typeof raw !== \"string\" || !raw.trim()) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n // Validate each entry's inner shape (it drives display) — drop any record whose `result` isn't\n // one of the four literals, and coerce `note`/`checkedAt` to safe types. A garbage blob yields\n // null rather than a record that would render \"auto: undefined\" on the dashboard.\n const out: Record<string, EvidenceRecord> = {};\n for (const [field, v] of Object.entries(parsed as Record<string, unknown>)) {\n if (!v || typeof v !== \"object\") continue;\n const o = v as Record<string, unknown>;\n if (o.result !== \"pass\" && o.result !== \"fail\" && o.result !== \"unknown\" && o.result !== \"n/a\")\n continue;\n out[field] = {\n result: o.result,\n checkedAt: typeof o.checkedAt === \"string\" ? o.checkedAt : null,\n note: typeof o.note === \"string\" ? o.note : \"\",\n };\n }\n return Object.keys(out).length > 0 ? out : null;\n}\n\nfunction lighthouseFromFields(f: Record<string, unknown>): LighthouseScores | null {\n const p = f[\"Lighthouse — Performance\"];\n const a = f[\"Lighthouse — Accessibility\"];\n const b = f[\"Lighthouse — Best Practices\"];\n const s = f[\"Lighthouse — SEO\"];\n if (\n typeof p !== \"number\" ||\n typeof a !== \"number\" ||\n typeof b !== \"number\" ||\n typeof s !== \"number\"\n )\n return null;\n return { performance: p, accessibility: a, bestPractices: b, seo: s };\n}\n\n/** The GA + Search-presence fields a draft can carry, written to the Reports row.\n * `searchFoundPage1` is written whenever the check ran (true or false — false is the\n * operator-only negative signal); `searchPosition` only when found on page 1. Shared by\n * the create path (DraftInput) and the reuse path (updateReportScores). */\nexport type ReportEnrichment = {\n gaUsersCurrent?: number;\n gaUsersPrevious?: number;\n searchFoundPage1?: boolean;\n searchPosition?: number;\n};\n\nexport type DraftInput = {\n reportId: string;\n siteId: string;\n reportType: ReportType;\n /** UTC `YYYY-MM` recurrence key. Omitted on legacy callers; written only when supplied. */\n period?: string;\n periodStart: Date;\n periodEnd: Date;\n completedOn: Date;\n lighthouse: LighthouseScores;\n lastTestedDate: Date | null;\n /** GA \"Users\" for the period / previous period. Omitted when GA is not configured\n * for the site or the fetch failed — the operator fills the fields manually. */\n gaUsersCurrent?: number;\n gaUsersPrevious?: number;\n /** Search-presence result. `searchFoundPage1` is written whenever the check ran (true or\n * false — false is the operator-only negative signal). `searchPosition` only when found. */\n searchFoundPage1?: boolean;\n searchPosition?: number;\n subjectOverride?: string;\n /** Checklist fields to tick at create time (the auto-tick \"pass\" set). */\n checklistTicks?: string[];\n /** Auto-tick evidence snapshot to persist (keyed by checklist field). */\n autoEvidence?: Record<string, EvidenceRecord>;\n};\n\nfunction ymd(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Escape a string for safe interpolation into an Airtable filterByFormula.\n * Airtable formulas use SQL-like string literals; we escape backslash and\n * double quote. Used wherever an externally-supplied string flows into a\n * formula (e.g. Resend message ids on the webhook path).\n */\nexport function escapeFormulaString(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\n/** Called with the record Airtable echoed back from the create, before it is\n * mapped (#539 Phase 5). Structurally typed rather than importing the db\n * layer's `RawRecord`, so this module stays the leaf it has always been. */\nexport type CreatedDraftMirror = (rec: {\n id: string;\n fields: Record<string, unknown>;\n}) => Promise<void>;\n\nexport async function createDraft(\n base: AirtableBase,\n input: DraftInput,\n mirror?: CreatedDraftMirror,\n): Promise<ReportRow> {\n // Set Delivery status to \"pending\" at creation time, NOT at send time. This\n // matters for H4: if stampSent wrote \"pending\" after the webhook had already\n // written \"delivered\" (race), the operator would see a regressed status.\n const fields: FieldSet = {\n \"Report ID\": input.reportId,\n Site: [input.siteId],\n \"Report type\": input.reportType,\n \"Period start\": ymd(input.periodStart),\n \"Period end\": ymd(input.periodEnd),\n \"Completed on\": ymd(input.completedOn),\n \"Lighthouse — Performance\": input.lighthouse.performance,\n \"Lighthouse — Accessibility\": input.lighthouse.accessibility,\n \"Lighthouse — Best Practices\": input.lighthouse.bestPractices,\n \"Lighthouse — SEO\": input.lighthouse.seo,\n \"Delivery status\": \"pending\",\n };\n if (input.lastTestedDate) fields[\"Last tested date\"] = ymd(input.lastTestedDate);\n // GA fields are written only when supplied (GA configured + fetch succeeded). When\n // omitted the row keeps them blank for manual entry — the pre-GA behavior.\n if (input.gaUsersCurrent !== undefined) fields[\"GA users (period)\"] = input.gaUsersCurrent;\n if (input.gaUsersPrevious !== undefined) fields[\"GA users (prev period)\"] = input.gaUsersPrevious;\n if (input.searchFoundPage1 !== undefined) fields[\"Search found page 1\"] = input.searchFoundPage1;\n if (input.searchPosition !== undefined) fields[\"Search position\"] = input.searchPosition;\n if (input.period !== undefined) fields[\"Period\"] = input.period;\n if (input.subjectOverride !== undefined) fields[\"Subject override\"] = input.subjectOverride;\n // Auto-ticked checklist boxes + the evidence snapshot. The booleans are the same columns the\n // operator/dashboard toggle; the JSON is what the health gate reads (isHealthGateClear/gatingHealth)\n // and also drives the dashboard's green/amber badges.\n for (const field of input.checklistTicks ?? []) fields[field] = true;\n if (input.autoEvidence && Object.keys(input.autoEvidence).length > 0) {\n fields[\"Checklist auto-evidence\"] = JSON.stringify(input.autoEvidence);\n }\n const created = (await base(REPORTS_TABLE).create([{ fields }])) as Records<FieldSet>;\n const rec = created[0];\n if (!rec) throw new Error(\"Airtable create returned no records\");\n const raw = { id: rec.id, fields: rec.fields as Record<string, unknown> };\n // The mirror gets what Airtable STORED, never the caller's DraftInput: parity\n // diffs Turso against the stored record, so mapping the input would diverge\n // the moment Airtable normalises a value. Injected rather than called\n // directly so this module keeps no db dependency; the injected\n // implementation owns the never-throw policy (see reports/report-mirror.ts).\n if (mirror) await mirror(raw);\n return mapRow(raw);\n}\n\nexport async function setDraftReady(\n base: AirtableBase,\n recordId: string,\n ready: boolean,\n): Promise<void> {\n await base(REPORTS_TABLE).update([{ id: recordId, fields: { \"Draft ready\": ready } }]);\n}\n\n/**\n * Overwrite the four `Lighthouse — *` score cells (and, when supplied, `Completed\n * on`) on an EXISTING Reports row. The launch re-run path uses this: it reuses the\n * already-created Launch row but must refresh its scores to match the freshly-run\n * audit — otherwise the re-rendered preview shows new scores while the row (and the\n * eventually-sent email, which reads the row) keeps the stale ones. The create path\n * already writes fresh scores via `createDraft`; this is its update-side mirror, using\n * the same exact field names so the two stay in lockstep.\n */\nexport async function updateReportScores(\n base: AirtableBase,\n recordId: string,\n scores: LighthouseScores,\n completedOn?: Date,\n enrichment?: ReportEnrichment,\n): Promise<void> {\n const fields: FieldSet = {\n \"Lighthouse — Performance\": scores.performance,\n \"Lighthouse — Accessibility\": scores.accessibility,\n \"Lighthouse — Best Practices\": scores.bestPractices,\n \"Lighthouse — SEO\": scores.seo,\n };\n if (completedOn) fields[\"Completed on\"] = ymd(completedOn);\n if (enrichment?.gaUsersCurrent !== undefined)\n fields[\"GA users (period)\"] = enrichment.gaUsersCurrent;\n if (enrichment?.gaUsersPrevious !== undefined)\n fields[\"GA users (prev period)\"] = enrichment.gaUsersPrevious;\n if (enrichment?.searchFoundPage1 !== undefined)\n fields[\"Search found page 1\"] = enrichment.searchFoundPage1;\n if (enrichment?.searchPosition !== undefined)\n fields[\"Search position\"] = enrichment.searchPosition;\n await base(REPORTS_TABLE).update([{ id: recordId, fields }]);\n}\n\n/** Write one report's operator commentary (#539 Phase 4 report review). The\n * caller (`setReportCommentary`) has already validated the length and refused\n * the write for an already-sent report, so this stays a plain single-field\n * writer — the same division of labour as `updateSiteField`/`setSiteDetail`. */\nexport async function updateReportCommentary(\n base: AirtableBase,\n recordId: string,\n text: string,\n): Promise<void> {\n await base(REPORTS_TABLE).update([{ id: recordId, fields: { Commentary: text } }]);\n}\n\nexport async function listSendableReports(base: AirtableBase): Promise<ReportRow[]> {\n const out: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({\n filterByFormula:\n \"AND({Draft ready} = TRUE(), {Approved to send} = TRUE(), {Sent at} = BLANK())\",\n pageSize: 100,\n })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\n/**\n * Fetch every Reports row, unfiltered. Site-scoped callers filter the result in\n * memory: the `Site` linked-record field CANNOT be formula-filtered by record id\n * (see findReportByPeriod's doc for why), and the fleet's Reports table is small\n * enough that one paged fetch-all beats N broken-or-per-site queries.\n */\nexport async function listAllReports(base: AirtableBase): Promise<ReportRow[]> {\n const out: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({ pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\nexport async function listReportsForSite(base: AirtableBase, siteId: string): Promise<ReportRow[]> {\n // Client-side match on the mapped siteId (mapRow reads the record id from the\n // REST response, where it IS present) — record ids can't appear in formulas.\n return (await listAllReports(base)).filter((r) => r.siteId === siteId);\n}\n\n/**\n * Mark a row as sent: write `Sent at` and (when known) `Resend message ID`.\n * Crucially does NOT touch `Delivery status` — that's set to \"pending\" in\n * createDraft and updated by the webhook from there. If we wrote \"pending\" here\n * we could clobber a \"delivered\" that the webhook raced ahead and wrote first (H4).\n *\n * `messageId` may be `null`: the 409 idempotency-conflict path has no recoverable\n * id (the original send's id was lost when the prior run's stamp failed), so it\n * passes null and we write ONLY `Sent at`. Writing a sentinel string like\n * \"idempotent-conflict\" into the id column would masquerade as a real Resend id\n * and silently orphan `findReportByMessageId` lookups; leaving it null is honest —\n * the row still stops replaying (Sent at is set), delivery tracking for that one\n * report is simply degraded (the id is genuinely unknowable on that path).\n */\nexport async function stampSent(\n base: AirtableBase,\n recordId: string,\n sentAt: Date,\n messageId: string | null,\n): Promise<void> {\n const fields: Record<string, string> = { \"Sent at\": sentAt.toISOString() };\n if (messageId !== null) fields[\"Resend message ID\"] = messageId;\n await base(REPORTS_TABLE).update([\n {\n id: recordId,\n fields,\n },\n ]);\n}\n\nexport async function setDeliveryStatus(\n base: AirtableBase,\n recordId: string,\n status: DeliveryStatus,\n): Promise<void> {\n await base(REPORTS_TABLE).update([{ id: recordId, fields: { \"Delivery status\": status } }]);\n}\n\n/**\n * Stamp the approval on a Reports row: flips `Approved to send` TRUE and records\n * who/when for the audit trail. The caller (approveReport handler) is responsible\n * for idempotency — this is the raw write. Never touches `Sent at`.\n */\nexport async function approveReportRow(\n base: AirtableBase,\n recordId: string,\n approvedAt: Date,\n approvedBy: string,\n): Promise<void> {\n await base(REPORTS_TABLE).update([\n {\n id: recordId,\n fields: {\n \"Approved to send\": true,\n \"Approved At\": approvedAt.toISOString(),\n \"Approved By\": approvedBy,\n },\n },\n ]);\n}\n\n/**\n * Stamp a logged send-anyway override on a Reports row: sets `Send override` TRUE, the reason, and\n * who/when — AND flips `Approved to send` TRUE (with the same Approved At/By stamp) so the daily\n * cron delivers the overridden report. Mirrors {@link approveReportRow}; never touches `Sent at`.\n */\nexport async function overrideReportRow(\n base: AirtableBase,\n recordId: string,\n overrideAt: Date,\n overrideBy: string,\n reason: string,\n): Promise<void> {\n const at = overrideAt.toISOString();\n await base(REPORTS_TABLE).update([\n {\n id: recordId,\n fields: {\n \"Send override\": true,\n \"Override reason\": reason,\n \"Override by\": overrideBy,\n \"Override at\": at,\n \"Approved to send\": true,\n \"Approved At\": at,\n \"Approved By\": overrideBy,\n },\n },\n ]);\n}\n\n/**\n * True when an `.find` rejection is a GENUINE not-found, not a transient failure.\n * The Airtable SDK stamps `.statusCode` (404) and/or `.error` (\"NOT_FOUND\") on\n * its errors. Anything else (429 rate-limit, 500 outage, bad-PAT 401, network\n * error) must NOT be masked as a 404 — see getReportById.\n */\nfunction isNotFoundError(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) return false;\n const e = err as { statusCode?: unknown; error?: unknown; name?: unknown; message?: unknown };\n if (e.statusCode === 404) return true;\n const tag = String(e.error ?? e.name ?? e.message ?? \"\");\n return tag === \"NOT_FOUND\" || /not found/i.test(tag);\n}\n\n/**\n * Fetch one Reports row by its Airtable record id, or null if it doesn't exist.\n * Only a GENUINE not-found (404 / NOT_FOUND) collapses to null; every other\n * failure (outage, 429, bad PAT, network error) is rethrown so the adapter\n * surfaces a 500 instead of a misleading 404. Swallowing all throws previously\n * turned an Airtable outage into a \"no such report\".\n */\nexport async function getReportById(\n base: AirtableBase,\n recordId: string,\n): Promise<ReportRow | null> {\n try {\n const rec = await base(REPORTS_TABLE).find(recordId);\n return mapRow({ id: rec.id, fields: rec.fields as Record<string, unknown> });\n } catch (err) {\n if (isNotFoundError(err)) return null;\n throw err;\n }\n}\n\nexport async function findReportByMessageId(\n base: AirtableBase,\n messageId: string,\n): Promise<ReportRow | null> {\n const rows: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({\n filterByFormula: `{Resend message ID} = \"${escapeFormulaString(messageId)}\"`,\n maxRecords: 1,\n })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return rows[0] ?? null;\n}\n\n/**\n * Find the Reports row for a `(site, reportType, period)` triple, or null. The\n * idempotency lookup behind search-before-create drafting.\n *\n * The site is matched CLIENT-side, never in the formula: Airtable's formula layer\n * renders linked-record fields ({Site}) as the linked rows' PRIMARY-FIELD NAMES,\n * not record ids, so any formula comparing {Site} or ARRAYJOIN({Site}) against a\n * `recXXX` id matches NOTHING (live-proven against the real base — do not\n * reintroduce that idiom). Record ids exist only in the REST response, where\n * mapRow reads them. So the formula filters on the real scalar fields (Report\n * type + Period — escaped, keeping it injection-safe if their source ever\n * changes), and the first mapped row whose siteId matches wins. The candidate\n * set is at most one row per site for the (type, period), so this stays small.\n */\nexport async function findReportByPeriod(\n base: AirtableBase,\n siteId: string,\n reportType: ReportType,\n period: string,\n): Promise<ReportRow | null> {\n const safeType = escapeFormulaString(reportType);\n const safePeriod = escapeFormulaString(period);\n const formula = `AND({Report type} = \"${safeType}\", {Period} = \"${safePeriod}\")`;\n const rows: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({ filterByFormula: formula, pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return rows.find((r) => r.siteId === siteId) ?? null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fetchGaUsers,
|
|
3
3
|
fetchSearch
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BBUSGEES.js";
|
|
5
5
|
import {
|
|
6
6
|
announcementSiteExtras,
|
|
7
7
|
resolveCopy
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MT2JINSS.js";
|
|
9
9
|
import {
|
|
10
10
|
siteSlug
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-7NYN2P6I.js";
|
|
12
12
|
|
|
13
13
|
// src/reports/report-data.ts
|
|
14
14
|
var PREVIEW_WINDOW_DAYS = 30;
|
|
@@ -60,4 +60,4 @@ export {
|
|
|
60
60
|
scoresFromRow,
|
|
61
61
|
buildReportDataForSite
|
|
62
62
|
};
|
|
63
|
-
//# sourceMappingURL=chunk-
|
|
63
|
+
//# sourceMappingURL=chunk-HEWSRXYV.js.map
|
|
@@ -0,0 +1,335 @@
|
|
|
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
|
+
var str = (v) => typeof v === "string" ? v : null;
|
|
29
|
+
var num = (v) => typeof v === "number" ? v : null;
|
|
30
|
+
var bool = (v) => typeof v === "number" ? v !== 0 : null;
|
|
31
|
+
function parseAwc(raw) {
|
|
32
|
+
if (typeof raw !== "string" || raw.trim() === "") return [];
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
if (!Array.isArray(parsed)) return [];
|
|
36
|
+
return parsed.filter((x) => typeof x === "string").map((s) => s.trim()).filter(Boolean);
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function rowFromJoined(r) {
|
|
42
|
+
const name = String(r.name ?? "");
|
|
43
|
+
const headerFilename = str(r.header_image_filename);
|
|
44
|
+
const headerType = str(r.header_image_type);
|
|
45
|
+
return {
|
|
46
|
+
id: String(r.id),
|
|
47
|
+
name,
|
|
48
|
+
url: str(r.url) ?? "",
|
|
49
|
+
// `sites.status` holds the RAW Airtable cell (the importer stores it verbatim
|
|
50
|
+
// so the hourly parity check compares raw-to-raw). Canonicalizing HERE — the
|
|
51
|
+
// twin of mapRow's seam — is what keeps the #558 equivalence instrument green.
|
|
52
|
+
status: canonicalizeStatus(r.status),
|
|
53
|
+
statusRaw: str(r.status),
|
|
54
|
+
pointOfContact: str(r.point_of_contact),
|
|
55
|
+
maintenanceFreq: toFrequency(r.maintenance_freq, `${name} maintenance`),
|
|
56
|
+
testingFreq: toFrequency(r.testing_freq, `${name} testing`),
|
|
57
|
+
maintenanceFreqRaw: str(r.maintenance_freq),
|
|
58
|
+
testingFreqRaw: str(r.testing_freq),
|
|
59
|
+
maintenanceDay: str(r.maintenance_day),
|
|
60
|
+
testingDay: str(r.testing_day),
|
|
61
|
+
ga4PropertyId: str(r.ga4_property_id),
|
|
62
|
+
searchQuery: str(r.search_query),
|
|
63
|
+
searchConsoleProperty: str(r.search_console_property),
|
|
64
|
+
analyticsSoftFailAt: str(r.analytics_soft_fail_at),
|
|
65
|
+
gitRepo: str(r.git_repo),
|
|
66
|
+
reportRecipientsTo: str(r.report_recipients_to),
|
|
67
|
+
reportRecipientsCc: str(r.report_recipients_cc),
|
|
68
|
+
acceptedWatchConditions: parseAwc(r.accepted_watch_conditions),
|
|
69
|
+
headerImage: headerFilename !== null && headerType !== null ? { url: "", filename: headerFilename, type: headerType } : null,
|
|
70
|
+
pScore: num(r.p_score),
|
|
71
|
+
rScore: num(r.r_score),
|
|
72
|
+
bpScore: num(r.bp_score),
|
|
73
|
+
seoScore: num(r.seo_score),
|
|
74
|
+
lastLighthouseAuditAt: str(r.lighthouse_at),
|
|
75
|
+
a11yViolations: num(r.a11y_violations),
|
|
76
|
+
depsDrifted: num(r.deps_drifted),
|
|
77
|
+
depsMajorBehind: num(r.deps_major_behind),
|
|
78
|
+
depsOutdated: num(r.deps_outdated),
|
|
79
|
+
depsMajorOutdated: num(r.deps_major_outdated),
|
|
80
|
+
securityVulnsCritical: num(r.vulns_critical),
|
|
81
|
+
securityVulnsHigh: num(r.vulns_high),
|
|
82
|
+
securityVulnsModerate: num(r.vulns_moderate),
|
|
83
|
+
securityVulnsLow: num(r.vulns_low),
|
|
84
|
+
securityAutoFixAttempts: num(r.auto_fix_attempts),
|
|
85
|
+
lastSecurityAuditAt: str(r.security_audit_at),
|
|
86
|
+
securityAdvisories: parseSecurityAdvisories(r.security_advisories),
|
|
87
|
+
certDaysRemaining: num(r.cert_days_remaining),
|
|
88
|
+
domainCheckedAt: str(r.domain_checked_at),
|
|
89
|
+
netlifyId: trimToNull(r.netlify_id),
|
|
90
|
+
deployStatus: str(r.deploy_status),
|
|
91
|
+
lastDeployAt: str(r.last_deploy_at),
|
|
92
|
+
deployLogUrl: str(r.deploy_log_url),
|
|
93
|
+
deployCheckedAt: str(r.deploy_checked_at),
|
|
94
|
+
functionHealth: toVerdict(r.function_health),
|
|
95
|
+
cmsReachable: toVerdict(r.cms_reachable),
|
|
96
|
+
turnstileWidget: toVerdict(r.turnstile_widget),
|
|
97
|
+
functionHealthCheckedAt: str(r.function_health_checked_at),
|
|
98
|
+
crossbrowserOk: bool(r.crossbrowser_ok),
|
|
99
|
+
mobileOk: bool(r.mobile_ok),
|
|
100
|
+
linksOk: bool(r.links_ok),
|
|
101
|
+
brokenLinks: num(r.broken_links),
|
|
102
|
+
browserCheckedAt: str(r.browser_checked_at),
|
|
103
|
+
reachableOk: toVerdict(r.uptime_reachable),
|
|
104
|
+
titleMetaOk: toVerdict(r.titles_meta_ok),
|
|
105
|
+
copyIntro: trimToNull(r.copy_intro),
|
|
106
|
+
copyContact: trimToNull(r.copy_contact),
|
|
107
|
+
copyFooter: trimToNull(r.copy_footer),
|
|
108
|
+
launchedAt: str(r.launched_at),
|
|
109
|
+
newsletterWebhook: trimToNull(r.newsletter_webhook),
|
|
110
|
+
notifyRouting: parseNotifyRouting(r.notify_routing),
|
|
111
|
+
notifyRoutingRaw: trimToNull(r.notify_routing),
|
|
112
|
+
mailchimpApiKey: trimToNull(r.mailchimp_api_key),
|
|
113
|
+
mailchimpAudienceId: trimToNull(r.mailchimp_audience_id),
|
|
114
|
+
requireTurnstile: typeof r.require_turnstile === "number" ? r.require_turnstile !== 0 : false,
|
|
115
|
+
renovateFailingCis: num(r.renovate_failing_cis),
|
|
116
|
+
defaultBranchCi: str(r.default_branch_ci),
|
|
117
|
+
lastCommitAt: str(r.last_commit_at),
|
|
118
|
+
githubSignalsAt: str(r.github_signals_at),
|
|
119
|
+
smokeOk: toVerdict(r.smoke_ok),
|
|
120
|
+
lastSmokeAt: str(r.last_smoke_at),
|
|
121
|
+
formE2eOk: toVerdict(r.form_e2e_ok),
|
|
122
|
+
formE2eCheckedAt: str(r.form_e2e_checked_at),
|
|
123
|
+
prismicModels: toPrismicModelsVerdict(r.prismic_models),
|
|
124
|
+
prismicModelsCheckedAt: str(r.prismic_models_checked_at),
|
|
125
|
+
prismicModelsDrift: str(r.prismic_models_drift),
|
|
126
|
+
prismicAckUntil: str(r.prismic_ack_until),
|
|
127
|
+
nextMaintenanceAt: str(r.next_maintenance_at),
|
|
128
|
+
nextTestingAt: str(r.next_testing_at)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
var SITE_COLUMNS = [
|
|
132
|
+
"sites.id",
|
|
133
|
+
"sites.slug",
|
|
134
|
+
"sites.name",
|
|
135
|
+
"sites.url",
|
|
136
|
+
"sites.status",
|
|
137
|
+
"sites.point_of_contact",
|
|
138
|
+
"sites.maintenance_freq",
|
|
139
|
+
"sites.testing_freq",
|
|
140
|
+
"sites.maintenance_day",
|
|
141
|
+
"sites.testing_day",
|
|
142
|
+
"sites.ga4_property_id",
|
|
143
|
+
"sites.search_query",
|
|
144
|
+
"sites.search_console_property",
|
|
145
|
+
"sites.git_repo",
|
|
146
|
+
"sites.netlify_id",
|
|
147
|
+
"sites.report_recipients_to",
|
|
148
|
+
"sites.report_recipients_cc",
|
|
149
|
+
"sites.copy_intro",
|
|
150
|
+
"sites.copy_contact",
|
|
151
|
+
"sites.copy_footer",
|
|
152
|
+
"sites.newsletter_webhook",
|
|
153
|
+
"sites.mailchimp_api_key",
|
|
154
|
+
"sites.mailchimp_audience_id",
|
|
155
|
+
"sites.notify_routing",
|
|
156
|
+
"sites.require_turnstile",
|
|
157
|
+
"sites.accepted_watch_conditions",
|
|
158
|
+
"sites.prismic_ack_until",
|
|
159
|
+
"sites.launched_at",
|
|
160
|
+
"sites.header_image_filename",
|
|
161
|
+
"sites.header_image_type",
|
|
162
|
+
"sites.header_image_generated_at",
|
|
163
|
+
"sites.legacy"
|
|
164
|
+
];
|
|
165
|
+
function joined(db) {
|
|
166
|
+
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"]);
|
|
167
|
+
}
|
|
168
|
+
async function mirrorSiteField(db, siteId, airtableColumn, value) {
|
|
169
|
+
await mirrorSiteFields(db, siteId, { [airtableColumn]: value });
|
|
170
|
+
}
|
|
171
|
+
async function mirrorSiteFields(db, siteId, fields) {
|
|
172
|
+
const patch = {};
|
|
173
|
+
for (const [airtableColumn, value] of Object.entries(fields)) {
|
|
174
|
+
const col = SITE_FIELDS[airtableColumn];
|
|
175
|
+
if (!col)
|
|
176
|
+
throw new Error(`mirrorSiteFields: importer claims no sites column for '${airtableColumn}'`);
|
|
177
|
+
patch[col] = siteValueFor(col, value);
|
|
178
|
+
}
|
|
179
|
+
if (Object.keys(patch).length === 0) return true;
|
|
180
|
+
const res = await db.updateTable("sites").set(patch).where("id", "=", siteId).executeTakeFirst();
|
|
181
|
+
return res.numUpdatedRows > 0n;
|
|
182
|
+
}
|
|
183
|
+
async function mirrorSiteInsert(db, rec, computedAt) {
|
|
184
|
+
const { site, health, schedule } = mapWebsiteRecord(rec, computedAt);
|
|
185
|
+
await db.insertInto("sites").values(site).onConflict((oc) => oc.column("id").doUpdateSet(site)).execute();
|
|
186
|
+
await db.insertInto("site_health").values(health).onConflict((oc) => oc.column("site_id").doUpdateSet(health)).execute();
|
|
187
|
+
await db.insertInto("site_schedule").values(schedule).onConflict((oc) => oc.column("site_id").doUpdateSet(schedule)).execute();
|
|
188
|
+
}
|
|
189
|
+
async function mirrorHealthFields(db, siteId, fields) {
|
|
190
|
+
const patch = {};
|
|
191
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
192
|
+
const m = healthColumnFor(field);
|
|
193
|
+
if (!m)
|
|
194
|
+
throw new Error(`mirrorHealthFields: importer claims no site_health column for '${field}'`);
|
|
195
|
+
patch[m.col] = m.coerce(value);
|
|
196
|
+
}
|
|
197
|
+
if (Object.keys(patch).length === 0) return true;
|
|
198
|
+
const res = await db.updateTable("site_health").set(patch).where("site_id", "=", siteId).executeTakeFirst();
|
|
199
|
+
return res.numUpdatedRows > 0n;
|
|
200
|
+
}
|
|
201
|
+
async function mirrorScheduleFields(db, siteId, fields, computedAt) {
|
|
202
|
+
const patch = {};
|
|
203
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
204
|
+
const m = scheduleColumnFor(field);
|
|
205
|
+
if (!m)
|
|
206
|
+
throw new Error(
|
|
207
|
+
`mirrorScheduleFields: importer claims no site_schedule column for '${field}'`
|
|
208
|
+
);
|
|
209
|
+
patch[m.col] = m.coerce(value);
|
|
210
|
+
}
|
|
211
|
+
if (Object.keys(patch).length === 0) return true;
|
|
212
|
+
patch.computed_at = computedAt;
|
|
213
|
+
const res = await db.updateTable("site_schedule").set(patch).where("site_id", "=", siteId).executeTakeFirst();
|
|
214
|
+
return res.numUpdatedRows > 0n;
|
|
215
|
+
}
|
|
216
|
+
async function getSiteBySlug(db, slug) {
|
|
217
|
+
const r = await joined(db).where("sites.slug", "=", slug).executeTakeFirst();
|
|
218
|
+
return r ? rowFromJoined(r) : null;
|
|
219
|
+
}
|
|
220
|
+
async function getSiteById(db, id) {
|
|
221
|
+
const r = await joined(db).where("sites.id", "=", id).executeTakeFirst();
|
|
222
|
+
return r ? rowFromJoined(r) : null;
|
|
223
|
+
}
|
|
224
|
+
async function listSites(db) {
|
|
225
|
+
const rows = await joined(db).orderBy("sites.name").execute();
|
|
226
|
+
return rows.map((r) => rowFromJoined(r));
|
|
227
|
+
}
|
|
228
|
+
var CHECKLIST_FIELD_BY_KEY = new Map(
|
|
229
|
+
[...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map((i) => [i.key, i.field])
|
|
230
|
+
);
|
|
231
|
+
function checklistFromStored(raw) {
|
|
232
|
+
let stored = {};
|
|
233
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(raw);
|
|
236
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
237
|
+
stored = parsed;
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const out = {};
|
|
242
|
+
for (const [key, field] of CHECKLIST_FIELD_BY_KEY) out[field] = Boolean(stored[key]);
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
function reportRowFromDb(r) {
|
|
246
|
+
const p = r.lighthouse_performance;
|
|
247
|
+
const a = r.lighthouse_accessibility;
|
|
248
|
+
const b = r.lighthouse_best_practices;
|
|
249
|
+
const seo = r.lighthouse_seo;
|
|
250
|
+
const lighthouse = typeof p === "number" && typeof a === "number" && typeof b === "number" && typeof seo === "number" ? { performance: p, accessibility: a, bestPractices: b, seo } : null;
|
|
251
|
+
return {
|
|
252
|
+
id: r.id,
|
|
253
|
+
reportId: r.report_id ?? "",
|
|
254
|
+
siteId: r.site_id ?? "",
|
|
255
|
+
reportType: toReportType(r.report_type ?? void 0),
|
|
256
|
+
period: r.period,
|
|
257
|
+
periodStart: r.period_start,
|
|
258
|
+
periodEnd: r.period_end,
|
|
259
|
+
completedOn: r.completed_on,
|
|
260
|
+
lighthouse,
|
|
261
|
+
gaUsersCurrent: r.ga_users_current,
|
|
262
|
+
gaUsersPrevious: r.ga_users_previous,
|
|
263
|
+
searchFoundPage1: bool(r.search_found_page1),
|
|
264
|
+
searchPosition: r.search_position,
|
|
265
|
+
lastTestedDate: r.last_tested_date,
|
|
266
|
+
commentary: r.commentary,
|
|
267
|
+
subjectOverride: r.subject_override,
|
|
268
|
+
draftReady: r.draft_ready !== 0,
|
|
269
|
+
approvedToSend: r.approved_to_send !== 0,
|
|
270
|
+
sentAt: r.sent_at,
|
|
271
|
+
approvedAt: r.approved_at,
|
|
272
|
+
approvedBy: r.approved_by,
|
|
273
|
+
deliveryStatus: r.delivery_status ?? "pending",
|
|
274
|
+
// The body lives IN the row (rendered_html) — the link points at the
|
|
275
|
+
// dashboard's own preview route instead of an EXPIRING Airtable signed URL.
|
|
276
|
+
// Strictly better for the operator: the old link 404'd once the URL aged out.
|
|
277
|
+
renderedHtmlAttachment: r.rendered_html !== null ? { url: `/api/reports/${r.id}/preview`, filename: `${r.report_id ?? r.id}.html` } : null,
|
|
278
|
+
resendMessageId: r.resend_message_id,
|
|
279
|
+
checklist: checklistFromStored(r.checklist),
|
|
280
|
+
autoEvidence: parseAutoEvidence(r.checklist_auto_evidence),
|
|
281
|
+
sendOverride: r.send_override !== 0,
|
|
282
|
+
overrideReason: r.override_reason,
|
|
283
|
+
overrideBy: r.override_by,
|
|
284
|
+
overrideAt: r.override_at
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async function listAllReports(db) {
|
|
288
|
+
const rows = await db.selectFrom("reports").selectAll().orderBy("period_start", "desc").orderBy("id").execute();
|
|
289
|
+
return rows.map(reportRowFromDb);
|
|
290
|
+
}
|
|
291
|
+
async function listReportsForSite(db, siteId) {
|
|
292
|
+
const rows = await db.selectFrom("reports").selectAll().where("site_id", "=", siteId).orderBy("period_start", "desc").orderBy("id").execute();
|
|
293
|
+
return rows.map(reportRowFromDb);
|
|
294
|
+
}
|
|
295
|
+
async function mirrorReportPatch(db, reportId, patch) {
|
|
296
|
+
if (Object.keys(patch).length === 0) return;
|
|
297
|
+
await db.updateTable("reports").set(patch).where("id", "=", reportId).execute();
|
|
298
|
+
}
|
|
299
|
+
async function mirrorReportInsert(db, rec) {
|
|
300
|
+
const row = mapReportRecord(rec, null);
|
|
301
|
+
const { rendered_html: _rh, ...rowSansHtml } = row;
|
|
302
|
+
void _rh;
|
|
303
|
+
await db.insertInto("reports").values(row).onConflict((oc) => oc.column("id").doUpdateSet(rowSansHtml)).execute();
|
|
304
|
+
}
|
|
305
|
+
async function storeRenderedHtml(db, reportId, html) {
|
|
306
|
+
await db.updateTable("reports").set({ rendered_html: html }).where("id", "=", reportId).execute();
|
|
307
|
+
}
|
|
308
|
+
async function getReportById(db, id) {
|
|
309
|
+
const r = await db.selectFrom("reports").selectAll().where("id", "=", id).executeTakeFirst();
|
|
310
|
+
return r ? reportRowFromDb(r) : null;
|
|
311
|
+
}
|
|
312
|
+
async function getReportHtml(db, id) {
|
|
313
|
+
const r = await db.selectFrom("reports").select(["rendered_html", "report_id"]).where("id", "=", id).executeTakeFirst();
|
|
314
|
+
if (!r || r.rendered_html === null) return null;
|
|
315
|
+
return { html: r.rendered_html, reportId: r.report_id };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export {
|
|
319
|
+
mirrorSiteField,
|
|
320
|
+
mirrorSiteFields,
|
|
321
|
+
mirrorSiteInsert,
|
|
322
|
+
mirrorHealthFields,
|
|
323
|
+
mirrorScheduleFields,
|
|
324
|
+
getSiteBySlug,
|
|
325
|
+
getSiteById,
|
|
326
|
+
listSites,
|
|
327
|
+
listAllReports,
|
|
328
|
+
listReportsForSite,
|
|
329
|
+
mirrorReportPatch,
|
|
330
|
+
mirrorReportInsert,
|
|
331
|
+
storeRenderedHtml,
|
|
332
|
+
getReportById,
|
|
333
|
+
getReportHtml
|
|
334
|
+
};
|
|
335
|
+
//# sourceMappingURL=chunk-HHMMUR4P.js.map
|