Skip to content

Commit c47e316

Browse files
ralyodioclaude
andauthored
calendar: a called-off game is sent as CANCELLED, not CONFIRMED (0.20.9) (#99)
nichedb now writes an "if necessary" playoff game ESPN deleted as post / Canceled (niche-db #186), the way ESPN writes a cancelled or postponed one. The page and reminders already handle that (post with no score draws the detail as the badge; reminders only fire for pre), but the .ics feed kept the entry CONFIRMED with an hour-before alarm. It is now STATUS:CANCELLED with no alarm, so subscribed calendars remove it. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7bf8cf3 commit c47e316

3 files changed

Lines changed: 42 additions & 8 deletions

File tree

‎apps/web/src/lib/ics.js‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ function title(event) {
6464
* @param {object[]} events
6565
* @param {{ name: string, siteUrl: string, defaultMinutes?: number }} opts
6666
*/
67+
/**
68+
* Called off, in the provider's words. ESPN writes a cancelled or postponed game
69+
* as `post` with that word as the detail and no score, and nichedb writes an
70+
* "if necessary" playoff game ESPN deleted the same way. A postponed fixture is
71+
* re-listed under a new id, so the old entry is done with either way.
72+
*/
73+
export const calledOff = (event) =>
74+
event?.state === 'post' && /^(cancell?ed|postponed)\b/i.test(event.status_detail ?? '');
75+
6776
export function buildCalendar(events, { name, siteUrl, defaultMinutes = 150 }) {
6877
const lines = [
6978
'BEGIN:VCALENDAR',
@@ -112,12 +121,19 @@ export function buildCalendar(events, { name, siteUrl, defaultMinutes = 150 }) {
112121
// Duration is a guess for most sports, so mark it as such rather than
113122
// blocking out someone's calendar as if it were a confirmed meeting.
114123
'TRANSP:TRANSPARENT',
115-
'STATUS:CONFIRMED',
116-
'BEGIN:VALARM',
117-
'TRIGGER:-PT60M',
118-
'ACTION:DISPLAY',
119-
`DESCRIPTION:${esc(`${title(e)} starts in an hour`)}`,
120-
'END:VALARM',
124+
// Kept in the feed rather than dropped: a client only removes an entry it
125+
// is told was cancelled, and one that simply vanishes from a subscription
126+
// stays on some calendars, alarm and all.
127+
...(calledOff(e)
128+
? ['STATUS:CANCELLED']
129+
: [
130+
'STATUS:CONFIRMED',
131+
'BEGIN:VALARM',
132+
'TRIGGER:-PT60M',
133+
'ACTION:DISPLAY',
134+
`DESCRIPTION:${esc(`${title(e)} starts in an hour`)}`,
135+
'END:VALARM',
136+
]),
121137
'END:VEVENT',
122138
);
123139
}

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tipoffwatch.com",
3-
"version": "0.20.8",
3+
"version": "0.20.9",
44
"private": true,
55
"type": "module",
66
"description": "Sports calendar and reminder PWA. Follow any team in the world, get told before they play.",

‎test/feeds.test.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test';
22

33
process.env.DATABASE_URL = 'postgres://localhost:5432/unused';
44

5-
const { buildCalendar } = await import('../apps/web/src/lib/ics.js');
5+
const { buildCalendar, calledOff } = await import('../apps/web/src/lib/ics.js');
66
const { buildFeed } = await import('../apps/web/src/lib/rss.js');
77

88
const EVENT = {
@@ -66,6 +66,24 @@ describe('iCalendar', () => {
6666
});
6767
});
6868

69+
describe('iCalendar: a game called off', () => {
70+
test('is sent as CANCELLED with no alarm, so subscribed calendars drop it', () => {
71+
const off = { ...EVENT, state: 'post', status_detail: 'Canceled', home_score: null };
72+
const out = buildCalendar([off], OPTS);
73+
expect(out).toContain('STATUS:CANCELLED');
74+
expect(out).not.toContain('STATUS:CONFIRMED');
75+
expect(out).not.toContain('BEGIN:VALARM');
76+
});
77+
78+
test('only a post fixture whose detail says so counts', () => {
79+
expect(calledOff({ state: 'post', status_detail: 'Postponed' })).toBe(true);
80+
expect(calledOff({ state: 'post', status_detail: 'Cancelled' })).toBe(true);
81+
expect(calledOff({ state: 'post', status_detail: 'Final' })).toBe(false);
82+
expect(calledOff({ state: 'pre', status_detail: 'Canceled' })).toBe(false);
83+
expect(buildCalendar([EVENT], OPTS)).toContain('STATUS:CONFIRMED');
84+
});
85+
});
86+
6987
describe('RSS', () => {
7088
const xml = buildFeed([EVENT], {
7189
title: 'T',

0 commit comments

Comments
 (0)