141 lines
18 KiB
TypeScript
141 lines
18 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Seed recurring anchor events into calendar_events.
|
|
*
|
|
* Two flavours, both stored in the same table, distinguished by `source`:
|
|
* - kind:'stay-home' → Bay-anchor weekends where Quinn STAYS HOME and works
|
|
* local demand spike instead of touring out
|
|
* - kind:'travel-to' → Geek/con/kink/Pride events globally that are high
|
|
* demographic-fit; cross-check before deciding a
|
|
* last-minute tour weekend
|
|
*
|
|
* Idempotent: uses (external_id, provider_slug) UNIQUE constraint.
|
|
*
|
|
* Dates: where a 2026 firm date is published, use it. Where the con is annual
|
|
* but 2026 dates aren't confirmed, use the typical-weekend estimate and let
|
|
* Quinn override as cons announce. Better imprecise data than no data.
|
|
*/
|
|
import { openDb, runMigrations } from '@/shared/db';
|
|
import { calendarEventMigrations } from '@/entities/calendar-event';
|
|
import { upsertCalendarEvent } from '@/entities/calendar-event';
|
|
import { logger } from '@/shared/logger';
|
|
|
|
const dbUrl = process.env['QUINN_DB_URL'] ?? 'postgres://quinn:quinn@localhost:25435/quinn';
|
|
const db = openDb(dbUrl);
|
|
|
|
await runMigrations(db, [...calendarEventMigrations]);
|
|
|
|
type AnchorKind = 'stay-home' | 'travel-to';
|
|
|
|
interface AnchorEvent {
|
|
readonly kind: AnchorKind;
|
|
readonly externalId: string;
|
|
readonly title: string;
|
|
readonly startAt: string;
|
|
readonly endAt: string;
|
|
readonly location: string;
|
|
readonly notes: string;
|
|
readonly demographicMatchScore: number;
|
|
}
|
|
|
|
const anchors: readonly AnchorEvent[] = [
|
|
// ── stay-home: Bay anchors (work local demand spike, do not tour out) ──
|
|
{ kind: 'stay-home', externalId: 'anchor:fanimecon-2026', title: 'FanimeCon', startAt: '2026-05-22', endAt: '2026-05-25', location: 'San Jose CA', demographicMatchScore: 9, notes: 'Anime con; 30k+ attendees; queer/trans-friendly; nerd-affluent. STAY HOME.' },
|
|
{ kind: 'stay-home', externalId: 'anchor:sf-pride-2026', title: 'SF Pride', startAt: '2026-06-27', endAt: '2026-06-28', location: 'San Francisco', demographicMatchScore: 10, notes: 'Major queer/trans demographic influx; premium SF weekend.' },
|
|
{ kind: 'stay-home', externalId: 'anchor:folsom-street-fair-2026', title: 'Folsom Street Fair', startAt: '2026-09-27', endAt: '2026-09-27', location: 'San Francisco', demographicMatchScore: 10, notes: 'Kink/leather demographic; high-relevance to GFE/sub services.' },
|
|
{ kind: 'stay-home', externalId: 'anchor:dore-alley-2026', title: 'Up Your Alley / Dore Alley', startAt: '2026-07-26', endAt: '2026-07-26', location: 'San Francisco', demographicMatchScore: 9, notes: 'Summer warmup to Folsom; kink-adjacent.' },
|
|
{ kind: 'stay-home', externalId: 'anchor:outside-lands-2026', title: 'Outside Lands', startAt: '2026-08-08', endAt: '2026-08-10', location: 'San Francisco', demographicMatchScore: 6, notes: 'Music festival; affluent young adult.' },
|
|
{ kind: 'stay-home', externalId: 'anchor:dreamforce-2026', title: 'Dreamforce', startAt: '2026-09-15', endAt: '2026-09-17', location: 'San Francisco', demographicMatchScore: 7, notes: 'Salesforce con; corporate expense-account spend.' },
|
|
{ kind: 'stay-home', externalId: 'anchor:wwdc-2026', title: 'Apple WWDC', startAt: '2026-06-08', endAt: '2026-06-12', location: 'San Jose CA', demographicMatchScore: 6, notes: 'Tech con; affluent attendees.' },
|
|
{ kind: 'stay-home', externalId: 'anchor:furcon-sj-2026', title: 'Further Confusion (FurCon)', startAt: '2026-01-15', endAt: '2026-01-19', location: 'San Jose CA', demographicMatchScore: 10, notes: 'Bay-adjacent fur con; queer-default high-spend demographic; STAY HOME for local pulse.' },
|
|
|
|
// ── travel-to: geek / anime / comic cons (high demographic fit) ──
|
|
{ kind: 'travel-to', externalId: 'anchor:collect-a-con-phx-2026', title: 'Collect-A-Con Phoenix', startAt: '2026-05-16', endAt: '2026-05-17', location: 'Phoenix AZ', demographicMatchScore: 9, notes: 'Anime/comics/collectibles at WestWorld Scottsdale; tour-target weekend match.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:sakuracon-2026', title: 'Sakura-Con', startAt: '2026-04-03', endAt: '2026-04-05', location: 'Seattle WA', demographicMatchScore: 8, notes: 'PNW anime; queer-friendly; small-pond Seattle weekend.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:anime-boston-2026', title: 'Anime Boston', startAt: '2026-04-03', endAt: '2026-04-05', location: 'Boston MA', demographicMatchScore: 8, notes: 'New England anime; college crowd.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:acen-2026', title: 'Anime Central (ACen)', startAt: '2026-05-15', endAt: '2026-05-17', location: 'Rosemont IL', demographicMatchScore: 8, notes: "Midwest's biggest anime con." },
|
|
{ kind: 'travel-to', externalId: 'anchor:animefest-dallas-2026', title: 'AnimeFest', startAt: '2026-05-29', endAt: '2026-06-01', location: 'Dallas TX', demographicMatchScore: 7, notes: 'Texas anime; trans-care legal climate caveat.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:anime-expo-2026', title: 'Anime Expo', startAt: '2026-07-02', endAt: '2026-07-05', location: 'Los Angeles CA', demographicMatchScore: 9, notes: 'Biggest US anime con (~110k); LA-presence weekend; very high fit.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:otakon-2026', title: 'Otakon', startAt: '2026-07-31', endAt: '2026-08-02', location: 'Washington DC', demographicMatchScore: 8, notes: 'East-coast anime flagship (~40k).' },
|
|
{ kind: 'travel-to', externalId: 'anchor:anime-nyc-2026', title: 'Anime NYC', startAt: '2026-11-20', endAt: '2026-11-22', location: 'New York NY', demographicMatchScore: 8, notes: 'Big East-coast late-year anime con.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:eccc-2026', title: 'Emerald City Comic Con', startAt: '2026-02-26', endAt: '2026-03-01', location: 'Seattle WA', demographicMatchScore: 7, notes: 'PNW comic; smaller than SDCC, less LE pressure.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:c2e2-2026', title: 'C2E2', startAt: '2026-03-27', endAt: '2026-03-29', location: 'Chicago IL', demographicMatchScore: 7, notes: "Midwest's biggest comic con." },
|
|
{ kind: 'travel-to', externalId: 'anchor:megacon-2026', title: 'MegaCon Orlando', startAt: '2026-05-21', endAt: '2026-05-24', location: 'Orlando FL', demographicMatchScore: 7, notes: 'Florida geek; check trans-care legal climate.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:phx-fan-fusion-2026', title: 'Phoenix Fan Fusion', startAt: '2026-05-29', endAt: '2026-05-31', location: 'Phoenix AZ', demographicMatchScore: 7, notes: 'Snowbird-adjacent geek crowd.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:awesome-con-2026', title: 'Awesome Con', startAt: '2026-06-12', endAt: '2026-06-14', location: 'Washington DC', demographicMatchScore: 7, notes: 'DC fandom + corporate disposable income.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:sdcc-2026', title: 'San Diego Comic-Con', startAt: '2026-07-23', endAt: '2026-07-26', location: 'San Diego CA', demographicMatchScore: 8, notes: '~130k attendees; hotels saturated months out; LE present but discreet works.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:dragoncon-2026', title: 'DragonCon', startAt: '2026-09-04', endAt: '2026-09-07', location: 'Atlanta GA', demographicMatchScore: 9, notes: '~80k Labor Day weekend; party-heavy, cosplay-heavy, queer-positive. Top last-min target.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:nycc-2026', title: 'New York Comic Con', startAt: '2026-10-08', endAt: '2026-10-11', location: 'New York NY', demographicMatchScore: 8, notes: '~200k; saturated top-pond but demographic shows.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:la-comic-con-2026', title: 'LA Comic Con', startAt: '2026-12-04', endAt: '2026-12-06', location: 'Los Angeles CA', demographicMatchScore: 7, notes: 'End-of-year LA-presence weekend.' },
|
|
|
|
// ── travel-to: gaming / esports ──
|
|
{ kind: 'travel-to', externalId: 'anchor:pax-east-2026', title: 'PAX East', startAt: '2026-04-09', endAt: '2026-04-12', location: 'Boston MA', demographicMatchScore: 8, notes: 'Gaming nerds; ~80k; high paid-intimacy demographic.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:gdc-2026', title: 'GDC', startAt: '2026-03-16', endAt: '2026-03-20', location: 'San Francisco', demographicMatchScore: 7, notes: 'Game-dev conference disposable income; treat like Dreamforce — Bay event.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:pax-west-2026', title: 'PAX West', startAt: '2026-09-04', endAt: '2026-09-07', location: 'Seattle WA', demographicMatchScore: 8, notes: 'West-coast gaming flagship; collides with DragonCon date — pick one.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:evo-2026', title: 'EVO', startAt: '2026-08-07', endAt: '2026-08-09', location: 'Las Vegas NV', demographicMatchScore: 8, notes: 'Fighting-game community ~10k; sits in DEF CON week.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:pax-unplugged-2026', title: 'PAX Unplugged', startAt: '2026-12-04', endAt: '2026-12-06', location: 'Philadelphia PA', demographicMatchScore: 7, notes: 'Board-game crowd; smaller but high fit.' },
|
|
|
|
// ── travel-to: tech / hacker ──
|
|
{ kind: 'travel-to', externalId: 'anchor:ces-2026', title: 'CES', startAt: '2026-01-06', endAt: '2026-01-09', location: 'Las Vegas NV', demographicMatchScore: 6, notes: 'Corporate; expense-account spend; not queer-fit but money is money.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:sxsw-2026', title: 'SXSW', startAt: '2026-03-13', endAt: '2026-03-22', location: 'Austin TX', demographicMatchScore: 7, notes: 'Tech+music+film 10-day; party-heavy; trans-care legal caveat.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:defcon-blackhat-2026', title: 'DEF CON + Black Hat', startAt: '2026-08-06', endAt: '2026-08-09', location: 'Las Vegas NV', demographicMatchScore: 8, notes: '~30k hackers; queer-tolerant; high disposable; chaotic week. Strong Vegas trigger.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:reinvent-2026', title: 'AWS re:Invent', startAt: '2026-11-30', endAt: '2026-12-04', location: 'Las Vegas NV', demographicMatchScore: 6, notes: 'Corporate cloud; expense-account.' },
|
|
|
|
// ── travel-to: furry (highest fit by demographic) ──
|
|
{ kind: 'travel-to', externalId: 'anchor:texas-furry-fiesta-2026', title: 'Texas Furry Fiesta', startAt: '2026-03-20', endAt: '2026-03-22', location: 'Dallas TX', demographicMatchScore: 9, notes: 'Smaller fur con; Texas heat caveat.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:furnal-equinox-2026', title: 'Furnal Equinox', startAt: '2026-03-27', endAt: '2026-03-29', location: 'Toronto ON', demographicMatchScore: 9, notes: 'Canada — visa/border-cross planning required.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:blfc-2026', title: 'Biggest Little Fur Con', startAt: '2026-05-08', endAt: '2026-05-10', location: 'Reno NV', demographicMatchScore: 9, notes: 'Reno + furry = small-pond perfect combination.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:fur-the-more-2026', title: "Fur the 'More", startAt: '2026-05-15', endAt: '2026-05-17', location: 'Tysons VA', demographicMatchScore: 9, notes: 'DC-area fur con.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:anthrocon-2026', title: 'Anthrocon', startAt: '2026-07-02', endAt: '2026-07-05', location: 'Pittsburgh PA', demographicMatchScore: 10, notes: '~13-14k; the big-money fur con; takes over the city. TOP last-min target.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:eurofurence-2026', title: 'Eurofurence', startAt: '2026-08-19', endAt: '2026-08-23', location: 'Berlin DE', demographicMatchScore: 9, notes: 'International — visa/passport prep; high-spend EU crowd.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:mff-2026', title: 'Midwest FurFest', startAt: '2026-12-03', endAt: '2026-12-06', location: 'Rosemont IL', demographicMatchScore: 10, notes: '~14k; #2 US fur con; Chicago hotel logistics solved.' },
|
|
|
|
// ── travel-to: kink / leather ──
|
|
{ kind: 'travel-to', externalId: 'anchor:mal-2026', title: 'MAL (Mid-Atlantic Leather)', startAt: '2026-01-16', endAt: '2026-01-19', location: 'Washington DC', demographicMatchScore: 8, notes: 'MLK weekend; east-coast leather.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:claw-2026', title: 'CLAW', startAt: '2026-04-16', endAt: '2026-04-19', location: 'Cleveland OH', demographicMatchScore: 9, notes: 'Cleveland Leather Annual Weekend; small but dense.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:iml-2026', title: 'IML', startAt: '2026-05-22', endAt: '2026-05-25', location: 'Chicago IL', demographicMatchScore: 9, notes: 'International Mr. Leather; ~10k; Memorial Day weekend; queer-default high-spend.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:folsom-europe-2026', title: 'Folsom Europe', startAt: '2026-09-10', endAt: '2026-09-13', location: 'Berlin DE', demographicMatchScore: 9, notes: 'International kink; high-spend EU.' },
|
|
|
|
// ── travel-to: Napa / St Helena (Bay-adjacent, high-end wine country) ──
|
|
{ kind: 'travel-to', externalId: 'anchor:premiere-napa-valley-2026', title: 'Premiere Napa Valley (charity wine auction)', startAt: '2026-02-21', endAt: '2026-02-21', location: 'St Helena CA', demographicMatchScore: 9, notes: 'Ultra-wealthy collectors; trade-day Friday + auction Saturday. CIA Greystone often hosts auxiliary events. Top-tier disposable.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:cia-worlds-of-flavor-2026', title: 'Worlds of Flavor (CIA Greystone)', startAt: '2026-03-09', endAt: '2026-03-11', location: 'St Helena CA', demographicMatchScore: 7, notes: 'CIA Greystone industry conference; food/wine professionals; smaller crowd but moneyed. Bay-adjacent.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:bottlerock-2026', title: 'BottleRock Napa Valley', startAt: '2026-05-22', endAt: '2026-05-24', location: 'Napa CA', demographicMatchScore: 9, notes: '~40k; $800-2k pass tiers; music+food+wine; very high disposable. Memorial Day collides with FanimeCon + IML — three-way scheduling decision.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:festival-napa-valley-2026', title: 'Festival Napa Valley', startAt: '2026-07-10', endAt: '2026-07-19', location: 'Napa CA', demographicMatchScore: 7, notes: 'Classical music + wine + culinary; 10-day; older affluent crowd; Bay-adjacent.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:napa-film-festival-2026', title: 'Napa Valley Film Festival', startAt: '2026-11-04', endAt: '2026-11-08', location: 'Napa CA', demographicMatchScore: 8, notes: 'Boutique film fest; Hollywood-money + wine-country crowd; Bay-adjacent.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:flavor-napa-valley-2026', title: 'Flavor! Napa Valley (CIA Greystone)', startAt: '2026-11-13', endAt: '2026-11-15', location: 'St Helena CA', demographicMatchScore: 8, notes: 'CIA Greystone consumer food event; foodie disposable; Bay-adjacent NorCal weekend.' },
|
|
|
|
// ── travel-to: Pride (major non-Bay markets) ──
|
|
{ kind: 'travel-to', externalId: 'anchor:la-pride-2026', title: 'LA Pride', startAt: '2026-06-13', endAt: '2026-06-14', location: 'Los Angeles CA', demographicMatchScore: 7, notes: 'LA-presence weekend.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:nyc-pride-2026', title: 'NYC Pride', startAt: '2026-06-27', endAt: '2026-06-28', location: 'New York NY', demographicMatchScore: 7, notes: 'Big tourism influx.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:chicago-pride-2026', title: 'Chicago Pride', startAt: '2026-06-27', endAt: '2026-06-28', location: 'Chicago IL', demographicMatchScore: 7, notes: 'Late June Pride weekend.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:dc-pride-2026', title: 'DC Pride', startAt: '2026-06-06', endAt: '2026-06-07', location: 'Washington DC', demographicMatchScore: 7, notes: 'Capital Pride weekend.' },
|
|
{ kind: 'travel-to', externalId: 'anchor:atlanta-pride-2026', title: 'Atlanta Pride', startAt: '2026-10-10', endAt: '2026-10-11', location: 'Atlanta GA', demographicMatchScore: 7, notes: 'Late-season; pairs nicely if doing post-DragonCon Atlanta follow-up.' },
|
|
];
|
|
|
|
let added = 0;
|
|
for (const a of anchors) {
|
|
const source = `manual:seed-recurring-anchors:${a.kind}`;
|
|
await upsertCalendarEvent(db, {
|
|
externalId: a.externalId,
|
|
title: a.title,
|
|
startAt: a.startAt,
|
|
endAt: a.endAt,
|
|
allDay: true,
|
|
location: a.location,
|
|
notes: a.notes,
|
|
calendarName: 'recurring-anchors',
|
|
providerSlug: 'quinn',
|
|
});
|
|
// Mark recurring + score + source via raw update (upsertCalendarEvent doesn't expose new columns yet)
|
|
await db`
|
|
UPDATE calendar_events
|
|
SET is_recurring = true,
|
|
demographic_match_score = ${a.demographicMatchScore},
|
|
source = ${source}
|
|
WHERE external_id = ${a.externalId} AND provider_slug = 'quinn'
|
|
`;
|
|
added += 1;
|
|
}
|
|
|
|
logger.info({ count: added }, 'recurring anchors seed complete');
|
|
await db.close();
|