fix: timezone-aware signals, low-priority cancelled events

- computeSignals uses startOfDay(timeZone) for 'later today' boundary
- Cancelled events get urgency 0.1, excluded from context inProgress/nextEvent

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2026-02-28 16:08:07 +00:00
parent e220ccc392
commit bb09de2da6
2 changed files with 44 additions and 11 deletions

View File

@@ -479,4 +479,29 @@ describe("computeSignals", () => {
}) })
expect(computeSignals(event2h1m, now).urgency).toBe(0.5) expect(computeSignals(event2h1m, now).urgency).toBe(0.5)
}) })
test("cancelled events get urgency 0.1 regardless of timing", () => {
const event = makeEvent({
status: "cancelled",
startDate: new Date("2026-01-15T12:20:00Z"), // would be 0.9 if not cancelled
})
const signals = computeSignals(event, now)
expect(signals.urgency).toBe(0.1)
expect(signals.timeRelevance).toBe(TimeRelevance.Ambient)
})
test("uses timezone for 'later today' boundary", () => {
// now = 2026-01-15T12:00:00Z = 2026-01-15T21:00:00 JST (UTC+9)
// event at 2026-01-15T15:30:00Z = 2026-01-16T00:30:00 JST — next day in JST
const event = makeEvent({
startDate: new Date("2026-01-15T15:30:00Z"),
})
// Without timezone: UTC day ends at 2026-01-16T00:00:00Z, event is before that → "later today"
expect(computeSignals(event, now).urgency).toBe(0.5)
// With Asia/Tokyo: local day ends at 2026-01-15T15:00:00Z (midnight Jan 16 JST),
// event is after that → "future days"
expect(computeSignals(event, now, "Asia/Tokyo").urgency).toBe(0.2)
})
}) })

View File

@@ -4,6 +4,7 @@ import { TimeRelevance, UnknownActionError } from "@aris/core"
import { DAVClient } from "tsdav" import { DAVClient } from "tsdav"
import type { CalDavDAVClient, CalDavEventData, CalDavFeedItem } from "./types.ts" import type { CalDavDAVClient, CalDavEventData, CalDavFeedItem } from "./types.ts"
import { CalDavEventStatus } from "./types.ts"
import { CalDavCalendarKey, type CalendarContext } from "./calendar-context.ts" import { CalDavCalendarKey, type CalendarContext } from "./calendar-context.ts"
import { parseICalEvents } from "./ical-parser.ts" import { parseICalEvents } from "./ical-parser.ts"
@@ -106,9 +107,10 @@ export class CalDavSource implements FeedSource<CalDavFeedItem> {
} }
const now = context.time const now = context.time
const inProgress = events.filter((e) => !e.isAllDay && e.startDate <= now && e.endDate > now) const active = events.filter((e) => e.status !== CalDavEventStatus.Cancelled)
const inProgress = active.filter((e) => !e.isAllDay && e.startDate <= now && e.endDate > now)
const upcoming = events const upcoming = active
.filter((e) => !e.isAllDay && e.startDate > now) .filter((e) => !e.isAllDay && e.startDate > now)
.sort((a, b) => a.startDate.getTime() - b.startDate.getTime()) .sort((a, b) => a.startDate.getTime() - b.startDate.getTime())
@@ -125,7 +127,7 @@ export class CalDavSource implements FeedSource<CalDavFeedItem> {
async fetchItems(context: Context): Promise<CalDavFeedItem[]> { async fetchItems(context: Context): Promise<CalDavFeedItem[]> {
const now = context.time const now = context.time
const events = await this.fetchEvents(context) const events = await this.fetchEvents(context)
return events.map((event) => createFeedItem(event, now)) return events.map((event) => createFeedItem(event, now, this.timeZone))
} }
private fetchEvents(context: Context): Promise<CalDavEventData[]> { private fetchEvents(context: Context): Promise<CalDavEventData[]> {
@@ -290,7 +292,15 @@ function startOfDay(date: Date, timeZone?: string): Date {
return new Date(Date.UTC(year, month - 1, day) - offsetMs) return new Date(Date.UTC(year, month - 1, day) - offsetMs)
} }
export function computeSignals(event: CalDavEventData, now: Date): FeedItemSignals { export function computeSignals(
event: CalDavEventData,
now: Date,
timeZone?: string,
): FeedItemSignals {
if (event.status === CalDavEventStatus.Cancelled) {
return { urgency: 0.1, timeRelevance: TimeRelevance.Ambient }
}
if (event.isAllDay) { if (event.isAllDay) {
return { urgency: 0.3, timeRelevance: TimeRelevance.Ambient } return { urgency: 0.3, timeRelevance: TimeRelevance.Ambient }
} }
@@ -315,11 +325,9 @@ export function computeSignals(event: CalDavEventData, now: Date): FeedItemSigna
return { urgency: 0.7, timeRelevance: TimeRelevance.Upcoming } return { urgency: 0.7, timeRelevance: TimeRelevance.Upcoming }
} }
// Later today // Later today (using local day boundary when timeZone is set)
const startOfDay = new Date(now) const todayStart = startOfDay(now, timeZone)
startOfDay.setUTCHours(0, 0, 0, 0) const endOfDay = new Date(todayStart.getTime() + 24 * 60 * 60 * 1000)
const endOfDay = new Date(startOfDay)
endOfDay.setUTCDate(endOfDay.getUTCDate() + 1)
if (event.startDate.getTime() < endOfDay.getTime()) { if (event.startDate.getTime() < endOfDay.getTime()) {
return { urgency: 0.5, timeRelevance: TimeRelevance.Upcoming } return { urgency: 0.5, timeRelevance: TimeRelevance.Upcoming }
@@ -329,12 +337,12 @@ export function computeSignals(event: CalDavEventData, now: Date): FeedItemSigna
return { urgency: 0.2, timeRelevance: TimeRelevance.Ambient } return { urgency: 0.2, timeRelevance: TimeRelevance.Ambient }
} }
function createFeedItem(event: CalDavEventData, now: Date): CalDavFeedItem { function createFeedItem(event: CalDavEventData, now: Date, timeZone?: string): CalDavFeedItem {
return { return {
id: `caldav-event-${event.uid}${event.recurrenceId ? `-${event.recurrenceId}` : ""}`, id: `caldav-event-${event.uid}${event.recurrenceId ? `-${event.recurrenceId}` : ""}`,
type: "caldav-event", type: "caldav-event",
timestamp: now, timestamp: now,
data: event, data: event,
signals: computeSignals(event, now), signals: computeSignals(event, now, timeZone),
} }
} }