feat: publish local calendars via two-way CalDAV (opt-in, secret token URL)

Backend:
- LocalCalendar gains caldav_published + dav_token + dav_ctag; LocalEvent gains
  etag (migrations in main.py). bump_dav() refreshes ctag/etag on every local
  event write (create/update/delete/import).
- local_router: PUT /calendars/{id} accepts caldav_published (mints/revokes
  token), new POST /calendars/{id}/dav-token/rotate, _cal_dict exposes
  caldav_published + caldav_url.
- New dav_router mounted at root (/dav/{token}/...): a minimal two-way CalDAV
  server (OPTIONS/PROPFIND/REPORT/GET/PUT/DELETE) reusing ical_io build/parse,
  ctag-based change detection. Secret token = auth, no login.

Frontend:
- Settings calendar table: per-local-calendar publish toggle + subscribe URL
  with copy and token-rotate; i18n (de/en) and styling.

Note: reverse proxy must allow WebDAV methods (PROPFIND/REPORT/PUT/DELETE).
VALARM/reminders are not round-tripped via CalDAV.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-01 10:19:01 +02:00
parent a591eb51d3
commit 47944ed952
9 changed files with 530 additions and 6 deletions

View File

@@ -1462,6 +1462,16 @@ a { color: var(--primary); text-decoration: none; }
.ct-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; margin-right: 5px; vertical-align: middle; flex-shrink: 0; }
.ct-empty { color: var(--text-3); font-size: 13px; padding: 16px 0; text-align: center; }
.ct-toggle { cursor: pointer; }
.ct-dav-toggle.on { color: var(--primary); }
.ct-dav-row td { padding-top: 4px; padding-bottom: 12px; }
.ct-dav-box { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ct-dav-label { font-size: 12px; color: var(--text-3); }
.ct-dav-url {
flex: 1; min-width: 220px; font-size: 12px; padding: 5px 8px;
border: 1px solid var(--border); border-radius: 6px;
background: var(--surface-2); color: var(--text-1);
}
.ct-dav-hint { font-size: 11px; color: var(--text-3); margin-top: 6px; max-width: 640px; }
.ct-eye, .ct-bell { opacity: .45; transition: opacity .15s; }
.ct-eye[data-ct-visible="1"], .ct-bell[data-ct-on="1"] { opacity: 1; }
.ct-eye:hover, .ct-bell:hover { opacity: 1; }