From 485b578e6df777e15c3befde90516f73cdb97c38 Mon Sep 17 00:00:00 2001 From: Luis Tanafranca <80248146+ltanafranca1004@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:14:38 -0700 Subject: [PATCH] feat(events-crawler): stage West Vancouver Memorial Library behind an enabled flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit West Van Memorial Library runs The Events Calendar (Tribe) on the same REST shape the five Phase-1 orgs use, so it needs no adapter code — only a registry entry and an image allowlist. Verified live against the crawler's exact URL shape (per_page + start_date + end_date over a 4-month window): 456 upcoming events, 25 returned, every one carrying a venue and a real featured image. Adds `Org.enabled` and lands the new org disabled. This matters because the weekly cron is LIVE (20260724120000_events_crawler_cron.sql — pg_cron jobid 18, Mondays 14:00 UTC), which makes deploying this function the act that puts a new org into production: the next run would auto-publish it into the shared events table the mobile app reads. Landing it disabled decouples "the code is merged" from "the source is live", so activation stays a separate one-line change gated on Savar's sign-off, per CLAUDE.md Decision Authority. The fan-out now iterates ACTIVE_ORGS, so a disabled org is genuinely inert rather than merely absent from the response counts. West Van is a public library rather than a settlement agency, so its calendar mixes relevant programming (Device Clinic, English conversation) with toddler storytimes and Pride Bingo. A relevance filter for library sources lands with the Phase-2 adapter work; keeping this org disabled until then means nothing unfiltered can reach the Events tab. The image hosts are allowlisted now rather than at activation, so flipping `enabled` later needs no accompanying web-side change. No migration, cron, or DB object touched. Nothing deployed. Verification: deno check --node-modules-dir=auto supabase/functions/*/index.ts supabase/functions/_shared/*.ts → clean npx tsc --noEmit → clean (0 lines) npx eslint next.config.ts → clean git diff --name-only origin/main | grep -E 'migrations|backfills' → empty Co-Authored-By: Claude Opus 5 (1M context) --- next.config.ts | 4 ++ supabase/functions/events-crawler/index.ts | 50 ++++++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/next.config.ts b/next.config.ts index d972775..465eea7 100644 --- a/next.config.ts +++ b/next.config.ts @@ -38,6 +38,10 @@ const nextConfig: NextConfig = { { protocol: "https", hostname: "www.centrecanada.org" }, { protocol: "https", hostname: "pirs.bc.ca" }, { protocol: "https", hostname: "www.pirs.bc.ca" }, + // Phase-2 crawler source, staged disabled in events-crawler/index.ts. Allowlisted + // ahead of activation so flipping `enabled` needs no web-side change. + { protocol: "https", hostname: "westvanlibrary.ca" }, + { protocol: "https", hostname: "www.westvanlibrary.ca" }, // events-crawler tier-2 covers (Pexels stock photos for events with no source image) { protocol: "https", hostname: "images.pexels.com" }, // News article images diff --git a/supabase/functions/events-crawler/index.ts b/supabase/functions/events-crawler/index.ts index d705134..f984598 100644 --- a/supabase/functions/events-crawler/index.ts +++ b/supabase/functions/events-crawler/index.ts @@ -37,11 +37,24 @@ interface Org { name: string; /** WordPress host serving /wp-json/tribe/events/v1/events. */ host: string; + /** + * Whether a run actually crawls this org. New orgs land as `false`. + * + * The weekly cron is LIVE (20260724120000_events_crawler_cron.sql — pg_cron jobid 18, + * 'events-crawler-weekly', Mondays 14:00 UTC), so deploying this function is what puts a + * new org into production: the next run would auto-publish it into the shared events + * table the mobile app reads. Landing new orgs disabled decouples the two, leaving + * activation as its own one-line, reviewable change — which needs Savar's sign-off, + * since it writes into shared mobile-facing data (CLAUDE.md, "Decision Authority"). + */ + enabled: boolean; } -// Phase 1: the five highest-relevance orgs, all on The Events Calendar (Tribe). +// Phase 1 (enabled): the five highest-relevance orgs, all on The Events Calendar (Tribe). +// Phase 2 (staged, disabled): further sources from the 2026-07-29 scoping round. +// // Add an org here (test /wp-json/tribe/events/v1/events first) to broaden coverage; -// remember to allowlist its image host in next.config.ts. +// remember to allowlist its image host in next.config.ts, and to land it `enabled: false`. // // NOTE on virtual events: none of these orgs sets Tribe's `is_virtual` flag (it ships // with the paid Virtual Events add-on) — it is false or absent on every event. They mark @@ -51,13 +64,31 @@ interface Org { // centrecanada.org is healthy but currently returns total:0 (empty upcoming calendar) — // a zero count from it is expected, not a fetch failure. const ORGS: Org[] = [ - { slug: 'mosaic', name: 'MOSAIC', host: 'mosaicbc.org' }, - { slug: 'burnaby-nh', name: 'Burnaby Neighbourhood House', host: 'burnabynh.ca' }, - { slug: 'success', name: 'S.U.C.C.E.S.S.', host: 'successbc.ca' }, - { slug: 'centre-canada', name: 'CentreCanada', host: 'centrecanada.org' }, - { slug: 'pirs', name: 'Pacific Immigrant Resources Society', host: 'pirs.bc.ca' }, + { slug: 'mosaic', name: 'MOSAIC', host: 'mosaicbc.org', enabled: true }, + { slug: 'burnaby-nh', name: 'Burnaby Neighbourhood House', host: 'burnabynh.ca', enabled: true }, + { slug: 'success', name: 'S.U.C.C.E.S.S.', host: 'successbc.ca', enabled: true }, + { slug: 'centre-canada', name: 'CentreCanada', host: 'centrecanada.org', enabled: true }, + { slug: 'pirs', name: 'Pacific Immigrant Resources Society', host: 'pirs.bc.ca', enabled: true }, + // --- Phase 2, staged and NOT crawled until `enabled` flips (see Org.enabled) --- + // A public library rather than a settlement agency, so its calendar mixes genuinely + // relevant programming (English conversation, newcomer sessions) with toddler + // storytimes. A relevance filter for library sources lands with the Phase-2 adapter + // work; this org stays disabled until then, so nothing unfiltered can reach the tab. + { + slug: 'westvan-library', + name: 'West Vancouver Memorial Library', + host: 'westvanlibrary.ca', + enabled: false, + }, ]; +/** + * The orgs a run actually crawls. Everything downstream — the fetch fan-out and the + * per-org counts in the response — is scoped to this, so a disabled org is inert rather + * than merely unreported. + */ +const ACTIVE_ORGS = ORGS.filter((org) => org.enabled); + const MAX_PER_ORG = 25; // soonest-first cap so MOSAIC's ~254 events don't flood the tab const MAX_DESCRIPTION_CHARS = 2000; const MAX_TITLE_CHARS = 300; @@ -655,7 +686,10 @@ Deno.serve(async (req: Request) => { // all orgs, so identical topic queries hit the API once. const pexelsCache = new Map>(); const perOrgResults = await Promise.all( - ORGS.map(async (org) => ({ slug: org.slug, rows: await fetchOrgEvents(org, pexelsCache) })), + ACTIVE_ORGS.map(async (org) => ({ + slug: org.slug, + rows: await fetchOrgEvents(org, pexelsCache), + })), ); const collected: EventRow[] = [];