@reddoorla/maintenance 0.75.0 → 0.76.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 (70) hide show
  1. package/dist/{announce-V6EYPPUR.js → announce-RUT247HS.js} +9 -6
  2. package/dist/{announce-V6EYPPUR.js.map → announce-RUT247HS.js.map} +1 -1
  3. package/dist/{blux-C24OVDDW.js → blux-RIJBRVCF.js} +1867 -277
  4. package/dist/blux-RIJBRVCF.js.map +1 -0
  5. package/dist/chunk-37S6L6G4.js +170 -0
  6. package/dist/chunk-37S6L6G4.js.map +1 -0
  7. package/dist/{chunk-PERP7LSS.js → chunk-3U3IGVAQ.js} +89 -13
  8. package/dist/chunk-3U3IGVAQ.js.map +1 -0
  9. package/dist/{chunk-IDBMYDVH.js → chunk-4VAUSRSS.js} +5 -3
  10. package/dist/{chunk-IDBMYDVH.js.map → chunk-4VAUSRSS.js.map} +1 -1
  11. package/dist/{run-migration-LJNNZWIV.js → chunk-6DOFNDXU.js} +5 -3
  12. package/dist/chunk-6DOFNDXU.js.map +1 -0
  13. package/dist/{chunk-NUIBQPB3.js → chunk-6SZBNALD.js} +62 -9
  14. package/dist/chunk-6SZBNALD.js.map +1 -0
  15. package/dist/{chunk-6OYICQSJ.js → chunk-CZAMYVOP.js} +9 -4
  16. package/dist/chunk-CZAMYVOP.js.map +1 -0
  17. package/dist/chunk-DRDAEELJ.js +197 -0
  18. package/dist/chunk-DRDAEELJ.js.map +1 -0
  19. package/dist/{chunk-KKL3RYFQ.js → chunk-DYKG53IR.js} +2 -2
  20. package/dist/{chunk-HBFMTM2E.js → chunk-JTRTG5CU.js} +3 -3
  21. package/dist/{chunk-JCITWZQR.js → chunk-MNE374CZ.js} +2 -50
  22. package/dist/chunk-MNE374CZ.js.map +1 -0
  23. package/dist/chunk-MPBXOJ3N.js +51 -0
  24. package/dist/chunk-MPBXOJ3N.js.map +1 -0
  25. package/dist/{chunk-I55YWV6T.js → chunk-QWFHC3VH.js} +3 -3
  26. package/dist/{chunk-KLVMJYZZ.js → chunk-SEQA7MC3.js} +2 -2
  27. package/dist/chunk-UWJZXL2D.js +9 -0
  28. package/dist/chunk-UWJZXL2D.js.map +1 -0
  29. package/dist/cli/bin.js +36 -9
  30. package/dist/cli/bin.js.map +1 -1
  31. package/dist/cli/commands/audit.js +1 -1
  32. package/dist/{digest-LEMZS5ZK.js → digest-75FWSC74.js} +9 -6
  33. package/dist/{digest-LEMZS5ZK.js.map → digest-75FWSC74.js.map} +1 -1
  34. package/dist/favicon-SRHSJZ62.js +42 -0
  35. package/dist/favicon-SRHSJZ62.js.map +1 -0
  36. package/dist/freeze-Y5OJFOST.js +442 -0
  37. package/dist/freeze-Y5OJFOST.js.map +1 -0
  38. package/dist/header-image-H4N54DHN.js +95 -0
  39. package/dist/header-image-H4N54DHN.js.map +1 -0
  40. package/dist/index.d.ts +63 -1
  41. package/dist/index.js +64 -16
  42. package/dist/index.js.map +1 -1
  43. package/dist/{init-7PTP4O2K.js → init-2KOISBCZ.js} +3 -3
  44. package/dist/{launch-W33FWWOZ.js → launch-K4EHKTXC.js} +7 -5
  45. package/dist/{launch-W33FWWOZ.js.map → launch-K4EHKTXC.js.map} +1 -1
  46. package/dist/{orchestrate-NYRYEKQM.js → orchestrate-CPR6KVS2.js} +4 -3
  47. package/dist/{preflight-JCQFKB6M.js → preflight-QRRMPI7K.js} +5 -4
  48. package/dist/{preflight-JCQFKB6M.js.map → preflight-QRRMPI7K.js.map} +1 -1
  49. package/dist/{report-POHQEV6F.js → report-SLGBDJ6O.js} +8 -6
  50. package/dist/{report-POHQEV6F.js.map → report-SLGBDJ6O.js.map} +1 -1
  51. package/dist/reports/header-image/assets/plate.png +0 -0
  52. package/dist/run-migration-WP57U5XO.js +9 -0
  53. package/dist/run-migration-WP57U5XO.js.map +1 -0
  54. package/dist/{selftest-OO2WDSDM.js → selftest-RVASRO3Z.js} +8 -6
  55. package/dist/{selftest-OO2WDSDM.js.map → selftest-RVASRO3Z.js.map} +1 -1
  56. package/dist/webflow-624FEBFD.js +548 -0
  57. package/dist/webflow-624FEBFD.js.map +1 -0
  58. package/package.json +3 -3
  59. package/dist/blux-C24OVDDW.js.map +0 -1
  60. package/dist/chunk-6OYICQSJ.js.map +0 -1
  61. package/dist/chunk-JCITWZQR.js.map +0 -1
  62. package/dist/chunk-NUIBQPB3.js.map +0 -1
  63. package/dist/chunk-PERP7LSS.js.map +0 -1
  64. package/dist/run-migration-LJNNZWIV.js.map +0 -1
  65. /package/dist/{chunk-KKL3RYFQ.js.map → chunk-DYKG53IR.js.map} +0 -0
  66. /package/dist/{chunk-HBFMTM2E.js.map → chunk-JTRTG5CU.js.map} +0 -0
  67. /package/dist/{chunk-I55YWV6T.js.map → chunk-QWFHC3VH.js.map} +0 -0
  68. /package/dist/{chunk-KLVMJYZZ.js.map → chunk-SEQA7MC3.js.map} +0 -0
  69. /package/dist/{init-7PTP4O2K.js.map → init-2KOISBCZ.js.map} +0 -0
  70. /package/dist/{orchestrate-NYRYEKQM.js.map → orchestrate-CPR6KVS2.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/commands/header-image.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { openBase, readAirtableConfig } from \"../../reports/airtable/client.js\";\nimport { listWebsites, siteSlug, ACTIVE_STATUSES } from \"../../reports/airtable/websites.js\";\nimport type { WebsiteRow } from \"../../reports/airtable/websites.js\";\nimport { uploadAttachment } from \"../../reports/airtable/attachments.js\";\nimport { generateHeaderImage } from \"../../reports/header-image/index.js\";\n\nexport type HeaderImageOptions = {\n all?: boolean;\n force?: boolean;\n writeAirtable?: boolean;\n outDir?: string;\n settleMs?: string;\n};\n\n/** Which rows this invocation should act on. Pure, so it is unit-tested. */\nexport function resolveTargets(\n rows: readonly WebsiteRow[],\n // `| undefined` spelled out because `exactOptionalPropertyTypes` otherwise\n // rejects the caller passing through possibly-undefined CLI flags.\n opts: { site?: string | undefined; all?: boolean | undefined; force?: boolean | undefined },\n): WebsiteRow[] {\n if (opts.site) {\n const want = siteSlug(opts.site);\n return rows.filter((r) => siteSlug(r.name) === want);\n }\n if (!opts.all) return [];\n return rows.filter((r) => {\n if (!r.url) return false;\n if (r.status === null || !ACTIVE_STATUSES.has(r.status)) return false;\n return opts.force ? true : !r.headerImage;\n });\n}\n\n/** Parse the `--settle-ms <ms>` flag. Unset → undefined (the generator's own\n * default settle). `Number(\"abc\")` is NaN, which would sail through as a NaN\n * timeout and silently skip the settle entirely, so anything non-finite or\n * negative is rejected up front rather than producing a subtly-wrong capture. */\nexport function parseSettleMs(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const n = Number(value);\n if (!Number.isFinite(n) || n < 0) {\n throw Object.assign(\n new Error(`--settle-ms must be a non-negative number of milliseconds, got \"${value}\"`),\n { exitCode: 2 },\n );\n }\n return n;\n}\n\n/** Injected in tests so the file-writing branch can be exercised without\n * driving a real browser (mirrors `GenerateInput.shooter`). */\ntype Generate = typeof generateHeaderImage;\n\n/**\n * Generate a header image per target row and either upload it to Airtable or\n * write it locally for review. Split out of {@link runHeaderImageCommand} so\n * the local-output path is testable without an Airtable base.\n */\nexport async function generateForTargets(\n targets: readonly WebsiteRow[],\n opts: HeaderImageOptions,\n generate: Generate = generateHeaderImage,\n): Promise<{ output: string; code: number }> {\n const outDir = resolve(opts.outDir ?? \"reports\");\n const settleMs = parseSettleMs(opts.settleMs);\n // `reports/` does not exist on a fresh checkout, so the writeFile() below\n // would throw ENOENT for the first operator to run the default (local) path.\n // Create it once, before the loop, rather than per site.\n if (!opts.writeAirtable) await mkdir(outDir, { recursive: true });\n\n const lines: string[] = [];\n let failed = 0;\n\n for (const row of targets) {\n try {\n const gen = await generate({\n url: row.url,\n slug: siteSlug(row.name),\n // `exactOptionalPropertyTypes` rejects an explicit `settleMs: undefined`\n // against `settleMs?: number`, so omit the key entirely when unset.\n ...(settleMs === undefined ? {} : { settleMs }),\n });\n if (opts.writeAirtable) {\n await uploadAttachment(row.id, \"Header image\", gen.bytes, gen.filename, gen.contentType);\n lines.push(\n `✔ ${row.name} — uploaded ${gen.filename} (${(gen.bytes.byteLength / 1024 / 1024).toFixed(2)} MB)`,\n );\n } else {\n const path = resolve(outDir, gen.filename);\n await writeFile(path, gen.bytes);\n lines.push(`✔ ${row.name} — wrote ${path} (review, then re-run with --write-airtable)`);\n }\n } catch (err) {\n failed++;\n lines.push(`✖ ${row.name} — ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n lines.push(`${targets.length - failed}/${targets.length} generated.`);\n return { output: lines.join(\"\\n\"), code: failed > 0 ? 1 : 0 };\n}\n\n/**\n * `header-image [site]` — capture a site's live homepage and composite its\n * report header image. Defaults to writing the JPEG locally so the operator can\n * eyeball it; `--write-airtable` uploads it to the Websites row's Header image\n * field. `--all` backfills every live site that has no header image yet.\n */\nexport async function runHeaderImageCommand(\n site: string | undefined,\n opts: HeaderImageOptions,\n): Promise<{ output: string; code: number }> {\n const base = openBase(readAirtableConfig());\n const rows = await listWebsites(base);\n const targets = resolveTargets(rows, { site, all: opts.all, force: opts.force });\n if (targets.length === 0) {\n return {\n output: site ? `No site matched \"${site}\".` : \"No sites need a header image.\",\n code: 1,\n };\n }\n return generateForTargets(targets, opts);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,iBAAiB;AACjC,SAAS,eAAe;AAgBjB,SAAS,eACd,MAGA,MACc;AACd,MAAI,KAAK,MAAM;AACb,UAAM,OAAO,SAAS,KAAK,IAAI;AAC/B,WAAO,KAAK,OAAO,CAAC,MAAM,SAAS,EAAE,IAAI,MAAM,IAAI;AAAA,EACrD;AACA,MAAI,CAAC,KAAK,IAAK,QAAO,CAAC;AACvB,SAAO,KAAK,OAAO,CAAC,MAAM;AACxB,QAAI,CAAC,EAAE,IAAK,QAAO;AACnB,QAAI,EAAE,WAAW,QAAQ,CAAC,gBAAgB,IAAI,EAAE,MAAM,EAAG,QAAO;AAChE,WAAO,KAAK,QAAQ,OAAO,CAAC,EAAE;AAAA,EAChC,CAAC;AACH;AAMO,SAAS,cAAc,OAA+C;AAC3E,MAAI,UAAU,OAAW,QAAO;AAChC,QAAM,IAAI,OAAO,KAAK;AACtB,MAAI,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,GAAG;AAChC,UAAM,OAAO;AAAA,MACX,IAAI,MAAM,mEAAmE,KAAK,GAAG;AAAA,MACrF,EAAE,UAAU,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AAWA,eAAsB,mBACpB,SACA,MACA,WAAqB,qBACsB;AAC3C,QAAM,SAAS,QAAQ,KAAK,UAAU,SAAS;AAC/C,QAAM,WAAW,cAAc,KAAK,QAAQ;AAI5C,MAAI,CAAC,KAAK,cAAe,OAAM,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AAEhE,QAAM,QAAkB,CAAC;AACzB,MAAI,SAAS;AAEb,aAAW,OAAO,SAAS;AACzB,QAAI;AACF,YAAM,MAAM,MAAM,SAAS;AAAA,QACzB,KAAK,IAAI;AAAA,QACT,MAAM,SAAS,IAAI,IAAI;AAAA;AAAA;AAAA,QAGvB,GAAI,aAAa,SAAY,CAAC,IAAI,EAAE,SAAS;AAAA,MAC/C,CAAC;AACD,UAAI,KAAK,eAAe;AACtB,cAAM,iBAAiB,IAAI,IAAI,gBAAgB,IAAI,OAAO,IAAI,UAAU,IAAI,WAAW;AACvF,cAAM;AAAA,UACJ,UAAK,IAAI,IAAI,oBAAe,IAAI,QAAQ,MAAM,IAAI,MAAM,aAAa,OAAO,MAAM,QAAQ,CAAC,CAAC;AAAA,QAC9F;AAAA,MACF,OAAO;AACL,cAAM,OAAO,QAAQ,QAAQ,IAAI,QAAQ;AACzC,cAAM,UAAU,MAAM,IAAI,KAAK;AAC/B,cAAM,KAAK,UAAK,IAAI,IAAI,iBAAY,IAAI,8CAA8C;AAAA,MACxF;AAAA,IACF,SAAS,KAAK;AACZ;AACA,YAAM,KAAK,UAAK,IAAI,IAAI,WAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,IAClF;AAAA,EACF;AAEA,QAAM,KAAK,GAAG,QAAQ,SAAS,MAAM,IAAI,QAAQ,MAAM,aAAa;AACpE,SAAO,EAAE,QAAQ,MAAM,KAAK,IAAI,GAAG,MAAM,SAAS,IAAI,IAAI,EAAE;AAC9D;AAQA,eAAsB,sBACpB,MACA,MAC2C;AAC3C,QAAM,OAAO,SAAS,mBAAmB,CAAC;AAC1C,QAAM,OAAO,MAAM,aAAa,IAAI;AACpC,QAAM,UAAU,eAAe,MAAM,EAAE,MAAM,KAAK,KAAK,KAAK,OAAO,KAAK,MAAM,CAAC;AAC/E,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,MACL,QAAQ,OAAO,oBAAoB,IAAI,OAAO;AAAA,MAC9C,MAAM;AAAA,IACR;AAAA,EACF;AACA,SAAO,mBAAmB,SAAS,IAAI;AACzC;","names":[]}
package/dist/index.d.ts CHANGED
@@ -71,6 +71,11 @@ type RouteResult = {
71
71
  title: string | null;
72
72
  /** The chromium `meta[name="description"]` content, or null when absent/not captured. */
73
73
  metaDescription: string | null;
74
+ /** Unsubstituted SvelteKit template placeholders (e.g. "%sveltekit.body%") the SERVED HTML
75
+ * carried — a broken app.html (a placeholder token trapped in a comment, a malformed template)
76
+ * ships these literally. Empty = clean. Optional: a runner that didn't capture it is treated as
77
+ * clean (undefined ≠ failure), but a token that IS seen is unambiguous corruption. */
78
+ leakedTemplateTokens?: string[];
74
79
  };
75
80
  type LinkResult = {
76
81
  url: string;
@@ -88,9 +93,18 @@ type PageFetch = {
88
93
  title: string | null;
89
94
  metaDescription: string | null;
90
95
  };
96
+ /** What a no-follow GET of the base URL observed: the raw status (null = network error/timeout)
97
+ * and the raw Location header (null when absent). See canonicalizeBaseUrl. */
98
+ type RedirectFetch = {
99
+ status: number | null;
100
+ location: string | null;
101
+ };
91
102
  /** Injected plain-fetch re-verification IO (see reverifyRoutes). Tests pass a fake. */
92
103
  type VerifyDeps = {
93
104
  fetchPage: (url: string) => Promise<PageFetch>;
105
+ /** Optional single `redirect:"manual"` GET, for canonicalizeBaseUrl's base-URL preflight.
106
+ * Optional so existing fakes stay valid; absent → canonicalization is a no-op. */
107
+ fetchRedirect?: (url: string) => Promise<RedirectFetch>;
94
108
  };
95
109
 
96
110
  /** Outcome of driving one site's contact form. `formPresent:false` ⇒ n/a
@@ -756,6 +770,47 @@ type ReportRow = {
756
770
  overrideAt: string | null;
757
771
  };
758
772
 
773
+ type ShootOptions = {
774
+ url: string;
775
+ width: number;
776
+ height: number;
777
+ deviceScaleFactor: number;
778
+ settleMs: number;
779
+ };
780
+ /** Injected browser IO. The real impl drives Playwright; tests pass a fake. */
781
+ type Shooter = {
782
+ shoot: (opts: ShootOptions) => Promise<Uint8Array>;
783
+ };
784
+
785
+ type GenerateInput = {
786
+ /** The site's production URL. */
787
+ url: string;
788
+ /** Airtable site slug; drives the attachment filename. */
789
+ slug?: string;
790
+ /** Injected browser IO (tests). */
791
+ shooter?: Shooter;
792
+ /** Per-site settle override for animation-heavy or consent-gated homepages. */
793
+ settleMs?: number;
794
+ };
795
+ type GeneratedHeaderImage = {
796
+ bytes: Uint8Array;
797
+ /** The domain as printed on the plate. */
798
+ domain: string;
799
+ filename: string;
800
+ contentType: "image/jpeg";
801
+ };
802
+ /** Strip scheme, leading www and any path — the plate prints a bare domain. */
803
+ declare function domainFromUrl(url: string): string;
804
+ /** Capture a site's homepage and composite its header image. */
805
+ declare function generateHeaderImage(input: GenerateInput): Promise<GeneratedHeaderImage>;
806
+
807
+ type RefreshHeaderDeps = {
808
+ generate?: (input: {
809
+ url: string;
810
+ slug?: string;
811
+ }) => Promise<GeneratedHeaderImage>;
812
+ upload?: (recordId: string, field: string, bytes: Uint8Array, filename: string, contentType: string) => Promise<void>;
813
+ };
759
814
  type DraftOptions = {
760
815
  /** Where to write the local preview HTML when `previewOnly`. Defaults to `reports/<slug>/draft.html`. */
761
816
  previewPath?: string;
@@ -772,6 +827,10 @@ type DraftOptions = {
772
827
  /** The mapped ReportRow being completed, returned as `reportRow` from the
773
828
  * complete path so callers keep the same shape they get on the create path. */
774
829
  existingRow?: ReportRow;
830
+ /** Injected deps for the draft-time header refresh, or `false` to skip it
831
+ * entirely. Tests set `false` (or a stub) so a unit suite never launches a
832
+ * browser or resolves DNS. Production leaves it unset and gets the real thing. */
833
+ refreshHeader?: RefreshHeaderDeps | false;
775
834
  };
776
835
  /** An enrichment fetch that *errored* (not one that was legitimately skipped
777
836
  * because it isn't configured / the site lacks the inputs). Surfaced so a
@@ -1142,4 +1201,7 @@ declare function renderCockpitHtml(model: CockpitModel): string;
1142
1201
  */
1143
1202
  declare function verifyBasicAuth(authHeader: string | null | undefined, expectedPassword: string | null): boolean;
1144
1203
 
1145
- export { ALL_AUDIT_NAMES, ALL_RECIPE_NAMES, type AirtableInventoryOptions, AuditName, AuditResult, BLURRED_CID, type BumpDepsOptions, type BundledImage, CHECK_CID, type ConvertToPnpmOptions, DEFAULT_INIT_STEPS, type DraftOptions, type DraftResult, type DueItem, type HeaderImage, type InitOptions, type InitResult, type InitStep, type InitStepResult, InventoryProvider, type LighthouseScores, type LocalPathOptions, type OnboardAudit, type OnboardOptions, type OrchestrateOptions, RecipeName, RecipeResult, type RenderResult, type ReportData, type ReportType, Site, type UpgradeSvelte4to5Options, a11yAudit, a11yFixturesPage, bumpDeps, convertToPnpm, depsAudit, draftReportForSite, findDueReports, fromAirtableBase, fromJsonFile, init, isRecipeName, lighthouseAudit, lintAudit, loadBundledImages, localPath, onboard, renderCockpitHtml, renderReportHtml, renderSiteDashboardHtml, runAudits, runAuditsAcross, securityAudit, selfCaretRange, selfPackageVersion, sendApprovedReports, svelteCodemods, upgradeSvelte4to5, verifyBasicAuth };
1204
+ /** Read the bundled 2400x3200 plate. */
1205
+ declare function loadPlate(): Promise<Uint8Array>;
1206
+
1207
+ export { ALL_AUDIT_NAMES, ALL_RECIPE_NAMES, type AirtableInventoryOptions, AuditName, AuditResult, BLURRED_CID, type BumpDepsOptions, type BundledImage, CHECK_CID, type ConvertToPnpmOptions, DEFAULT_INIT_STEPS, type DraftOptions, type DraftResult, type DueItem, type GenerateInput, type GeneratedHeaderImage, type HeaderImage, type InitOptions, type InitResult, type InitStep, type InitStepResult, InventoryProvider, type LighthouseScores, type LocalPathOptions, type OnboardAudit, type OnboardOptions, type OrchestrateOptions, RecipeName, RecipeResult, type RefreshHeaderDeps, type RenderResult, type ReportData, type ReportType, Site, type UpgradeSvelte4to5Options, a11yAudit, a11yFixturesPage, bumpDeps, convertToPnpm, depsAudit, domainFromUrl, draftReportForSite, findDueReports, fromAirtableBase, fromJsonFile, generateHeaderImage, init, isRecipeName, lighthouseAudit, lintAudit, loadBundledImages, loadPlate, localPath, onboard, renderCockpitHtml, renderReportHtml, renderSiteDashboardHtml, runAudits, runAuditsAcross, securityAudit, selfCaretRange, selfPackageVersion, sendApprovedReports, svelteCodemods, upgradeSvelte4to5, verifyBasicAuth };
package/dist/index.js CHANGED
@@ -1,15 +1,20 @@
1
+ import "./chunk-CZAMYVOP.js";
1
2
  import {
2
3
  draftReportForSite
3
- } from "./chunk-NUIBQPB3.js";
4
- import "./chunk-KLVMJYZZ.js";
5
- import "./chunk-RHPDQW6D.js";
6
- import "./chunk-6OYICQSJ.js";
4
+ } from "./chunk-6SZBNALD.js";
5
+ import "./chunk-SEQA7MC3.js";
6
+ import {
7
+ domainFromUrl,
8
+ generateHeaderImage,
9
+ loadPlate
10
+ } from "./chunk-37S6L6G4.js";
7
11
  import {
8
12
  approveBlockers
9
- } from "./chunk-HBFMTM2E.js";
13
+ } from "./chunk-JTRTG5CU.js";
10
14
  import {
11
15
  findDueReports
12
16
  } from "./chunk-D4FVAIEJ.js";
17
+ import "./chunk-RHPDQW6D.js";
13
18
  import {
14
19
  upgradeSvelte4to5
15
20
  } from "./chunk-ZO7WNY57.js";
@@ -17,7 +22,7 @@ import {
17
22
  DEFAULT_INIT_STEPS,
18
23
  a11yFixturesPage,
19
24
  init
20
- } from "./chunk-KKL3RYFQ.js";
25
+ } from "./chunk-DYKG53IR.js";
21
26
  import {
22
27
  convertToPnpm
23
28
  } from "./chunk-2X322ZES.js";
@@ -37,7 +42,7 @@ import {
37
42
  parseAddresses,
38
43
  sendApprovedReports,
39
44
  withGlobalCc
40
- } from "./chunk-IDBMYDVH.js";
45
+ } from "./chunk-4VAUSRSS.js";
41
46
  import "./chunk-GECESMVS.js";
42
47
  import {
43
48
  BLURRED_CID,
@@ -50,7 +55,8 @@ import {
50
55
  loadBundledImages,
51
56
  renderReportHtml,
52
57
  safeUrl
53
- } from "./chunk-JCITWZQR.js";
58
+ } from "./chunk-MNE374CZ.js";
59
+ import "./chunk-MPBXOJ3N.js";
54
60
  import "./chunk-WEKL2HYJ.js";
55
61
  import {
56
62
  bumpDeps
@@ -73,7 +79,7 @@ import {
73
79
  runAudits,
74
80
  runAuditsAcross,
75
81
  securityAudit
76
- } from "./chunk-PERP7LSS.js";
82
+ } from "./chunk-3U3IGVAQ.js";
77
83
  import "./chunk-GASBX52O.js";
78
84
  import "./chunk-YEGHUODU.js";
79
85
  import "./chunk-LKGVSM2O.js";
@@ -606,8 +612,23 @@ th, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid #eee; }
606
612
  @media (prefers-color-scheme: dark) { th, td { border-color: #2a2a2a; } }
607
613
  .muted { color: #999; }
608
614
  .empty { color: #999; padding: 1rem; border: 1px dashed #ccc; border-radius: 6px; text-align: center; }
609
- button.approve { font: inherit; padding: 0.35rem 0.85rem; border: 1px solid #2c7; border-radius: 6px; background: #2c7; color: #fff; cursor: pointer; }
615
+ button.approve { font: inherit; padding: 0.35rem 0.85rem; border: 1px solid #2c7; border-radius: 6px; background: #2c7; color: #fff; cursor: pointer; position: relative; transition: background-color 0.12s ease, border-color 0.12s ease; }
616
+ button.approve:hover:not(:disabled) { background: #25b36a; border-color: #25b36a; }
617
+ button.approve:active:not(:disabled) { background: #1e9a5b; border-color: #1e9a5b; }
618
+ button.approve:focus-visible { outline: 2px solid #2c7; outline-offset: 2px; }
610
619
  button.approve:disabled { opacity: 0.6; cursor: default; }
620
+ /* In-flight: the label goes transparent rather than being removed, so the button keeps
621
+ its exact width and the row never reflows mid-request. The spinner is a pseudo-element,
622
+ NOT injected markup \u2014 this handler is deliberately textContent/title-only so server
623
+ strings can never become HTML, and a ::after keeps that property intact. */
624
+ button.approve.is-loading { color: transparent; cursor: progress; }
625
+ button.approve.is-loading::after { content: ""; position: absolute; top: 50%; left: 50%; width: 0.85em; height: 0.85em; margin: -0.45em 0 0 -0.45em; border: 2px solid #ffffff80; border-top-color: transparent; border-radius: 50%; animation: approve-spin 0.8s linear infinite; }
626
+ @keyframes approve-spin { to { transform: rotate(360deg); } }
627
+ /* Never remove the spin outright \u2014 a frozen ring reads as "hung". Slow it instead. */
628
+ @media (prefers-reduced-motion: reduce) { button.approve.is-loading::after { animation-duration: 2.4s; } }
629
+ /* Terminal success. Wins over :disabled's dimming (the button stays disabled after a
630
+ successful approve, and a 0.6-opacity "Approved" reads as not-quite-done). */
631
+ button.approve.is-approved, button.approve.is-approved:disabled { background: #14663c; border-color: #14663c; color: #fff; opacity: 1; }
611
632
  .pending-info { display: flex; flex-wrap: wrap; gap: 0.35rem 1rem; font-size: 0.82rem; margin: 0.3rem 0 0.15rem; }
612
633
  .recipients-missing { color: #e57373; }
613
634
  .preflight { font-size: 0.78rem; padding: 0.1rem 0.45rem; border-radius: 999px; white-space: nowrap; }
@@ -677,12 +698,17 @@ button.approve:disabled { opacity: 0.6; cursor: default; }
677
698
  }
678
699
  .pill { font-size: 0.75rem; padding: 0.1rem 0.5rem; border-radius: 999px; font-weight: 700; }
679
700
  .override { margin: 0.4rem 0 0.15rem 0.25rem; }
680
- button.override-toggle { font: inherit; font-size: 0.78rem; padding: 0.15rem 0.6rem; border: 1px solid #b00; border-radius: 6px; background: transparent; color: #b00; cursor: pointer; }
681
- @media (prefers-color-scheme: dark) { button.override-toggle { border-color: #ff8a80; color: #ff8a80; } }
701
+ button.override-toggle { font: inherit; font-size: 0.78rem; padding: 0.15rem 0.6rem; border: 1px solid #b00; border-radius: 6px; background: transparent; color: #b00; cursor: pointer; transition: background-color 0.12s ease; }
702
+ button.override-toggle:hover { background: #bb00001a; }
703
+ button.override-toggle:focus-visible { outline: 2px solid #b00; outline-offset: 2px; }
704
+ @media (prefers-color-scheme: dark) { button.override-toggle { border-color: #ff8a80; color: #ff8a80; } button.override-toggle:hover { background: #ff8a801f; } button.override-toggle:focus-visible { outline-color: #ff8a80; } }
682
705
  .override-form { margin-top: 0.35rem; display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
683
706
  .override-reason { font: inherit; font-size: 0.85rem; padding: 0.25rem 0.45rem; border: 1px solid #ccc; border-radius: 4px; background: transparent; color: inherit; min-width: 220px; flex: 1 1 220px; }
684
707
  @media (prefers-color-scheme: dark) { .override-reason { border-color: #444; } }
685
- button.override-submit { font: inherit; font-size: 0.8rem; padding: 0.25rem 0.7rem; border: 1px solid #b00; border-radius: 6px; background: #b00; color: #fff; cursor: pointer; }
708
+ button.override-submit { font: inherit; font-size: 0.8rem; padding: 0.25rem 0.7rem; border: 1px solid #b00; border-radius: 6px; background: #b00; color: #fff; cursor: pointer; transition: background-color 0.12s ease, border-color 0.12s ease; }
709
+ button.override-submit:hover:not(:disabled) { background: #900; border-color: #900; }
710
+ button.override-submit:active:not(:disabled) { background: #7a0000; border-color: #7a0000; }
711
+ button.override-submit:focus-visible { outline: 2px solid #b00; outline-offset: 2px; }
686
712
  button.override-submit:disabled { opacity: 0.6; cursor: default; }
687
713
  .override-status { font-size: 0.78rem; color: #999; }
688
714
  .vuln-list { list-style: none; padding: 0; margin: 0; }
@@ -704,7 +730,10 @@ button.override-submit:disabled { opacity: 0.6; cursor: default; }
704
730
  .detail dd input, .detail dd select, .detail dd textarea { width: 100%; box-sizing: border-box; font: inherit; padding: 0.25rem 0.4rem; border: 1px solid #ccc; border-radius: 4px; background: transparent; color: inherit; }
705
731
  .detail dd textarea { min-height: 3.5rem; resize: vertical; }
706
732
  .detail-saved { font-size: 0.8rem; color: #2a8; }
707
- button.trigger-renovate { font: inherit; font-size: 0.8rem; padding: 0.15rem 0.6rem; margin-left: 0.5rem; border: 1px solid #888; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; }
733
+ button.trigger-renovate { font: inherit; font-size: 0.8rem; padding: 0.15rem 0.6rem; margin-left: 0.5rem; border: 1px solid #888; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; transition: background-color 0.12s ease, border-color 0.12s ease; }
734
+ button.trigger-renovate:hover:not(:disabled) { background: #88888822; border-color: #666; }
735
+ button.trigger-renovate:focus-visible { outline: 2px solid #888; outline-offset: 2px; }
736
+ button.trigger-renovate:disabled { opacity: 0.6; cursor: default; }
708
737
  /* Alarm chip strip \u2014 mirrors fleet-render.ts chip styles (NOT the page's .pill classes). */
709
738
  .chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
710
739
  .chip { font-size: 0.8rem; padding: 0.1rem 0.5rem; border-radius: 6px; background: #f0f0f0; }
@@ -779,18 +808,31 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
779
808
  document.querySelectorAll("button.approve").forEach((b) => {
780
809
  b.addEventListener("click", async () => {
781
810
  b.disabled = true;
811
+ // is-loading swaps the label for a spinner; aria-busy is the same news for a
812
+ // screen reader, which cannot see it. Both are cleared in the finally below so
813
+ // no exit path can strand the button spinning forever.
814
+ b.classList.add("is-loading");
815
+ b.setAttribute("aria-busy", "true");
782
816
  try {
783
817
  const res = await fetch(b.dataset.approveUrl, { method: "POST" });
784
818
  if (res.ok) {
785
819
  b.textContent = "Approved";
820
+ // Terminal success: darker green, and it stays disabled (approving twice is
821
+ // a no-op server-side, but the button should not invite the second click).
822
+ b.classList.add("is-approved");
786
823
  } else {
787
824
  // A 409 carries { reason, blockers } \u2014 surface WHY instead of a bare
788
825
  // "Failed" next to a possibly-stale green chip. textContent/title
789
826
  // assignment only (no innerHTML), so server strings stay inert.
790
827
  const data = await res.json().catch(() => null);
791
828
  b.textContent = data && data.reason === "send-blocked" ? "Blocked" : "Failed";
792
- if (data && Array.isArray(data.blockers)) b.title = data.blockers.join("
793
- ");
829
+ // Double-backslash, NOT single: this whole block lives inside the renderer's
830
+ // template literal, so a single-backslash escape is consumed HERE at build
831
+ // time and emits a real newline into the served HTML \u2014 an unterminated string
832
+ // literal that kills the parse of the entire script element, taking every
833
+ // handler on the page with it (exactly how approve/override/renovate went
834
+ // dead). Note this comment must also avoid backticks for the same reason.
835
+ if (data && Array.isArray(data.blockers)) b.title = data.blockers.join("\\n");
794
836
  b.disabled = false;
795
837
  }
796
838
  } catch {
@@ -798,6 +840,9 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
798
840
  // the button doesn't sit permanently disabled reading "Approve".
799
841
  b.textContent = "Failed";
800
842
  b.disabled = false;
843
+ } finally {
844
+ b.classList.remove("is-loading");
845
+ b.removeAttribute("aria-busy");
801
846
  }
802
847
  });
803
848
  });
@@ -1614,15 +1659,18 @@ export {
1614
1659
  bumpDeps,
1615
1660
  convertToPnpm,
1616
1661
  depsAudit,
1662
+ domainFromUrl,
1617
1663
  draftReportForSite,
1618
1664
  findDueReports,
1619
1665
  fromAirtableBase,
1620
1666
  fromJsonFile,
1667
+ generateHeaderImage,
1621
1668
  init,
1622
1669
  isRecipeName,
1623
1670
  lighthouseAudit,
1624
1671
  lintAudit,
1625
1672
  loadBundledImages,
1673
+ loadPlate,
1626
1674
  localPath,
1627
1675
  onboard,
1628
1676
  renderCockpitHtml,