diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-09-20 07:57:36 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-09-20 07:57:36 +0000 |
| commit | 4d160762aeef531208f8cf6094a0e58a9e7d53ec (patch) | |
| tree | 79584994808a38a2518ad986dc739daf720d3d77 /web/templates/partials | |
| parent | c5ab2f9a7f6a0b1e456e13934268d1509504c55a (diff) | |
Add .ics calendar feed support
doot reads Google Calendar through a service account, by calendar ID. That
cannot consume an .ics URL, and it cannot see a calendar subscribed to inside a
personal Google account either, because Google does not allow re-sharing a
subscribed calendar. Published schedules — sports fixtures, school terms, bin
collections — are distributed almost exclusively as .ics, so none of them could
be added at all.
Feeds are now fetched on the normal calendar refresh and merged into
calendar_events alongside Google events, so the timeline, widget and everything
downstream keep seeing one list and needed no changes.
Failure handling is deliberately asymmetric. A Google error is still returned as
an error, unchanged, so an outage there surfaces. A single failing feed is
recorded against that feed and skipped, because one dead URL should not take the
whole calendar down — and last_error is shown in settings, since a feed that has
quietly stopped resolving otherwise looks identical to a team with no games
left.
The parser handles what real feeds actually contain: line unfolding, CRLF and
LF, UTC / TZID / VALUE=DATE times, and escaped text. Two details worth naming:
a TZID time is wall-clock in that zone, and reading it as UTC would put a 1:15pm
Pacific kickoff on the timeline at 1:15pm local — wrong by the offset but
entirely plausible-looking. And non-calendar content is a hard error rather than
zero events, because an HTML error page from a moved feed URL is the common
failure and "no events" reads as success.
RRULE is deliberately unsupported: every schedule feed this targets expands
recurrence itself, one VEVENT per fixture, and a correct expansion is a large
amount of subtle code. A recurring event appears once rather than wrongly.
Settings gains an add/enable/disable/remove UI. webcal:// links are accepted and
rewritten, since that is how feeds are usually advertised.
16 parser tests, 5 store tests. Verified against the real Seattle Seahawks feed:
21 events, all with valid times.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'web/templates/partials')
| -rw-r--r-- | web/templates/partials/ics-feeds.html | 43 |
1 files changed, 43 insertions, 0 deletions
diff --git a/web/templates/partials/ics-feeds.html b/web/templates/partials/ics-feeds.html new file mode 100644 index 0000000..4ee1ad6 --- /dev/null +++ b/web/templates/partials/ics-feeds.html @@ -0,0 +1,43 @@ +{{define "ics-feed-list"}} +<div id="ics-feed-list" class="space-y-3"> + {{if .Feeds}} + {{range .Feeds}} + <div class="flex flex-wrap items-center gap-3 p-3 rounded-lg border border-white/10 bg-white/5"> + <div class="flex-1 min-w-0"> + <div class="flex items-center gap-2"> + <span class="text-white {{if not .Enabled}}opacity-50{{end}}">{{.Name}}</span> + {{if not .Enabled}}<span class="text-xs text-white/40">(off)</span>{{end}} + </div> + <div class="text-xs text-white/40 truncate">{{.URL}}</div> + <div class="text-xs mt-1 {{if .LastError}}text-red-400{{else}}text-white/40{{end}}"> + {{if .LastError}} + Failed: {{.LastError}} + {{else}} + {{.EventCount}} events · last checked {{.LastFetched}} + {{end}} + </div> + </div> + <form hx-post="/settings/ics-feeds/{{.ID}}/toggle" hx-target="#ics-feed-list" hx-swap="outerHTML"> + <input type="hidden" name="enabled" value="{{if .Enabled}}false{{else}}true{{end}}"> + <button type="submit" class="text-xs px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/20 text-white/70 hover:text-white transition-colors"> + {{if .Enabled}}Disable{{else}}Enable{{end}} + </button> + </form> + <button class="text-xs px-3 py-1.5 rounded-lg border border-red-500/30 text-red-400 hover:bg-red-500/10 transition-colors" + hx-delete="/settings/ics-feeds/{{.ID}}" + hx-target="#ics-feed-list" + hx-swap="outerHTML" + hx-confirm="Remove {{.Name}}? Its events disappear from the timeline."> + Remove + </button> + </div> + {{end}} + {{else}} + <p class="text-sm text-white/40"> + No calendar feeds yet. Paste an .ics or webcal:// link below — published + schedules (sports fixtures, school terms, bin collections) are almost + always distributed this way. + </p> + {{end}} +</div> +{{end}} |
