@reddoorla/maintenance 0.67.0 → 0.69.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.
Files changed (99) hide show
  1. package/README.md +16 -0
  2. package/dist/airtable-WGQQ3KAD.js +9 -0
  3. package/dist/{announce-74ZIAV26.js → announce-72R3LRNT.js} +9 -11
  4. package/dist/{announce-74ZIAV26.js.map → announce-72R3LRNT.js.map} +1 -1
  5. package/dist/{bump-deps-HYBYNPMY.js → bump-deps-QNM3H4GI.js} +2 -2
  6. package/dist/{chunk-YKOOAWVL.js → chunk-2DCA7FHZ.js} +21 -15
  7. package/dist/chunk-2DCA7FHZ.js.map +1 -0
  8. package/dist/{chunk-LCKKIJ22.js → chunk-42KKVLYN.js} +3 -3
  9. package/dist/{chunk-VKAYZVMI.js → chunk-45AUGDFR.js} +2 -1
  10. package/dist/chunk-45AUGDFR.js.map +1 -0
  11. package/dist/{chunk-WCN54744.js → chunk-6G7BC5SN.js} +4 -4
  12. package/dist/{chunk-SYTPVYIK.js → chunk-6JFOAPW4.js} +241 -4
  13. package/dist/chunk-6JFOAPW4.js.map +1 -0
  14. package/dist/{chunk-Y4O44LZB.js → chunk-6MT5W6UF.js} +2 -2
  15. package/dist/{chunk-OAMJINM5.js → chunk-A5KYRGQA.js} +4 -4
  16. package/dist/{chunk-JDGIO54P.js → chunk-AESJBEM7.js} +43 -3
  17. package/dist/chunk-AESJBEM7.js.map +1 -0
  18. package/dist/{chunk-CL5DYUWQ.js → chunk-C3PDS623.js} +3 -1
  19. package/dist/chunk-C3PDS623.js.map +1 -0
  20. package/dist/chunk-E2GKKUEX.js +322 -0
  21. package/dist/chunk-E2GKKUEX.js.map +1 -0
  22. package/dist/{chunk-KN5OC5YH.js → chunk-GKSXZIMX.js} +3 -3
  23. package/dist/{chunk-XVR7UM6Z.js → chunk-JNXCDUYB.js} +4 -4
  24. package/dist/{chunk-ULL4AD3Y.js → chunk-O6G4HHMF.js} +7 -9
  25. package/dist/{chunk-ULL4AD3Y.js.map → chunk-O6G4HHMF.js.map} +1 -1
  26. package/dist/{chunk-6LIWDIXU.js → chunk-RINXPU3H.js} +2 -2
  27. package/dist/{chunk-T34EUM5B.js → chunk-XWPZ46IN.js} +2 -2
  28. package/dist/{chunk-7MJJEEII.js → chunk-YQMYQAJX.js} +2 -2
  29. package/dist/cli/bin.js +32 -14
  30. package/dist/cli/bin.js.map +1 -1
  31. package/dist/cli/commands/audit.js +8 -8
  32. package/dist/client/index.d.ts +84 -0
  33. package/dist/client/index.js +47 -0
  34. package/dist/client/index.js.map +1 -0
  35. package/dist/{convert-to-pnpm-5MPZWFHM.js → convert-to-pnpm-XH7NYRVK.js} +2 -2
  36. package/dist/{digest-MECMZPFE.js → digest-Q2TAQHXF.js} +17 -8
  37. package/dist/digest-Q2TAQHXF.js.map +1 -0
  38. package/dist/ensure-site-2PF522VL.js +87 -0
  39. package/dist/ensure-site-2PF522VL.js.map +1 -0
  40. package/dist/{github-signals-VWBMQNLH.js → github-signals-CLQC2IPY.js} +7 -7
  41. package/dist/index.d.ts +7 -1
  42. package/dist/index.js +103 -37
  43. package/dist/index.js.map +1 -1
  44. package/dist/{init-X3BCJAUG.js → init-UA6D7GKW.js} +7 -7
  45. package/dist/{launch-NQCL5K5H.js → launch-AENAS2O2.js} +15 -17
  46. package/dist/{launch-NQCL5K5H.js.map → launch-AENAS2O2.js.map} +1 -1
  47. package/dist/{lighthouse-airtable-QUNOSDT5.js → lighthouse-airtable-UBSXKHAQ.js} +3 -3
  48. package/dist/{onboard-7JOBZY35.js → onboard-X6KJ5Z2V.js} +2 -2
  49. package/dist/{orchestrate-JQX72RJE.js → orchestrate-GADNFGDH.js} +5 -7
  50. package/dist/preflight-D5RQGX4V.js +64 -0
  51. package/dist/preflight-D5RQGX4V.js.map +1 -0
  52. package/dist/recipes/sync-configs.js +2 -2
  53. package/dist/{renovate-dispatch-PWXFZNHN.js → renovate-dispatch-LG4RJGDY.js} +2 -2
  54. package/dist/{report-QUKNKUUE.js → report-3FJ224H5.js} +8 -9
  55. package/dist/{report-QUKNKUUE.js.map → report-3FJ224H5.js.map} +1 -1
  56. package/dist/{self-updating-OKJKU2FE.js → self-updating-JTTFOJ7Q.js} +2 -2
  57. package/dist/{selftest-6JVYR24F.js → selftest-55OJIWEB.js} +10 -12
  58. package/dist/{selftest-6JVYR24F.js.map → selftest-55OJIWEB.js.map} +1 -1
  59. package/dist/{svelte-codemods-GG6EEES5.js → svelte-codemods-KJLNH6FQ.js} +2 -2
  60. package/dist/{sync-configs-UFHZZPL7.js → sync-configs-TIBO6DUJ.js} +4 -4
  61. package/dist/{upgrade-S25N3473.js → upgrade-TPTE26FG.js} +2 -2
  62. package/dist/{websites-U3UZS5VG.js → websites-6YTT5ZTO.js} +2 -2
  63. package/dist/{write-audits-to-airtable-GAJUCSTY.js → write-audits-to-airtable-IWOO4FFH.js} +4 -4
  64. package/package.json +5 -1
  65. package/dist/airtable-33SY2UBZ.js +0 -9
  66. package/dist/chunk-CL5DYUWQ.js.map +0 -1
  67. package/dist/chunk-JDGIO54P.js.map +0 -1
  68. package/dist/chunk-OCS7SZUZ.js +0 -243
  69. package/dist/chunk-OCS7SZUZ.js.map +0 -1
  70. package/dist/chunk-OGGRFBL4.js +0 -14
  71. package/dist/chunk-OGGRFBL4.js.map +0 -1
  72. package/dist/chunk-SYTPVYIK.js.map +0 -1
  73. package/dist/chunk-VKAYZVMI.js.map +0 -1
  74. package/dist/chunk-YKOOAWVL.js.map +0 -1
  75. package/dist/digest-MECMZPFE.js.map +0 -1
  76. /package/dist/{airtable-33SY2UBZ.js.map → airtable-WGQQ3KAD.js.map} +0 -0
  77. /package/dist/{bump-deps-HYBYNPMY.js.map → bump-deps-QNM3H4GI.js.map} +0 -0
  78. /package/dist/{chunk-LCKKIJ22.js.map → chunk-42KKVLYN.js.map} +0 -0
  79. /package/dist/{chunk-WCN54744.js.map → chunk-6G7BC5SN.js.map} +0 -0
  80. /package/dist/{chunk-Y4O44LZB.js.map → chunk-6MT5W6UF.js.map} +0 -0
  81. /package/dist/{chunk-OAMJINM5.js.map → chunk-A5KYRGQA.js.map} +0 -0
  82. /package/dist/{chunk-KN5OC5YH.js.map → chunk-GKSXZIMX.js.map} +0 -0
  83. /package/dist/{chunk-XVR7UM6Z.js.map → chunk-JNXCDUYB.js.map} +0 -0
  84. /package/dist/{chunk-6LIWDIXU.js.map → chunk-RINXPU3H.js.map} +0 -0
  85. /package/dist/{chunk-T34EUM5B.js.map → chunk-XWPZ46IN.js.map} +0 -0
  86. /package/dist/{chunk-7MJJEEII.js.map → chunk-YQMYQAJX.js.map} +0 -0
  87. /package/dist/{convert-to-pnpm-5MPZWFHM.js.map → convert-to-pnpm-XH7NYRVK.js.map} +0 -0
  88. /package/dist/{github-signals-VWBMQNLH.js.map → github-signals-CLQC2IPY.js.map} +0 -0
  89. /package/dist/{init-X3BCJAUG.js.map → init-UA6D7GKW.js.map} +0 -0
  90. /package/dist/{lighthouse-airtable-QUNOSDT5.js.map → lighthouse-airtable-UBSXKHAQ.js.map} +0 -0
  91. /package/dist/{onboard-7JOBZY35.js.map → onboard-X6KJ5Z2V.js.map} +0 -0
  92. /package/dist/{orchestrate-JQX72RJE.js.map → orchestrate-GADNFGDH.js.map} +0 -0
  93. /package/dist/{renovate-dispatch-PWXFZNHN.js.map → renovate-dispatch-LG4RJGDY.js.map} +0 -0
  94. /package/dist/{self-updating-OKJKU2FE.js.map → self-updating-JTTFOJ7Q.js.map} +0 -0
  95. /package/dist/{svelte-codemods-GG6EEES5.js.map → svelte-codemods-KJLNH6FQ.js.map} +0 -0
  96. /package/dist/{sync-configs-UFHZZPL7.js.map → sync-configs-TIBO6DUJ.js.map} +0 -0
  97. /package/dist/{upgrade-S25N3473.js.map → upgrade-TPTE26FG.js.map} +0 -0
  98. /package/dist/{websites-U3UZS5VG.js.map → websites-6YTT5ZTO.js.map} +0 -0
  99. /package/dist/{write-audits-to-airtable-GAJUCSTY.js.map → write-audits-to-airtable-IWOO4FFH.js.map} +0 -0
@@ -8,19 +8,19 @@ import {
8
8
  } from "../../chunk-4NAOR44V.js";
9
9
  import {
10
10
  resolveSites
11
- } from "../../chunk-7MJJEEII.js";
11
+ } from "../../chunk-YQMYQAJX.js";
12
12
  import {
13
13
  ALL_AUDIT_NAMES,
14
14
  runOneAudit
15
- } from "../../chunk-WCN54744.js";
15
+ } from "../../chunk-6G7BC5SN.js";
16
+ import "../../chunk-GASBX52O.js";
17
+ import "../../chunk-YEGHUODU.js";
16
18
  import "../../chunk-LKGVSM2O.js";
17
19
  import {
18
20
  recordFleetEventsBestEffort
19
21
  } from "../../chunk-OTBA7DON.js";
20
22
  import "../../chunk-PH3AGF4Z.js";
21
23
  import "../../chunk-HIN3P4ZN.js";
22
- import "../../chunk-GASBX52O.js";
23
- import "../../chunk-YEGHUODU.js";
24
24
  import "../../chunk-FDIFTIXC.js";
25
25
  import "../../chunk-HVOOCK6L.js";
26
26
  import "../../chunk-XXTZBPUY.js";
@@ -231,9 +231,9 @@ ${skipNotice}`;
231
231
  let writeBackFailed = false;
232
232
  if (opts.writeAirtable !== void 0) {
233
233
  const { openBase, readAirtableConfig } = await import("../../client-EPVO3FEH.js");
234
- const { listWebsites } = await import("../../websites-U3UZS5VG.js");
234
+ const { listWebsites } = await import("../../websites-6YTT5ZTO.js");
235
235
  if (opts.fleet !== void 0) {
236
- const { writeFleetAuditsToAirtable, formatFleetWriteSummary } = await import("../../write-audits-to-airtable-GAJUCSTY.js");
236
+ const { writeFleetAuditsToAirtable, formatFleetWriteSummary } = await import("../../write-audits-to-airtable-IWOO4FFH.js");
237
237
  const base = openBase(readAirtableConfig());
238
238
  const websites = await listWebsites(base);
239
239
  const fleetWrite = await writeFleetAuditsToAirtable({ base, websites, results });
@@ -249,8 +249,8 @@ ${skipNotice}`;
249
249
 
250
250
  ${formatFleetWriteSummary(fleetWrite)}`;
251
251
  } else {
252
- const { resolveSlugFromCwd } = await import("../../lighthouse-airtable-QUNOSDT5.js");
253
- const { writeAuditsToAirtable } = await import("../../write-audits-to-airtable-GAJUCSTY.js");
252
+ const { resolveSlugFromCwd } = await import("../../lighthouse-airtable-UBSXKHAQ.js");
253
+ const { writeAuditsToAirtable } = await import("../../write-audits-to-airtable-IWOO4FFH.js");
254
254
  const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd(cwd);
255
255
  let writeSummary = null;
256
256
  await new Listr(
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Load-aware page readiness for splash screens and intro overlays.
3
+ *
4
+ * Every splash in the fleet today hides the page behind a blind `setTimeout`,
5
+ * which punishes fast loads (a fully-painted page sits under an opaque cover
6
+ * for the full timer) and can still under-shoot slow ones. `whenPageReady`
7
+ * replaces the timer with real signals — eager-image settlement, document
8
+ * load, caller-supplied promises — bracketed by a floor (`minMs`, so a brand
9
+ * moment never flashes) and a ceiling (`maxMs`, so a stalled resource can
10
+ * never wedge the splash open).
11
+ *
12
+ * Framework-free and SSR-safe: with no DOM present it resolves immediately
13
+ * with reason "no-dom", so it can be called unguarded from universal code.
14
+ * The `env` parameter exists for tests (vitest runs in a node environment);
15
+ * production callers omit it and the globals are used.
16
+ *
17
+ * Local structural types stand in for DOM lib types because this package
18
+ * compiles with `lib: ["ES2022"]` and no DOM — same approach as
19
+ * src/configs/svelte.ts, which declares minimal shapes to avoid a peer dep.
20
+ */
21
+ type ListenerTarget = {
22
+ addEventListener(type: string, listener: () => void, options?: {
23
+ once?: boolean;
24
+ }): void;
25
+ };
26
+ type ImageLike = ListenerTarget & {
27
+ /** True once the load attempt is terminal — fires for both success AND error. */
28
+ complete: boolean;
29
+ };
30
+ type DocumentLike = ListenerTarget & {
31
+ readyState: string;
32
+ querySelectorAll(selector: string): ArrayLike<ImageLike>;
33
+ };
34
+ type WindowLike = ListenerTarget & {
35
+ matchMedia?(query: string): {
36
+ matches: boolean;
37
+ };
38
+ };
39
+ type PageReadyEnv = {
40
+ document?: DocumentLike | undefined;
41
+ window?: WindowLike | undefined;
42
+ };
43
+ type PageReadyOptions = {
44
+ /** Never resolve before this many ms — keeps a splash from flashing. Default 400. */
45
+ minMs?: number;
46
+ /** Always resolve by this many ms, loaded or not. Default 2500. */
47
+ maxMs?: number;
48
+ /**
49
+ * Selector for images that must settle (load OR error) before the page
50
+ * counts as ready. Defaults to `img[loading="eager"]` — above-the-fold
51
+ * imagery. Pass `false` to skip image tracking.
52
+ */
53
+ waitForImages?: string | false;
54
+ /**
55
+ * Also wait for `document.readyState === "complete"` (the window `load`
56
+ * event). Default false: full load includes every iframe and third-party
57
+ * script, which routinely lags the meaningful first paint by seconds —
58
+ * eager images are usually the right signal for a splash.
59
+ */
60
+ waitForDocument?: boolean;
61
+ /** Extra readiness signals (e.g. a video poster decode). Rejections count as settled. */
62
+ signals?: ReadonlyArray<Promise<unknown>>;
63
+ };
64
+ type PageReadyReason = "ready" | "timeout" | "no-dom";
65
+ /**
66
+ * Resolve when the page is visually ready — or at `maxMs`, whichever comes
67
+ * first — but never before `minMs`. The returned reason says which path won.
68
+ *
69
+ * @example Reveal a splash overlay when the hero has painted:
70
+ * ```ts
71
+ * onMount(() => {
72
+ * whenPageReady({ minMs: 600, maxMs: 2400 }).then(() => (isReady = true));
73
+ * });
74
+ * ```
75
+ */
76
+ declare function whenPageReady(options?: PageReadyOptions, env?: PageReadyEnv): Promise<PageReadyReason>;
77
+ /**
78
+ * True when the user has asked the OS for reduced motion. Splash callers
79
+ * should collapse `minMs` to 0 and skip decorative fades when this is set.
80
+ * False when no DOM (SSR) or `matchMedia` is unavailable.
81
+ */
82
+ declare function prefersReducedMotion(env?: PageReadyEnv): boolean;
83
+
84
+ export { type PageReadyEnv, type PageReadyOptions, type PageReadyReason, prefersReducedMotion, whenPageReady };
@@ -0,0 +1,47 @@
1
+ // src/client/page-ready.ts
2
+ var DEFAULT_MIN_MS = 400;
3
+ var DEFAULT_MAX_MS = 2500;
4
+ var DEFAULT_IMAGE_SELECTOR = 'img[loading="eager"]';
5
+ function globals() {
6
+ return globalThis;
7
+ }
8
+ function delay(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
10
+ }
11
+ function documentComplete(doc, win) {
12
+ if (doc.readyState === "complete") return Promise.resolve();
13
+ return new Promise((resolve) => win.addEventListener("load", () => resolve(), { once: true }));
14
+ }
15
+ function imageSettled(img) {
16
+ if (img.complete) return Promise.resolve();
17
+ return new Promise((resolve) => {
18
+ img.addEventListener("load", () => resolve(), { once: true });
19
+ img.addEventListener("error", () => resolve(), { once: true });
20
+ });
21
+ }
22
+ function whenPageReady(options = {}, env = {}) {
23
+ const doc = env.document ?? globals().document;
24
+ const win = env.window ?? globals().window;
25
+ if (!doc || !win) return Promise.resolve("no-dom");
26
+ const minMs = options.minMs ?? DEFAULT_MIN_MS;
27
+ const maxMs = options.maxMs ?? DEFAULT_MAX_MS;
28
+ const selector = options.waitForImages ?? DEFAULT_IMAGE_SELECTOR;
29
+ const waits = [];
30
+ if (options.waitForDocument) waits.push(documentComplete(doc, win));
31
+ if (selector !== false) {
32
+ for (const img of Array.from(doc.querySelectorAll(selector))) waits.push(imageSettled(img));
33
+ }
34
+ for (const signal of options.signals ?? []) waits.push(signal.catch(() => void 0));
35
+ const ready = Promise.all(waits).then(() => "ready");
36
+ const capped = maxMs === Infinity ? ready : Promise.race([ready, delay(maxMs).then(() => "timeout")]);
37
+ return Promise.all([capped, delay(minMs)]).then(([reason]) => reason);
38
+ }
39
+ function prefersReducedMotion(env = {}) {
40
+ const win = env.window ?? globals().window;
41
+ return win?.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
42
+ }
43
+ export {
44
+ prefersReducedMotion,
45
+ whenPageReady
46
+ };
47
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/page-ready.ts"],"sourcesContent":["/**\n * Load-aware page readiness for splash screens and intro overlays.\n *\n * Every splash in the fleet today hides the page behind a blind `setTimeout`,\n * which punishes fast loads (a fully-painted page sits under an opaque cover\n * for the full timer) and can still under-shoot slow ones. `whenPageReady`\n * replaces the timer with real signals — eager-image settlement, document\n * load, caller-supplied promises — bracketed by a floor (`minMs`, so a brand\n * moment never flashes) and a ceiling (`maxMs`, so a stalled resource can\n * never wedge the splash open).\n *\n * Framework-free and SSR-safe: with no DOM present it resolves immediately\n * with reason \"no-dom\", so it can be called unguarded from universal code.\n * The `env` parameter exists for tests (vitest runs in a node environment);\n * production callers omit it and the globals are used.\n *\n * Local structural types stand in for DOM lib types because this package\n * compiles with `lib: [\"ES2022\"]` and no DOM — same approach as\n * src/configs/svelte.ts, which declares minimal shapes to avoid a peer dep.\n */\n\ntype ListenerTarget = {\n addEventListener(type: string, listener: () => void, options?: { once?: boolean }): void;\n};\n\ntype ImageLike = ListenerTarget & {\n /** True once the load attempt is terminal — fires for both success AND error. */\n complete: boolean;\n};\n\ntype DocumentLike = ListenerTarget & {\n readyState: string;\n querySelectorAll(selector: string): ArrayLike<ImageLike>;\n};\n\ntype WindowLike = ListenerTarget & {\n matchMedia?(query: string): { matches: boolean };\n};\n\nexport type PageReadyEnv = {\n document?: DocumentLike | undefined;\n window?: WindowLike | undefined;\n};\n\nexport type PageReadyOptions = {\n /** Never resolve before this many ms — keeps a splash from flashing. Default 400. */\n minMs?: number;\n /** Always resolve by this many ms, loaded or not. Default 2500. */\n maxMs?: number;\n /**\n * Selector for images that must settle (load OR error) before the page\n * counts as ready. Defaults to `img[loading=\"eager\"]` — above-the-fold\n * imagery. Pass `false` to skip image tracking.\n */\n waitForImages?: string | false;\n /**\n * Also wait for `document.readyState === \"complete\"` (the window `load`\n * event). Default false: full load includes every iframe and third-party\n * script, which routinely lags the meaningful first paint by seconds —\n * eager images are usually the right signal for a splash.\n */\n waitForDocument?: boolean;\n /** Extra readiness signals (e.g. a video poster decode). Rejections count as settled. */\n signals?: ReadonlyArray<Promise<unknown>>;\n};\n\nexport type PageReadyReason = \"ready\" | \"timeout\" | \"no-dom\";\n\nconst DEFAULT_MIN_MS = 400;\nconst DEFAULT_MAX_MS = 2500;\nconst DEFAULT_IMAGE_SELECTOR = 'img[loading=\"eager\"]';\n\nfunction globals(): PageReadyEnv {\n return globalThis as unknown as PageReadyEnv;\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));\n}\n\nfunction documentComplete(doc: DocumentLike, win: WindowLike): Promise<void> {\n if (doc.readyState === \"complete\") return Promise.resolve();\n return new Promise((resolve) => win.addEventListener(\"load\", () => resolve(), { once: true }));\n}\n\nfunction imageSettled(img: ImageLike): Promise<void> {\n if (img.complete) return Promise.resolve();\n return new Promise((resolve) => {\n img.addEventListener(\"load\", () => resolve(), { once: true });\n img.addEventListener(\"error\", () => resolve(), { once: true });\n });\n}\n\n/**\n * Resolve when the page is visually ready — or at `maxMs`, whichever comes\n * first — but never before `minMs`. The returned reason says which path won.\n *\n * @example Reveal a splash overlay when the hero has painted:\n * ```ts\n * onMount(() => {\n * whenPageReady({ minMs: 600, maxMs: 2400 }).then(() => (isReady = true));\n * });\n * ```\n */\nexport function whenPageReady(\n options: PageReadyOptions = {},\n env: PageReadyEnv = {},\n): Promise<PageReadyReason> {\n const doc = env.document ?? globals().document;\n const win = env.window ?? globals().window;\n if (!doc || !win) return Promise.resolve(\"no-dom\");\n\n const minMs = options.minMs ?? DEFAULT_MIN_MS;\n const maxMs = options.maxMs ?? DEFAULT_MAX_MS;\n const selector = options.waitForImages ?? DEFAULT_IMAGE_SELECTOR;\n\n const waits: Promise<unknown>[] = [];\n if (options.waitForDocument) waits.push(documentComplete(doc, win));\n if (selector !== false) {\n for (const img of Array.from(doc.querySelectorAll(selector))) waits.push(imageSettled(img));\n }\n for (const signal of options.signals ?? []) waits.push(signal.catch(() => undefined));\n\n const ready = Promise.all(waits).then<PageReadyReason>(() => \"ready\");\n const capped =\n maxMs === Infinity\n ? ready\n : Promise.race([ready, delay(maxMs).then<PageReadyReason>(() => \"timeout\")]);\n\n return Promise.all([capped, delay(minMs)]).then(([reason]) => reason);\n}\n\n/**\n * True when the user has asked the OS for reduced motion. Splash callers\n * should collapse `minMs` to 0 and skip decorative fades when this is set.\n * False when no DOM (SSR) or `matchMedia` is unavailable.\n */\nexport function prefersReducedMotion(env: PageReadyEnv = {}): boolean {\n const win = env.window ?? globals().window;\n return win?.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches ?? false;\n}\n"],"mappings":";AAoEA,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,yBAAyB;AAE/B,SAAS,UAAwB;AAC/B,SAAO;AACT;AAEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC;AACtE;AAEA,SAAS,iBAAiB,KAAmB,KAAgC;AAC3E,MAAI,IAAI,eAAe,WAAY,QAAO,QAAQ,QAAQ;AAC1D,SAAO,IAAI,QAAQ,CAAC,YAAY,IAAI,iBAAiB,QAAQ,MAAM,QAAQ,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC;AAC/F;AAEA,SAAS,aAAa,KAA+B;AACnD,MAAI,IAAI,SAAU,QAAO,QAAQ,QAAQ;AACzC,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI,iBAAiB,QAAQ,MAAM,QAAQ,GAAG,EAAE,MAAM,KAAK,CAAC;AAC5D,QAAI,iBAAiB,SAAS,MAAM,QAAQ,GAAG,EAAE,MAAM,KAAK,CAAC;AAAA,EAC/D,CAAC;AACH;AAaO,SAAS,cACd,UAA4B,CAAC,GAC7B,MAAoB,CAAC,GACK;AAC1B,QAAM,MAAM,IAAI,YAAY,QAAQ,EAAE;AACtC,QAAM,MAAM,IAAI,UAAU,QAAQ,EAAE;AACpC,MAAI,CAAC,OAAO,CAAC,IAAK,QAAO,QAAQ,QAAQ,QAAQ;AAEjD,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,WAAW,QAAQ,iBAAiB;AAE1C,QAAM,QAA4B,CAAC;AACnC,MAAI,QAAQ,gBAAiB,OAAM,KAAK,iBAAiB,KAAK,GAAG,CAAC;AAClE,MAAI,aAAa,OAAO;AACtB,eAAW,OAAO,MAAM,KAAK,IAAI,iBAAiB,QAAQ,CAAC,EAAG,OAAM,KAAK,aAAa,GAAG,CAAC;AAAA,EAC5F;AACA,aAAW,UAAU,QAAQ,WAAW,CAAC,EAAG,OAAM,KAAK,OAAO,MAAM,MAAM,MAAS,CAAC;AAEpF,QAAM,QAAQ,QAAQ,IAAI,KAAK,EAAE,KAAsB,MAAM,OAAO;AACpE,QAAM,SACJ,UAAU,WACN,QACA,QAAQ,KAAK,CAAC,OAAO,MAAM,KAAK,EAAE,KAAsB,MAAM,SAAS,CAAC,CAAC;AAE/E,SAAO,QAAQ,IAAI,CAAC,QAAQ,MAAM,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,MAAM,MAAM,MAAM;AACtE;AAOO,SAAS,qBAAqB,MAAoB,CAAC,GAAY;AACpE,QAAM,MAAM,IAAI,UAAU,QAAQ,EAAE;AACpC,SAAO,KAAK,aAAa,kCAAkC,EAAE,WAAW;AAC1E;","names":[]}
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-4NAOR44V.js";
9
9
  import {
10
10
  resolveSites
11
- } from "./chunk-7MJJEEII.js";
11
+ } from "./chunk-YQMYQAJX.js";
12
12
  import {
13
13
  convertToPnpm
14
14
  } from "./chunk-2X322ZES.js";
@@ -50,4 +50,4 @@ async function runConvertToPnpmCommand(site, opts) {
50
50
  export {
51
51
  runConvertToPnpmCommand
52
52
  };
53
- //# sourceMappingURL=convert-to-pnpm-5MPZWFHM.js.map
53
+ //# sourceMappingURL=convert-to-pnpm-XH7NYRVK.js.map
@@ -3,31 +3,39 @@ import {
3
3
  collectCiAlerts,
4
4
  collectDeliveryFailures,
5
5
  collectLighthouseAlerts,
6
+ collectPreflightBlocked,
6
7
  collectRenovateAlerts,
7
8
  collectVulnAlerts,
8
9
  diffAttention,
9
10
  readDigestState,
10
11
  writeDigestState
11
- } from "./chunk-JDGIO54P.js";
12
+ } from "./chunk-AESJBEM7.js";
13
+ import "./chunk-E2GKKUEX.js";
14
+ import "./chunk-45AUGDFR.js";
12
15
  import {
13
16
  isIdempotencyConflict
14
- } from "./chunk-OGGRFBL4.js";
17
+ } from "./chunk-2DCA7FHZ.js";
18
+ import "./chunk-GECESMVS.js";
15
19
  import {
16
20
  escapeHtml,
17
21
  isPendingApproval,
18
22
  listAllReports
19
- } from "./chunk-OCS7SZUZ.js";
23
+ } from "./chunk-6JFOAPW4.js";
24
+ import {
25
+ defaultResendClient
26
+ } from "./chunk-WEKL2HYJ.js";
27
+ import "./chunk-OTBA7DON.js";
28
+ import "./chunk-PH3AGF4Z.js";
29
+ import "./chunk-HIN3P4ZN.js";
30
+ import "./chunk-U5SZ4FOP.js";
20
31
  import {
21
32
  listWebsites,
22
33
  siteSlug
23
- } from "./chunk-CL5DYUWQ.js";
34
+ } from "./chunk-C3PDS623.js";
24
35
  import {
25
36
  openBase,
26
37
  readAirtableConfig
27
38
  } from "./chunk-HN6I2XWI.js";
28
- import {
29
- defaultResendClient
30
- } from "./chunk-WEKL2HYJ.js";
31
39
  import "./chunk-6VAL7XJT.js";
32
40
 
33
41
  // src/reports/digest.ts
@@ -115,6 +123,7 @@ async function collectAttention(deps) {
115
123
  return [
116
124
  ...runCollector("vuln", () => collectVulnAlerts(websites, deps.baseUrl)),
117
125
  ...runCollector("delivery", () => collectDeliveryFailures(reports, sitesById, deps.baseUrl)),
126
+ ...runCollector("preflight", () => collectPreflightBlocked(reports, sitesById, deps.baseUrl)),
118
127
  ...runCollector("lighthouse", () => collectLighthouseAlerts(websites, deps.baseUrl)),
119
128
  ...runCollector("renovate", () => collectRenovateAlerts(websites, deps.baseUrl, now)),
120
129
  ...runCollector("ci", () => collectCiAlerts(websites, deps.baseUrl, now)),
@@ -226,4 +235,4 @@ export {
226
235
  renderDigestHtml,
227
236
  runDigest
228
237
  };
229
- //# sourceMappingURL=digest-MECMZPFE.js.map
238
+ //# sourceMappingURL=digest-Q2TAQHXF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/digest.ts"],"sourcesContent":["// src/reports/digest.ts\nimport { openBase, readAirtableConfig, type AirtableBase } from \"./airtable/client.js\";\nimport { listAllReports, isPendingApproval } from \"./airtable/reports.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { listWebsites, siteSlug, type WebsiteRow } from \"./airtable/websites.js\";\nimport { defaultResendClient, type ResendClient } from \"./send/resend.js\";\nimport { isIdempotencyConflict } from \"./send/idempotency.js\";\nimport {\n collectVulnAlerts,\n collectDeliveryFailures,\n collectPreflightBlocked,\n collectLighthouseAlerts,\n collectRenovateAlerts,\n collectCiAlerts,\n collectAnalyticsFailures,\n} from \"../alerts/digest-collectors.js\";\nimport { diffAttention, readDigestState, writeDigestState } from \"../alerts/digest-state.js\";\nimport { escapeHtml as esc } from \"../util/html.js\";\nimport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n} from \"../alerts/attention.js\";\n\n// The attention/digest contract lives in `../alerts/attention.ts` (a dependency-free\n// types module) so the `alerts/*` collectors can depend on it without importing back\n// from this renderer/IO module — see attention.ts for the cycle it breaks. Re-exported\n// here so existing `from \"./digest.js\"` type importers keep resolving.\nexport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n} from \"../alerts/attention.js\";\n\nconst GREY = \"#757575\";\nconst RED = \"#C00\";\n\n/** Shared anchor style — Gmail does not inherit font-family into <a> tags. */\nconst ANCHOR_STYLE = `color:${RED};font-family:helvetica,sans-serif`;\n\nfunction readySection(items: ReadyItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Ready for your yes</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">Nothing waiting on you.</p>`;\n }\n const rows = items\n .map((it) => {\n const safeUrl = it.dashboardUrl.startsWith(\"https://\") ? it.dashboardUrl : undefined;\n const link = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">review &amp; approve</a>`\n : `review &amp; approve`;\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(it.siteName)}</strong> — ${esc(it.reportType)} (${esc(it.period)})\n — ${link}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${rows}</table>`;\n}\n\nconst SEVERITY_ORDER: Record<AttentionSeverity, number> = { critical: 0, warning: 1 };\n\n/** Render the per-item status badge (\"NEW\"/\"WORSE\"); standing items get nothing. */\nfunction attentionBadge(status?: AttentionStatus): string {\n if (status === \"new\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">NEW</strong> `;\n if (status === \"worse\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">WORSE</strong> `;\n return \"\";\n}\n\nfunction attentionSection(items: AttentionItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Needs attention</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">All clear — nothing needs attention.</p>`;\n }\n\n // Group by siteName, preserving first-seen site order; sort within a site by\n // severity (critical first).\n const bySite = new Map<string, AttentionItem[]>();\n for (const it of items) {\n const bucket = bySite.get(it.siteName);\n if (bucket) bucket.push(it);\n else bySite.set(it.siteName, [it]);\n }\n\n const groups = [...bySite.entries()]\n .map(([siteName, siteItems]) => {\n const sorted = [...siteItems].sort(\n (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity],\n );\n const rows = sorted\n .map((it) => {\n const safeUrl = it.url?.startsWith(\"https://\") ? it.url : undefined;\n const titleHtml = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">${esc(it.title)}</a>`\n : esc(it.title);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">${attentionBadge(it.status)}${titleHtml}</td>\n </tr>`;\n })\n .join(\"\");\n return `\n <tr>\n <td style=\"color:#222;font-family:helvetica,sans-serif;font-size:16px;font-weight:700;padding:8px 0 4px\">${esc(siteName)}</td>\n </tr>\n ${rows}`;\n })\n .join(\"\");\n\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${groups}</table>`;\n}\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\n/** Single-operator fleet — fallback when OPERATOR_EMAIL is unset. */\nconst DIGEST_OPERATOR_FALLBACK = \"info@reddoorla.com\";\n\n/** UTC \"YYYY-MM-DD\" — the Resend idempotency key suffix, so a same-day cron re-fire dedupes. */\nfunction digestDateKey(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * The gate for \"Ready for your yes\": Draft ready ∧ ¬Approved to send ∧ Sent at BLANK.\n *\n * Implemented as `listAllReports(base).filter(...)` (the authorized deviation from the plan's\n * draft, which pre-dated Slice 1's merged fix): `listAllReports` already calls `mapRow`, which\n * handles `Period` correctly, so no local `rawToReportRow` duplicate is needed. The JS filter\n * does real work — the test fake does NOT evaluate `filterByFormula` — so correctness is\n * test-provable here.\n *\n * Exported: the fleet homepage (Task 3.5b) reuses it for the pending-approval count.\n */\nexport async function listPendingApproval(base: AirtableBase): Promise<ReportRow[]> {\n return (await listAllReports(base)).filter(isPendingApproval);\n}\n\n// ── collectAttention (IO wrapper, sibling to runDigest) ──────────────────────\n\nexport type CollectAttentionDeps = {\n base: AirtableBase;\n /** Same baseUrl value runDigest threads; used for the /s/<slug> links. */\n baseUrl: string;\n /** Pre-fetched Websites rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listWebsites`. */\n websites?: WebsiteRow[];\n /** Pre-fetched Reports rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listAllReports`. */\n reports?: ReportRow[];\n /** Clock for the GitHub-signals staleness gate (collectCiAlerts /\n * collectRenovateAlerts skip a >3-day-stale sweep). Defaults to wall-clock;\n * runDigest threads its run-start `today` so a quiet repo's frozen CI/Renovate\n * signal stops badging once its sweep goes stale. */\n now?: Date;\n};\n\n/** Run a single collector under a try/catch: a thrown collector logs and yields []\n * so one broken signal never blanks the whole \"Needs attention\" section. */\nfunction runCollector(label: string, fn: () => AttentionItem[]): AttentionItem[] {\n try {\n return fn();\n } catch (e) {\n console.warn(`⚠ attention collector \"${label}\" failed: ${(e as Error).message}`);\n return [];\n }\n}\n\n/**\n * Fetch the free signals once (listAllReports + listWebsites) — or reuse the\n * `reports`/`websites` arrays runDigest already read, so a single run reads each\n * table once — build the sitesById map the delivery collector needs, and run each\n * pure collector isolated. Returns the union of items; diffing/badging happens in\n * runDigest.\n *\n * The Renovate + CI signals come from the SAME persisted collectors the operator\n * cockpit (`buildCockpitModel`) runs — `collectRenovateAlerts` (key\n * `renovate:<siteId>`) and `collectCiAlerts` (key `ci:<siteId>`), reading the\n * nightly-persisted `renovateFailingCis`/`defaultBranchCi` fields. This is the\n * key-space unification: because the digest writes the shared Digest State\n * snapshot with these same keys, the cockpit's NEW/WORSE diff finds them and the\n * two surfaces agree (the prior live per-PR `renovate:<repo>#<n>` sweep never\n * matched the cockpit's keys, so its cards badged NEW forever).\n */\nexport async function collectAttention(deps: CollectAttentionDeps): Promise<AttentionItem[]> {\n const reports = deps.reports ?? (await listAllReports(deps.base));\n const websites = deps.websites ?? (await listWebsites(deps.base));\n const now = deps.now ?? new Date();\n const sitesById = new Map<string, WebsiteRow>(websites.map((w) => [w.id, w]));\n return [\n ...runCollector(\"vuln\", () => collectVulnAlerts(websites, deps.baseUrl)),\n ...runCollector(\"delivery\", () => collectDeliveryFailures(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"preflight\", () => collectPreflightBlocked(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"lighthouse\", () => collectLighthouseAlerts(websites, deps.baseUrl)),\n ...runCollector(\"renovate\", () => collectRenovateAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"ci\", () => collectCiAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"analytics\", () => collectAnalyticsFailures(websites, deps.baseUrl, now)),\n ];\n}\n\nexport type DigestRunOptions = {\n resend?: ResendClient;\n /** Dashboard origin for the /s/<slug> links, e.g. \"https://reddoor-maintenance.netlify.app\". */\n baseUrl: string;\n /**\n * Inject a pre-opened Airtable base (tests, server handlers).\n * When omitted, `openBase(readAirtableConfig())` is called from the environment.\n */\n base?: AirtableBase;\n};\n\nexport async function runDigest(\n options: DigestRunOptions,\n): Promise<{ output: string; code: number }> {\n // Capture clock BEFORE any await so the idempotency key can't roll past midnight mid-run.\n const today = new Date();\n try {\n const base = options.base ?? openBase(readAirtableConfig());\n // Read each table ONCE for the whole run, then thread the arrays into\n // collectAttention so it doesn't re-fetch (was: listWebsites ×2, listAllReports\n // ×2). Pending is derived in-line with listPendingApproval's exact predicate.\n const reports = await listAllReports(base);\n const websites = await listWebsites(base);\n const sites = new Map(websites.map((w) => [w.id, w]));\n\n const pending = reports.filter(isPendingApproval);\n\n const readyForYourYes: ReadyItem[] = [];\n const baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n for (const r of pending) {\n const site = sites.get(r.siteId);\n if (!site) continue; // orphan report → skip rather than render a broken link\n // An empty Name slugs to \"\" → `/s/` is a dead link (getWebsiteBySlug can't\n // match it). Fall back to the fleet homepage so the operator still lands\n // somewhere usable instead of a 404.\n const slug = siteSlug(site.name);\n readyForYourYes.push({\n siteName: site.name,\n reportType: r.reportType,\n period: r.period ?? \"—\",\n dashboardUrl: slug ? `${baseUrl}/s/${slug}` : baseUrl,\n });\n }\n\n // M5: collect the free signals (isolated), diff against yesterday's snapshot.\n // Renovate + CI come from the persisted collectors (the same ones the cockpit\n // runs), so the snapshot this digest writes carries the `renovate:<siteId>` /\n // `ci:<siteId>` keys the cockpit diffs against — no live GitHub sweep here.\n const collected = await collectAttention({\n base,\n baseUrl: options.baseUrl,\n websites,\n reports,\n now: today,\n });\n const prior = await readDigestState(base);\n const { tagged, next } = diffAttention(collected, prior, digestDateKey(today));\n const needsAttention = tagged;\n\n // No-noise default: skip entirely when there's nothing to report.\n if (readyForYourYes.length === 0 && needsAttention.length === 0) {\n // On a skip, `collected` is [] so `next` is {} — still persist it so a key that\n // resolved on a quiet day clears and a later recurrence diffs as NEW (spec §10).\n // Wrapped: a write failure can't fail the skip.\n try {\n await writeDigestState(base, next);\n } catch (e) {\n console.warn(`⚠ digest state write failed: ${(e as Error).message}`);\n }\n return { output: \"Digest skipped (nothing ready, nothing needs attention).\", code: 0 };\n }\n\n const html = renderDigestHtml({ readyForYourYes, needsAttention });\n const client = options.resend ?? defaultResendClient();\n const to = [process.env.OPERATOR_EMAIL?.trim() || DIGEST_OPERATOR_FALLBACK];\n const n = readyForYourYes.length;\n const reportWord = n === 1 ? \"report\" : \"reports\";\n let result: Awaited<ReturnType<typeof client.send>>;\n try {\n result = await client.send({\n from: FROM_ADDRESS,\n to,\n subject: `Your fleet — ${digestDateKey(today)}: ${n} ${reportWord} ready for your yes`,\n html,\n idempotencyKey: `digest-${digestDateKey(today)}`,\n });\n } catch (err) {\n // A same-UTC-day re-run whose content changed re-sends with the same\n // `digest-<date>` idempotency key but a DIFFERENT body. Resend rejects that\n // with a 409 (`invalid_idempotent_request`) — \"This idempotency key has been\n // used ... but the request body was modified ...\". The operator already got\n // today's digest on the first send, so re-sending a changed version same-day\n // would just be a duplicate: treat it as an \"already sent today\" no-op.\n //\n // ResendClient (send/resend.ts) wraps the API error as a plain Error and only\n // preserves the *message* string (no name/statusCode), so the message\n // substring is the only reliable discriminator — match defensively on it.\n // Any OTHER send error re-throws to the outer catch → {code:1}, so a genuine\n // Resend/network failure still fails loudly.\n if (isIdempotencyConflict(err)) {\n // Do NOT write the snapshot: the first send already persisted it; writing\n // this run's `next` would diff against the first run's snapshot and mis-badge.\n return {\n output:\n \"Digest already sent today (content changed since the first send) — skipped to avoid a duplicate.\",\n code: 0,\n };\n }\n throw err;\n }\n // Persist the next snapshot AFTER a successful send. A write failure is caught +\n // logged: the digest already went out, tomorrow re-news at worst. (The send-FAILURE\n // path never reaches here — the outer catch returns code 1 with no write, preserving\n // the NEW badge for the retry.)\n try {\n await writeDigestState(base, next);\n } catch (e) {\n console.warn(`⚠ digest state write failed: ${(e as Error).message}`);\n }\n return { output: `Digest sent to ${to.join(\", \")} (${result.messageId})`, code: 0 };\n } catch (err) {\n // Re-throw config errors (exitCode=2: missing env vars, bad config) so runOrExit\n // surfaces them with the correct process exit code rather than collapsing to 1.\n if (typeof (err as { exitCode?: unknown }).exitCode === \"number\") {\n throw err;\n }\n const message = err instanceof Error ? err.message : String(err);\n return { output: `digest failed: ${message}`, code: 1 };\n }\n}\n\n/** Pure render of the unified daily operator digest. No IO — the caller (runDigest)\n * collects the rows and decides whether to send. */\nexport function renderDigestHtml(sections: DigestSections): string {\n return `<!doctype html>\n<html>\n <head><meta charset=\"utf-8\"></head>\n <body style=\"margin:0;padding:0;background:#ffffff\">\n <table width=\"100%\" style=\"border-collapse:collapse\">\n <tr>\n <td align=\"center\" style=\"padding:24px\">\n <table width=\"600\" style=\"border-collapse:collapse\">\n <tr>\n <td>\n <h1 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px\">Your fleet today</h1>\n ${readySection(sections.readyForYourYes)}\n ${attentionSection(sections.needsAttention)}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,87 @@
1
+ import {
2
+ WEBSITES_TABLE,
3
+ listWebsites,
4
+ siteSlug
5
+ } from "./chunk-C3PDS623.js";
6
+ import {
7
+ openBase,
8
+ readAirtableConfig
9
+ } from "./chunk-HN6I2XWI.js";
10
+ import "./chunk-6VAL7XJT.js";
11
+
12
+ // src/reports/airtable/ensure-site.ts
13
+ var COLS = {
14
+ url: "url",
15
+ pointOfContact: "point of contact",
16
+ gitRepo: "Git repo"
17
+ };
18
+ async function ensureSite(base, input) {
19
+ const slug = siteSlug(input.slug);
20
+ if (!slug) throw new Error(`ensure-site: '${input.slug}' does not slugify to a usable slug`);
21
+ if (input.displayName && siteSlug(input.displayName) !== slug)
22
+ throw new Error(
23
+ `ensure-site: display name '${input.displayName}' slugifies to '${siteSlug(input.displayName)}', not '${slug}' \u2014 the row would not be found on re-run`
24
+ );
25
+ const existing = (await listWebsites(base)).find((w) => siteSlug(w.name) === slug);
26
+ if (!existing) {
27
+ const fields = {
28
+ Name: input.displayName ?? slug,
29
+ Status: "in development",
30
+ [COLS.gitRepo]: input.gitRepo ?? `reddoorla/${slug}`
31
+ };
32
+ if (input.url) fields[COLS.url] = input.url;
33
+ if (input.pointOfContact) fields[COLS.pointOfContact] = input.pointOfContact;
34
+ const created = await base(WEBSITES_TABLE).create([{ fields }]);
35
+ return {
36
+ status: "created",
37
+ siteId: created[0].id,
38
+ updatedFields: [],
39
+ skippedMismatches: []
40
+ };
41
+ }
42
+ const updates = {};
43
+ const skippedMismatches = [];
44
+ const blank = (v) => v === null || v === void 0 || v === "";
45
+ const consider = (column, provided, current) => {
46
+ if (!provided) return;
47
+ if (blank(current)) updates[column] = provided;
48
+ else if (current !== provided) skippedMismatches.push(column);
49
+ };
50
+ consider(COLS.url, input.url, existing.url || null);
51
+ consider(COLS.pointOfContact, input.pointOfContact, existing.pointOfContact);
52
+ consider(COLS.gitRepo, input.gitRepo, existing.gitRepo);
53
+ const updatedFields = Object.keys(updates);
54
+ if (updatedFields.length > 0) {
55
+ await base(WEBSITES_TABLE).update([{ id: existing.id, fields: updates }]);
56
+ }
57
+ return { status: "exists", siteId: existing.id, updatedFields, skippedMismatches };
58
+ }
59
+
60
+ // src/cli/commands/ensure-site.ts
61
+ async function runEnsureSiteCommand(slug, opts) {
62
+ if (!slug) return { output: "Provide a <slug> (e.g. `ensure-site roalson`).", code: 2 };
63
+ try {
64
+ const base = openBase(readAirtableConfig());
65
+ const result = await ensureSite(base, {
66
+ slug,
67
+ ...opts.name ? { displayName: opts.name } : {},
68
+ ...opts.url ? { url: opts.url } : {},
69
+ ...opts.contact ? { pointOfContact: opts.contact } : {},
70
+ ...opts.gitRepo ? { gitRepo: opts.gitRepo } : {}
71
+ });
72
+ const filled = result.updatedFields.length > 0 ? ` \u2014 filled blank field(s): ${result.updatedFields.join(", ")}` : "";
73
+ const skipped = result.skippedMismatches.length > 0 ? ` \u2014 differs from existing, left untouched (edit in Airtable): ${result.skippedMismatches.join(", ")}` : "";
74
+ const nameNote = result.status === "created" && !opts.name ? ` \u2014 Name set to "${slug}"; retitle in Airtable before forms/announce go live (or re-create with --name)` : "";
75
+ return {
76
+ output: `[${slug}] ${result.status} (${result.siteId})${filled}${skipped}${nameNote}`,
77
+ code: 0
78
+ };
79
+ } catch (err) {
80
+ const e = err;
81
+ return { output: e.message ?? String(err), code: e.exitCode ?? 1 };
82
+ }
83
+ }
84
+ export {
85
+ runEnsureSiteCommand
86
+ };
87
+ //# sourceMappingURL=ensure-site-2PF522VL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/airtable/ensure-site.ts","../src/cli/commands/ensure-site.ts"],"sourcesContent":["import type { FieldSet, Records } from \"airtable\";\nimport type { AirtableBase } from \"./client.js\";\nimport { WEBSITES_TABLE, listWebsites, siteSlug } from \"./websites.js\";\n\nexport type EnsureSiteInput = {\n /** Canonical slug — matches by siteSlug(Name); the Name on create when no displayName given. */\n slug: string;\n /** Human display name written to Name on create. Name is consumed VERBATIM in\n * client-facing copy (forms auto-reply intro, report subjects), so a bare\n * machine slug there reads as \"Thanks for reaching out to acme-co.\" Must\n * slugify to the same slug or the row wouldn't be found on re-run. */\n displayName?: string;\n url?: string;\n pointOfContact?: string;\n /** owner/repo. Default on create: `reddoorla/<slug>`. */\n gitRepo?: string;\n};\n\nexport type EnsureSiteResult = {\n status: \"created\" | \"exists\";\n siteId: string;\n /** Airtable column names written on the exists path (create path writes all). */\n updatedFields: string[];\n /** Inputs that DIFFERED from an existing non-blank cell and were left\n * untouched (fill-blanks-only) — surfaced so a resumed bootstrap with a\n * corrected value doesn't silently discard it. */\n skippedMismatches: string[];\n};\n\n/** Column-name map (load-bearing magic strings — see websites.ts's mapRow header). */\nconst COLS = {\n url: \"url\",\n pointOfContact: \"point of contact\",\n gitRepo: \"Git repo\",\n} as const;\n\n/**\n * Find-or-create the Websites row for a slug so a new site exists in the fleet\n * inventory (audits, form-ingest slug resolution, reports) from day one.\n *\n * Fill-blanks-only on the exists path: this command runs from a bootstrap skill\n * that may be re-run to resume — it must never clobber operator-edited cells.\n * Frequencies are deliberately NOT set (launch flips the lifecycle); Status is\n * only written on create (\"in development\").\n */\nexport async function ensureSite(\n base: AirtableBase,\n input: EnsureSiteInput,\n): Promise<EnsureSiteResult> {\n const slug = siteSlug(input.slug);\n if (!slug) throw new Error(`ensure-site: '${input.slug}' does not slugify to a usable slug`);\n if (input.displayName && siteSlug(input.displayName) !== slug)\n throw new Error(\n `ensure-site: display name '${input.displayName}' slugifies to '${siteSlug(input.displayName)}', not '${slug}' — the row would not be found on re-run`,\n );\n\n const existing = (await listWebsites(base)).find((w) => siteSlug(w.name) === slug);\n\n if (!existing) {\n const fields: FieldSet = {\n Name: input.displayName ?? slug,\n Status: \"in development\",\n [COLS.gitRepo]: input.gitRepo ?? `reddoorla/${slug}`,\n };\n if (input.url) fields[COLS.url] = input.url;\n if (input.pointOfContact) fields[COLS.pointOfContact] = input.pointOfContact;\n const created = (await base(WEBSITES_TABLE).create([{ fields }])) as Records<FieldSet>;\n return {\n status: \"created\",\n siteId: created[0]!.id,\n updatedFields: [],\n skippedMismatches: [],\n };\n }\n\n const updates: FieldSet = {};\n const skippedMismatches: string[] = [];\n const blank = (v: unknown) => v === null || v === undefined || v === \"\";\n const consider = (column: string, provided: string | undefined, current: unknown) => {\n if (!provided) return;\n if (blank(current)) updates[column] = provided;\n else if (current !== provided) skippedMismatches.push(column);\n };\n consider(COLS.url, input.url, existing.url || null);\n consider(COLS.pointOfContact, input.pointOfContact, existing.pointOfContact);\n consider(COLS.gitRepo, input.gitRepo, existing.gitRepo);\n\n const updatedFields = Object.keys(updates);\n if (updatedFields.length > 0) {\n await base(WEBSITES_TABLE).update([{ id: existing.id, fields: updates }]);\n }\n return { status: \"exists\", siteId: existing.id, updatedFields, skippedMismatches };\n}\n","import { openBase, readAirtableConfig } from \"../../reports/airtable/client.js\";\nimport { ensureSite } from \"../../reports/airtable/ensure-site.js\";\n\nexport type EnsureSiteCommandOptions = {\n name?: string;\n url?: string;\n contact?: string;\n gitRepo?: string;\n cwd?: string;\n};\n\n/**\n * `ensure-site <slug>` — create/verify the fleet-inventory row for a new site.\n * Day-one step of the /new-site bootstrap skill. Fill-blanks-only; safe to re-run.\n */\nexport async function runEnsureSiteCommand(\n slug: string | undefined,\n opts: EnsureSiteCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (!slug) return { output: \"Provide a <slug> (e.g. `ensure-site roalson`).\", code: 2 };\n try {\n const base = openBase(readAirtableConfig());\n const result = await ensureSite(base, {\n slug,\n ...(opts.name ? { displayName: opts.name } : {}),\n ...(opts.url ? { url: opts.url } : {}),\n ...(opts.contact ? { pointOfContact: opts.contact } : {}),\n ...(opts.gitRepo ? { gitRepo: opts.gitRepo } : {}),\n });\n const filled =\n result.updatedFields.length > 0\n ? ` — filled blank field(s): ${result.updatedFields.join(\", \")}`\n : \"\";\n const skipped =\n result.skippedMismatches.length > 0\n ? ` — differs from existing, left untouched (edit in Airtable): ${result.skippedMismatches.join(\", \")}`\n : \"\";\n const nameNote =\n result.status === \"created\" && !opts.name\n ? ` — Name set to \"${slug}\"; retitle in Airtable before forms/announce go live (or re-create with --name)`\n : \"\";\n return {\n output: `[${slug}] ${result.status} (${result.siteId})${filled}${skipped}${nameNote}`,\n code: 0,\n };\n } catch (err) {\n const e = err as { message?: string; exitCode?: number };\n return { output: e.message ?? String(err), code: e.exitCode ?? 1 };\n }\n}\n"],"mappings":"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","names":[]}
@@ -3,26 +3,26 @@ import {
3
3
  } from "./chunk-PZQ3WED2.js";
4
4
  import {
5
5
  formatFleetWriteSummary
6
- } from "./chunk-KN5OC5YH.js";
7
- import "./chunk-T34EUM5B.js";
6
+ } from "./chunk-GKSXZIMX.js";
7
+ import "./chunk-XWPZ46IN.js";
8
8
  import {
9
9
  detectSignalEvents,
10
10
  fleetSweptEvent
11
11
  } from "./chunk-5L3FPQIJ.js";
12
+ import {
13
+ makeGitHub
14
+ } from "./chunk-YEGHUODU.js";
12
15
  import {
13
16
  recordFleetEventsBestEffort
14
17
  } from "./chunk-OTBA7DON.js";
15
18
  import "./chunk-PH3AGF4Z.js";
16
19
  import "./chunk-HIN3P4ZN.js";
17
- import {
18
- makeGitHub
19
- } from "./chunk-YEGHUODU.js";
20
20
  import "./chunk-HVOOCK6L.js";
21
21
  import {
22
22
  listWebsites,
23
23
  siteSlug,
24
24
  updateGitHubSignals
25
- } from "./chunk-CL5DYUWQ.js";
25
+ } from "./chunk-C3PDS623.js";
26
26
  import {
27
27
  openBase,
28
28
  readAirtableConfig
@@ -134,4 +134,4 @@ export {
134
134
  githubSignalsExitCode,
135
135
  runGitHubSignalsCommand
136
136
  };
137
- //# sourceMappingURL=github-signals-VWBMQNLH.js.map
137
+ //# sourceMappingURL=github-signals-CLQC2IPY.js.map
package/dist/index.d.ts CHANGED
@@ -333,6 +333,12 @@ type WebsiteRow = {
333
333
  pointOfContact: string | null;
334
334
  maintenanceFreq: Frequency;
335
335
  testingFreq: Frequency;
336
+ /** The literal Airtable cell values behind the coerced frequencies. `toFrequency`
337
+ * maps any unrecognized value ("Quaterly", "Monthly ") to "None" so nothing bogus
338
+ * reaches a client email — which also makes typos invisible downstream. Preflight
339
+ * validates THESE to make that failure mode loud. Null = blank cell. */
340
+ maintenanceFreqRaw: string | null;
341
+ testingFreqRaw: string | null;
336
342
  /** Last manually-recorded maintenance day (used as fallback when no Reports row exists). */
337
343
  maintenanceDay: string | null;
338
344
  testingDay: string | null;
@@ -810,7 +816,7 @@ type AttentionStatus = "new" | "worse" | "standing";
810
816
  type AttentionItem = {
811
817
  /** Stable identity for diffing: `vuln:<siteId>`, `delivery:<reportId>`. */
812
818
  key: string;
813
- kind: "vuln" | "delivery" | "renovate" | "lighthouse" | "ci" | "analytics";
819
+ kind: "vuln" | "delivery" | "renovate" | "lighthouse" | "ci" | "analytics" | "preflight";
814
820
  /** Grouping key in the (component-3) render. */
815
821
  siteName: string;
816
822
  title: string;