feat: detect passholder preview days and filter plain buyouts
All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m9s

- Buyout days are now treated as closed unless they carry a Passholder
  Preview event, in which case they surface as a distinct purple cell
  in the UI showing "Passholder" + hours
- DB gains a special_type column (auto-migrated on next startup)
- scrape.ts threads specialType through to upsertDay
- debug.ts now shows events, isBuyout, isPassholderPreview, and
  specialType in the parsed result section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 10:53:05 -04:00
parent 7c28d8f89f
commit 91e09b0548
6 changed files with 114 additions and 26 deletions

View File

@@ -13,11 +13,12 @@ export function openDb(): Database.Database {
db.pragma("journal_mode = WAL");
db.exec(`
CREATE TABLE IF NOT EXISTS park_days (
park_id TEXT NOT NULL,
date TEXT NOT NULL, -- YYYY-MM-DD
is_open INTEGER NOT NULL DEFAULT 0,
hours_label TEXT,
scraped_at TEXT NOT NULL,
park_id TEXT NOT NULL,
date TEXT NOT NULL, -- YYYY-MM-DD
is_open INTEGER NOT NULL DEFAULT 0,
hours_label TEXT,
special_type TEXT, -- 'passholder_preview' | null
scraped_at TEXT NOT NULL,
PRIMARY KEY (park_id, date)
);
CREATE TABLE IF NOT EXISTS park_api_ids (
@@ -28,6 +29,12 @@ export function openDb(): Database.Database {
discovered_at TEXT NOT NULL
)
`);
// Migrate existing databases that predate the special_type column
try {
db.exec(`ALTER TABLE park_days ADD COLUMN special_type TEXT`);
} catch {
// Column already exists — safe to ignore
}
return db;
}
@@ -36,21 +43,24 @@ export function upsertDay(
parkId: string,
date: string,
isOpen: boolean,
hoursLabel?: string
hoursLabel?: string,
specialType?: string
) {
db.prepare(`
INSERT INTO park_days (park_id, date, is_open, hours_label, scraped_at)
VALUES (?, ?, ?, ?, ?)
INSERT INTO park_days (park_id, date, is_open, hours_label, special_type, scraped_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (park_id, date) DO UPDATE SET
is_open = excluded.is_open,
hours_label = excluded.hours_label,
scraped_at = excluded.scraped_at
`).run(parkId, date, isOpen ? 1 : 0, hoursLabel ?? null, new Date().toISOString());
is_open = excluded.is_open,
hours_label = excluded.hours_label,
special_type = excluded.special_type,
scraped_at = excluded.scraped_at
`).run(parkId, date, isOpen ? 1 : 0, hoursLabel ?? null, specialType ?? null, new Date().toISOString());
}
export interface DayData {
isOpen: boolean;
hoursLabel: string | null;
specialType: string | null;
}
/**
@@ -65,7 +75,7 @@ export function getDateRange(
): Record<string, Record<string, DayData>> {
const rows = db
.prepare(
`SELECT park_id, date, is_open, hours_label
`SELECT park_id, date, is_open, hours_label, special_type
FROM park_days
WHERE date >= ? AND date <= ?`
)
@@ -74,6 +84,7 @@ export function getDateRange(
date: string;
is_open: number;
hours_label: string | null;
special_type: string | null;
}[];
const result: Record<string, Record<string, DayData>> = {};
@@ -82,6 +93,7 @@ export function getDateRange(
result[row.park_id][row.date] = {
isOpen: row.is_open === 1,
hoursLabel: row.hours_label,
specialType: row.special_type,
};
}
return result;