Add today-view plugin with Tana, ICS, and Donetick data sources
Implements an e-ink daily dashboard plugin ("today") with four sections:
date/focus/success header, agenda timeline, chores checklist, and
due/overdue task lists.
Data sources:
- Focus & success text: Tana daily note (src/sources/tana.ts)
- Due/overdue tasks: Tana task search (src/sources/tana.ts)
- Agenda events: ICS calendar feeds (src/sources/ics.ts)
- Chores: Donetick API (src/sources/donetick.ts)
All sources fetch in parallel and fall back gracefully on error.
Tests use mock HTTP servers with synthetic data — no real services needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ddcb03d3dd
commit
2e34246d14
12 changed files with 1665 additions and 11 deletions
71
src/sources/ics.ts
Normal file
71
src/sources/ics.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import * as ical from 'node-ical';
|
||||
import type { AgendaEvent } from '../todayview';
|
||||
|
||||
export async function fetchICSAgenda(
|
||||
urls: string[]
|
||||
): Promise<AgendaEvent[]> {
|
||||
const now = new Date();
|
||||
const startOfDay = new Date(now);
|
||||
startOfDay.setHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date(now);
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
|
||||
const allEvents = (
|
||||
await Promise.all(urls.map((url) => fetchOne(url, startOfDay, endOfDay)))
|
||||
).flat();
|
||||
|
||||
allEvents.sort((a, b) => a.sortKey - b.sortKey);
|
||||
|
||||
return allEvents.map(({ sortKey, ...event }) => event);
|
||||
}
|
||||
|
||||
interface RawAgendaEvent extends AgendaEvent {
|
||||
sortKey: number;
|
||||
}
|
||||
|
||||
async function fetchOne(
|
||||
url: string,
|
||||
start: Date,
|
||||
end: Date
|
||||
): Promise<RawAgendaEvent[]> {
|
||||
const resp = await fetch(url, {
|
||||
headers: [['User-Agent', 'private TRMNL fetcher']],
|
||||
});
|
||||
const data = await resp.text();
|
||||
const parsed = Object.values(await ical.async.parseICS(data)).filter(
|
||||
(a) => a?.type === 'VEVENT'
|
||||
);
|
||||
|
||||
const events: RawAgendaEvent[] = [];
|
||||
|
||||
for (const event of parsed) {
|
||||
for (const instance of ical.expandRecurringEvent(event, {
|
||||
from: start,
|
||||
to: end,
|
||||
excludeExdates: true,
|
||||
expandOngoing: true,
|
||||
includeOverrides: true,
|
||||
})) {
|
||||
if (instance.isFullDay) continue;
|
||||
|
||||
const startTime = formatTime(instance.start);
|
||||
const endTime = formatTime(instance.end);
|
||||
const location = (instance.event as any).location ?? '';
|
||||
|
||||
events.push({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
title: (instance.summary as string) ?? '',
|
||||
where: location,
|
||||
kind: '',
|
||||
sortKey: instance.start.valueOf(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
function formatTime(d: Date): string {
|
||||
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue