@reddoorla/maintenance 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -314,7 +314,60 @@ RESEND_WEBHOOK_SECRET=whsec_XXXX # only for the deployed webhook
314
314
 
315
315
  Renders + sends every Reports row with `Draft ready=true && Approved to send=true && Sent at IS NULL`. Stamps `Sent at` + `Delivery status=pending` on each.
316
316
 
317
- 5. **Delivery status updates automatically** via the Resend webhook (Netlify Function at `netlify/functions/resend-webhook.mts`) — `Delivery status` flips to `delivered` / `bounced` / `complained` as events arrive.
317
+ 5. **Delivery status updates automatically** via the Resend webhook (Netlify Function at `netlify/functions/resend-webhook.mts`) — `Delivery status` flips to `delivered` / `bounced` / `complained` as events arrive. Deploy procedure: see [Webhook deployment](#webhook-deployment-netlify--resend) below.
318
+
319
+ ### Webhook deployment (Netlify + Resend)
320
+
321
+ The Resend delivery webhook lives at `netlify/functions/resend-webhook.mts` in this repo. Deploying it is a one-time operation per environment: connect the repo to a Netlify site, set three env vars, register the deployed URL with Resend.
322
+
323
+ **1. Create the Netlify site:**
324
+
325
+ - New site → Import from Git → pick `tucksravin/reddoor-maintenance`.
326
+ - Branch to deploy: `main`. Build settings are read from [`netlify.toml`](netlify.toml) (no build command needed — functions-only).
327
+ - Site name: pick something stable (e.g. `reddoor-webhooks`) since it ends up in the public webhook URL.
328
+
329
+ **2. Set env vars** in Site settings → Environment variables:
330
+
331
+ | Variable | Value |
332
+ | ----------------------- | --------------------------------------------------------------------------- |
333
+ | `AIRTABLE_PAT` | Same PAT used by the CLI (read+write on the Websites + Reports tables) |
334
+ | `AIRTABLE_BASE_ID` | `appHG8nLOzULzXOER` |
335
+ | `RESEND_WEBHOOK_SECRET` | Generated by Resend in step 4 — paste back here after creating the endpoint |
336
+
337
+ **3. Trigger a deploy** (Deploys → Trigger deploy → Deploy site). When it goes green, curl the health endpoint:
338
+
339
+ ```bash
340
+ curl https://<your-site>.netlify.app/.netlify/functions/resend-webhook
341
+ ```
342
+
343
+ Expected:
344
+
345
+ ```json
346
+ {
347
+ "status": "ok",
348
+ "service": "reddoor-resend-webhook",
349
+ "env": {
350
+ "RESEND_WEBHOOK_SECRET": true,
351
+ "AIRTABLE_PAT": true,
352
+ "AIRTABLE_BASE_ID": true
353
+ }
354
+ }
355
+ ```
356
+
357
+ Any `false` means the env var didn't land — re-check step 2 and re-deploy. The health endpoint reports presence-only and never returns secret values, so the output is safe to share in a support ticket.
358
+
359
+ **4. Register the webhook in Resend:** [resend.com/webhooks](https://resend.com/webhooks) → Add Endpoint → paste the function URL from step 3 → select events `email.delivered`, `email.bounced`, `email.complained` → save. Copy the generated **Signing Secret** (`whsec_…`) into Netlify's `RESEND_WEBHOOK_SECRET` env var (step 2) and trigger another deploy so the secret takes effect.
360
+
361
+ **5. End-to-end smoke test (using ERP Industrials):**
362
+
363
+ ```bash
364
+ # from your CLI:
365
+ reddoor-maint report erp-industrials # drafts a Maintenance report → Airtable
366
+ # approve the draft in Airtable, then:
367
+ reddoor-maint report --send-ready
368
+ ```
369
+
370
+ Within a few minutes, watch the ERP Reports row's `Delivery status` flip from `pending` → `delivered` as Resend pings the webhook. Function logs in Netlify show each event with its messageId + new status. If the webhook returns 5xx, svix retries; check the Netlify function log for the error envelope.
318
371
 
319
372
  ### Frequency math
320
373
 
package/dist/cli/bin.js CHANGED
@@ -284,12 +284,37 @@ var init_reports = __esm({
284
284
 
285
285
  // src/reports/maintenance-email/assets/index.ts
286
286
  import { readFile as readFile13 } from "fs/promises";
287
+ import { existsSync as existsSync2 } from "fs";
287
288
  import { dirname as dirname2, join as join21 } from "path";
288
289
  import { fileURLToPath as fileURLToPath2 } from "url";
290
+ function resolveAssetsDir() {
291
+ if (cachedAssetsDir) return cachedAssetsDir;
292
+ let dir = dirname2(fileURLToPath2(import.meta.url));
293
+ while (true) {
294
+ const srcCandidate = join21(dir, "src", "reports", "maintenance-email", "assets", "check.png");
295
+ if (existsSync2(srcCandidate)) {
296
+ cachedAssetsDir = dirname2(srcCandidate);
297
+ return cachedAssetsDir;
298
+ }
299
+ const distCandidate = join21(dir, "dist", "reports", "maintenance-email", "assets", "check.png");
300
+ if (existsSync2(distCandidate)) {
301
+ cachedAssetsDir = dirname2(distCandidate);
302
+ return cachedAssetsDir;
303
+ }
304
+ const parent = dirname2(dir);
305
+ if (parent === dir) {
306
+ throw new Error(
307
+ `loadBundledImages: could not locate maintenance-email assets dir by walking up from ${fileURLToPath2(import.meta.url)}. Checked both src/ and dist/ layouts.`
308
+ );
309
+ }
310
+ dir = parent;
311
+ }
312
+ }
289
313
  async function loadBundledImages() {
314
+ const assetsDir = resolveAssetsDir();
290
315
  const [check, blurred] = await Promise.all([
291
- readFile13(join21(here, "check.png")),
292
- readFile13(join21(here, "blurredTests.jpg"))
316
+ readFile13(join21(assetsDir, "check.png")),
317
+ readFile13(join21(assetsDir, "blurredTests.jpg"))
293
318
  ]);
294
319
  return {
295
320
  check: {
@@ -306,13 +331,13 @@ async function loadBundledImages() {
306
331
  }
307
332
  };
308
333
  }
309
- var here, CHECK_CID, BLURRED_CID;
334
+ var CHECK_CID, BLURRED_CID, cachedAssetsDir;
310
335
  var init_assets = __esm({
311
336
  "src/reports/maintenance-email/assets/index.ts"() {
312
337
  "use strict";
313
- here = dirname2(fileURLToPath2(import.meta.url));
314
338
  CHECK_CID = "rd-check-png";
315
339
  BLURRED_CID = "rd-blurred-tests-jpg";
340
+ cachedAssetsDir = null;
316
341
  }
317
342
  });
318
343
 
@@ -2940,8 +2965,8 @@ import { fileURLToPath } from "url";
2940
2965
  import { dirname, join as join18 } from "path";
2941
2966
  function selfPackageVersion(callerImportMetaUrl) {
2942
2967
  try {
2943
- const here3 = dirname(fileURLToPath(callerImportMetaUrl));
2944
- const raw = readFileSync(join18(here3, "..", "..", "package.json"), "utf-8");
2968
+ const here2 = dirname(fileURLToPath(callerImportMetaUrl));
2969
+ const raw = readFileSync(join18(here2, "..", "..", "package.json"), "utf-8");
2945
2970
  const pkg = JSON.parse(raw);
2946
2971
  return pkg.version ?? "0.0.0";
2947
2972
  } catch {
@@ -3498,8 +3523,8 @@ function resolvePackageVersion(fromDir) {
3498
3523
  }
3499
3524
 
3500
3525
  // src/cli/bin.ts
3501
- var here2 = dirname5(fileURLToPath3(import.meta.url));
3502
- var version = resolvePackageVersion(here2);
3526
+ var here = dirname5(fileURLToPath3(import.meta.url));
3527
+ var version = resolvePackageVersion(here);
3503
3528
  var AUDIT_DESCRIPTIONS = {
3504
3529
  deps: "Diff site package.json against the bundled baseline version map.",
3505
3530
  lighthouse: "Run @lhci/cli autorun using the canonical lighthouserc.",