// The Founder's Break - weekly reminder. // // Runs on a plain interval (the cron runner has no day-of-week syntax), works // out the Eastern weekday itself, and only sends on Wednesday. Deterministic on // purpose: no LLM anywhere in the send path. A kv marker keyed to the event date // makes a double-fire a no-op, which is what lets the job run more than once a // day without ever mailing the list twice. // // env: CORBEN_KEY, LIST_ID, FROM, NOTIFY // event: { force: true } sends regardless of the day, for testing. var event = ctx.event, env = ctx.env, fetch = ctx.fetch, log = ctx.log, kv = ctx.kv; var API = "https://api.corben.world/mcp"; var TZ = "America/New_York"; var VENUE = "Silver Q Billiards & Sports Bar"; var STREET = "225 W University Ave"; var CITY = "Gainesville, FL 32601"; var SITE = "https://foundersbreak.onetouch.studio"; var MAP = "https://www.google.com/maps/search/?api=1&query=Silver+Q+Billiards+225+W+University+Ave+Gainesville+FL+32601"; var WEEKDAY = 4; // Thursday var SEND_DAY = 3; // Wednesday var CANCELLED = []; // ISO dates to skip; keep in step with site.config.js async function tool(name, args) { var res = await fetch(API, { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + env.CORBEN_KEY }, body: JSON.stringify({ tool: name, arguments: args }), }); if (!res.ok) throw new Error(name + " failed: " + res.status + " " + String(res.text).slice(0, 200)); return res.json; } function etNow() { var now = new Date(); var iso = new Intl.DateTimeFormat("en-CA", { timeZone: TZ, year: "numeric", month: "2-digit", day: "2-digit", }).format(now); var wd = new Intl.DateTimeFormat("en-US", { timeZone: TZ, weekday: "short" }).format(now); var hour = Number(new Intl.DateTimeFormat("en-US", { timeZone: TZ, hour: "2-digit", hourCycle: "h23" }).format(now)) % 24; var order = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 }; return { iso: iso, day: order[wd], hour: hour }; } function addDays(iso, n) { var p = iso.split("-").map(Number); var d = new Date(Date.UTC(p[0], p[1] - 1, p[2])); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); } function pretty(iso) { var p = iso.split("-").map(Number); return new Intl.DateTimeFormat("en-US", { timeZone: "UTC", weekday: "long", month: "long", day: "numeric", }).format(new Date(Date.UTC(p[0], p[1] - 1, p[2]))); } function esc(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """).replace(/'/g, "'"); } var HERO = "https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/e7119448-83ba-4da4-95d8-b64045c21160/email-header-trim.png"; var SANS = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif"; // One shell for every outbound email so they all look like the site. `image` // is optional: the internal notification does not need a photograph. function shell(opts) { var image = opts.image ? '
| ' + ' |
| ' +
' ' + esc(when) + ' ' + '7:00 to 9:00 PM ' + '' +
'' +
VENUE + ' Free to get in. 18 and over, so bring an ID. ' + ' |
| ' + '' + label + '' + ' |
| ' + 'Yes, I am coming' + ' | ' + '' + ' | ' + 'Not this week' + ' | ' + '
One tap, nothing to fill in. You are already on the list either way.
'; } // ── main ──────────────────────────────────────────────────────────────── var force = !!(event && event.force); var now = etNow(); if (!force) { if (now.day !== SEND_DAY) return { skipped: "not wednesday (et day " + now.day + ")" }; if (now.hour < 9 || now.hour > 20) return { skipped: "outside sending hours (et hour " + now.hour + ")" }; } // Tomorrow is the Thursday we are reminding people about. var target = addDays(now.iso, (WEEKDAY - now.day + 7) % 7 || 7); if (CANCELLED.indexOf(target) !== -1) return { skipped: "next Break is cancelled: " + target }; var marker = "founders-break:reminder:" + target; if (!force && (await kv.get(marker))) return { skipped: "already sent for " + target }; var when = pretty(target); var body = 'Tomorrow night. Same tables, same time. Come build something with people who are also building something.
' + marquee(when) + 'Still zero pool skills required. Bring a valid ID and whatever you are working on.
' + answerButtons(); var campaign = await tool("create_campaign", { name: "Founder's Break reminder " + target, subject: "Tomorrow: The Founder's Break, " + when, list_id: env.LIST_ID, from_address_id: env.FROM_ADDRESS_ID, reply_to: env.NOTIFY, html_body: shell({ headline: "See you tomorrow.", preview: when + ", 7:00 PM at Silver Q. Free, 18+.", image: HERO, imageAlt: "A blueprint drawing of a pool table and the break shot.", body: body, }), text_body: "The Break is tomorrow, " + when + ".\n\n" + VENUE + ", " + STREET + ", " + CITY + "\n7:00 to 9:00 PM. Free, 18+.\n\nZero pool skills required. Bring a valid ID.\n\n" + SITE, tags: ["founders-break", "reminder"], }); var campaignId = campaign && (campaign.id || (campaign.result && campaign.result.id)); if (!campaignId) throw new Error("create_campaign returned no id: " + JSON.stringify(campaign).slice(0, 200)); // Mark before sending. A crash after this leaves the week unsent, which is a // far better failure than mailing the whole list twice. await kv.set(marker, new Date().toISOString()); await tool("send_campaign", { id: campaignId }); log("sent reminder for " + target); return { ok: true, event_date: target, campaign_id: campaignId };