// 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/5b98d309-7283-4202-bf43-263dbcec74c2/email-header-web.png"; var SITE_ID = "4ae63819-3f63-4fea-b0de-eb1e5befe607"; var PIXEL = "https://api.corben.world/analytics/open.gif"; // 1x1 open pixel. Opens skew high because Apple Mail Privacy Protection and // Gmail's proxy prefetch images, so read it as a ceiling, not a headcount. function openPixel(kind, who, when) { return ''; } var UNSUB = "https://api.corben.world/campaigns/unsubscribe?user_id=c23ea202-5442-41b4-9a1b-1586e81c50eb&email="; 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(opts.imageAlt || ' : ""; return '' + '' + '' + '' + '' + '
' + esc(opts.preview || "") + '
' + '' + '
' + '' + image + '' + '' + '' + '
' + '

The Founder\'s Break

' + '

' + opts.headline + '

' + '
' + opts.body + '
' + '

' + 'Hosted by OneTouch Studio. ' + 'You are getting this because you RSVPed at foundersbreak.onetouch.studio.' + '

' + 'Unsubscribe from these emails' + ' and we will not email you about the Break again.' + '

' + (opts.pixel || '') + ''; } // The date and place, sized like an invitation rather than a spec sheet. function marquee(when) { return '' + '
' + '

' + esc(when) + '

' + '

7:00 to 9:00 PM

' + '

' + '' + VENUE + '
' + STREET + ', ' + CITY + '

' + '

Free entry. 18 and over, so bring an ID.

' + '
'; } function button(label, href) { return '
' + '' + label + '' + '
'; } // Yes / No in one tap. The links land on the site, which posts to the same // /forms/rsvp the form uses, so there is no second public write endpoint. // {{email}} is a campaign merge field and resolves per recipient. function answerButtons() { var yes = SITE + "/?a=yes&e={{email}}#rsvp"; var no = SITE + "/?a=no&e={{email}}#rsvp"; return '' + '' + '' + '' + '' + '
' + 'Yes, I am coming' + '' + 'Not this week' + '
' + '

One tap, nothing to fill in. Either answer keeps you on the list for future weeks.

'; } // ── 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 = '

The Break is tomorrow night. Same place, same time.

' + marquee(when) + '

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, unsubscribeFor: '{{email}}', pixel: openPixel('reminder', '{{email}}', target), }), text_body: "The Break is tomorrow, " + when + ".\n\n" + VENUE + ", " + STREET + ", " + CITY + "\n7:00 to 9:00 PM. Free entry, 18 and over.\n\nBring 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 };