@reddoorla/maintenance 0.54.1 → 0.54.3

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
@@ -294,6 +294,8 @@ RESEND_API_KEY=re_XXXX
294
294
  RESEND_WEBHOOK_SECRET=whsec_XXXX # only for the deployed webhook
295
295
  ```
296
296
 
297
+ > The above is the **report CLI** env. The **deployed Netlify functions** (dashboard + form-ingest) additionally require `TURSO_DATABASE_URL` (+ optional `TURSO_AUTH_TOKEN`) — submissions live in libSQL/Turso, not Airtable. See [Site deployment](#site-deployment-netlify--resend) for the full env table.
298
+
297
299
  ### Operator flow
298
300
 
299
301
  0. **Prereq: refresh Lighthouse scores on each Websites row.** From each site's checkout:
@@ -334,25 +336,28 @@ RESEND_WEBHOOK_SECRET=whsec_XXXX # only for the deployed webhook
334
336
 
335
337
  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.
336
338
 
337
- 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.
339
+ 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 [Site deployment](#site-deployment-netlify--resend) below.
338
340
 
339
- ### Webhook deployment (Netlify + Resend)
341
+ ### Site deployment (Netlify + Resend)
340
342
 
341
- 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.
343
+ This repo's Netlify site hosts the whole dashboard + forms surface: the Resend delivery webhook (`netlify/functions/resend-webhook.mts`), the cockpit/per-site dashboard, the `/submissions` page, and the form-ingest + submission-status endpoints. Deploying is a one-time operation per environment: connect the repo to a Netlify site, set its env vars, register the deployed URL with Resend.
342
344
 
343
345
  **1. Create the Netlify site:**
344
346
 
345
- - New site → Import from Git → pick `tucksravin/reddoor-maintenance`.
347
+ - New site → Import from Git → pick `reddoorla/reddoor-maintenance`.
346
348
  - Branch to deploy: `main`. Build settings are read from [`netlify.toml`](netlify.toml) (no build command needed — functions-only).
347
349
  - Site name: pick something stable (e.g. `reddoor-webhooks`) since it ends up in the public webhook URL.
348
350
 
349
351
  **2. Set env vars** in Site settings → Environment variables:
350
352
 
351
- | Variable | Value |
352
- | ----------------------- | --------------------------------------------------------------------------- |
353
- | `AIRTABLE_PAT` | Same PAT used by the CLI (read+write on the Websites + Reports tables) |
354
- | `AIRTABLE_BASE_ID` | `appHG8nLOzULzXOER` |
355
- | `RESEND_WEBHOOK_SECRET` | Generated by Resend in step 4 — paste back here after creating the endpoint |
353
+ | Variable | Value |
354
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
355
+ | `AIRTABLE_PAT` | Same PAT used by the CLI (read+write on the Websites + Reports tables) |
356
+ | `AIRTABLE_BASE_ID` | `appHG8nLOzULzXOER` |
357
+ | `RESEND_WEBHOOK_SECRET` | Generated by Resend in step 4 — paste back here after creating the endpoint |
358
+ | `TURSO_DATABASE_URL` | `libsql://<db>-<org>.turso.io` — submissions + spam counters live here; the dashboard + forms functions 500 without it |
359
+ | `TURSO_AUTH_TOKEN` | Auth token for that Turso url (required for a remote `libsql://` url) |
360
+ | `FORMS_INGEST_TOKEN` | Shared secret gating `POST /api/forms/:slug` — the SAME value is set on each fleet site |
356
361
 
357
362
  **3. Trigger a deploy** (Deploys → Trigger deploy → Deploy site). When it goes green, curl the health endpoint:
358
363
 
package/dist/cli/bin.js CHANGED
@@ -361,7 +361,7 @@ function securityFields(counts) {
361
361
  }
362
362
  function domainFields(result) {
363
363
  const fields = { "Domain checked at": result.checkedAt };
364
- if (result.certDaysRemaining !== null) fields["Cert days remaining"] = result.certDaysRemaining;
364
+ fields["Cert days remaining"] = result.certDaysRemaining;
365
365
  return fields;
366
366
  }
367
367
  function browserFields(r) {
@@ -2430,17 +2430,30 @@ function readDbConfig() {
2430
2430
  const authToken = process.env.TURSO_AUTH_TOKEN;
2431
2431
  return authToken ? { url, authToken } : { url };
2432
2432
  }
2433
+ function ensureMigrated(url, client) {
2434
+ if (url === ":memory:") return runMigrations(client).then(() => void 0);
2435
+ const cached = migrationsByUrl.get(url);
2436
+ if (cached) return cached;
2437
+ const p = runMigrations(client).then(() => void 0).catch((err) => {
2438
+ migrationsByUrl.delete(url);
2439
+ throw err;
2440
+ });
2441
+ migrationsByUrl.set(url, p);
2442
+ return p;
2443
+ }
2433
2444
  async function openDb(cfg) {
2434
2445
  const clientConfig = cfg.authToken ? { url: cfg.url, authToken: cfg.authToken } : { url: cfg.url };
2435
2446
  const client = createClient(clientConfig);
2436
- await runMigrations(client);
2447
+ await ensureMigrated(cfg.url, client);
2437
2448
  return new Kysely({ dialect: new LibsqlDialect({ client }) });
2438
2449
  }
2450
+ var migrationsByUrl;
2439
2451
  var init_client2 = __esm({
2440
2452
  "src/db/client.ts"() {
2441
2453
  "use strict";
2442
2454
  init_credentials();
2443
2455
  init_migrate();
2456
+ migrationsByUrl = /* @__PURE__ */ new Map();
2444
2457
  }
2445
2458
  });
2446
2459
 
@@ -3608,11 +3621,15 @@ async function browserAudit(ctx) {
3608
3621
  await runner.close?.();
3609
3622
  }
3610
3623
  }
3624
+ var FETCH_TIMEOUT_MS = 1e4;
3611
3625
  function defaultDiscoverDeps() {
3612
3626
  return {
3613
3627
  fetchText: async (url) => {
3614
3628
  try {
3615
- const res = await fetch(url, { redirect: "follow" });
3629
+ const res = await fetch(url, {
3630
+ redirect: "follow",
3631
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
3632
+ });
3616
3633
  if (!res.ok) return null;
3617
3634
  return await res.text();
3618
3635
  } catch {
@@ -3713,9 +3730,17 @@ async function defaultBrowserRunner() {
3713
3730
  for (const url of urls) {
3714
3731
  let status;
3715
3732
  try {
3716
- let res = await fetch(url, { method: "HEAD", redirect: "follow" });
3733
+ let res = await fetch(url, {
3734
+ method: "HEAD",
3735
+ redirect: "follow",
3736
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
3737
+ });
3717
3738
  if (res.status === 405 || res.status === 501) {
3718
- res = await fetch(url, { method: "GET", redirect: "follow" });
3739
+ res = await fetch(url, {
3740
+ method: "GET",
3741
+ redirect: "follow",
3742
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
3743
+ });
3719
3744
  }
3720
3745
  status = res.status;
3721
3746
  } catch {
@@ -5101,7 +5126,12 @@ function makeGitHub(deps) {
5101
5126
  return r.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
5102
5127
  },
5103
5128
  async secretExists(repo, name) {
5104
- const out = await gh(["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
5129
+ const out = await gh([
5130
+ "api",
5131
+ `repos/${repo}/actions/secrets?per_page=100`,
5132
+ "--jq",
5133
+ ".secrets[].name"
5134
+ ]);
5105
5135
  return out.split("\n").map((l) => l.trim()).includes(name);
5106
5136
  },
5107
5137
  async autoMergeEnabled(repo) {
@@ -5111,7 +5141,7 @@ function makeGitHub(deps) {
5111
5141
  async findOpenSelfUpdatingPR(repo) {
5112
5142
  const out = await gh([
5113
5143
  "api",
5114
- `repos/${repo}/pulls?state=open`,
5144
+ `repos/${repo}/pulls?state=open&per_page=100`,
5115
5145
  "--jq",
5116
5146
  '.[] | select(.head.ref | startswith("maint/self-updating-")) | .html_url'
5117
5147
  ]);
@@ -6980,7 +7010,7 @@ async function draftDueReports(base, today) {
6980
7010
  continue;
6981
7011
  }
6982
7012
  const pendingEarlier = reports.find(
6983
- (r) => r.siteId === item.site.id && r.reportType === item.reportType && r.sentAt === null && r.period !== null && r.period < period
7013
+ (r) => r.siteId === item.site.id && r.reportType === item.reportType && r.draftReady && r.sentAt === null && r.period !== null && r.period < period
6984
7014
  );
6985
7015
  if (pendingEarlier) {
6986
7016
  skipped++;