Move completed implementation plans (2026-04-20), design specs, and audit documents to docs/archive/ for historical reference while keeping the main docs/ directory focused on active documentation. Archived: - 1 implementation plan (superpowers/plans/) - 2 design specs (superpowers/specs/) - 3 design documents (designs/) - 5 audit/proposal documents (root level) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
5.7 KiB
ICS-URL Subscription — Design Spec
Date: 2026-04-20
Status: Approved
Overview
Allow all family members to subscribe to external calendars via ICS-URL (e.g. public Google, Outlook, or any webcal-compatible feed). Events are fetched periodically and stored locally. Users can choose whether a subscription is private or shared with the whole family.
Requirements
- Any user (not just admins) can add subscriptions
- Per-subscription visibility: private (only creator) or shared (all family members)
- Custom color per subscription, chosen by the user
- Sync interval: shared with existing
SYNC_INTERVAL_MINUTESsetting - Manual "Sync now" button per subscription
- Events from subscriptions are editable; user-modified events are not overwritten on re-sync
- Events are deleted when their subscription is deleted
1. Database
New table: ics_subscriptions
CREATE TABLE ics_subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#6366f1',
shared INTEGER NOT NULL DEFAULT 0, -- 0 = private, 1 = shared with all
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
last_sync TEXT,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
Migrations to calendar_events
Two new columns added via append-only migration entries:
subscription_id INTEGER REFERENCES ics_subscriptions(id) ON DELETE CASCADE— links event to its subscriptionuser_modified INTEGER NOT NULL DEFAULT 0— set to 1 when user edits the event; prevents sync from overwriting
The external_source CHECK constraint ('local', 'google', 'apple') must be extended to include 'ics'. Since SQLite does not support ALTER COLUMN, this is done by recreating the table in the migration.
Visibility filter
The calendar events API filters ICS events as follows:
WHERE external_source != 'ics'
OR subscription_id IN (
SELECT id FROM ics_subscriptions
WHERE shared = 1 OR created_by = :userId
)
2. Backend
New file: server/services/ics-parser.js
Extract parseICS, unfoldLines, formatICSDate, tzLocalToUTC, applyDuration from apple-calendar.js into a shared module. Both apple-calendar.js and the new ics-subscription.js import from here. No logic changes.
New file: server/services/ics-subscription.js
| Export | Description |
|---|---|
fetchAndParse(url) |
Normalize webcal:// → https://, HTTP GET the ICS URL, pass response text to parseICS() |
sync(subscriptionId?) |
Sync one subscription (by id) or all; skip events with user_modified = 1; upsert via external_calendar_id = UID; update last_sync |
getAll(userId) |
Return all subscriptions visible to userId (own + shared) |
create(userId, { name, url, color, shared }) |
Insert new subscription, trigger initial sync |
update(userId, id, fields) |
Update name/color/shared; only creator or admin |
remove(userId, id) |
Delete subscription + cascade-delete events; only creator or admin |
New routes: /api/v1/calendar/subscriptions
| Method | Path | Action | Auth |
|---|---|---|---|
GET |
/ |
List visible subscriptions | any user |
POST |
/ |
Create subscription | any user |
PATCH |
/:id |
Update name/color/shared | creator or admin |
DELETE |
/:id |
Delete subscription + events | creator or admin |
POST |
/:id/sync |
Manual sync | creator or admin |
All handlers wrapped in try/catch. Responses follow { data: ... } / { error, code } convention.
Sync integration
server/index.js syncAll() function calls icsSubscription.sync() alongside the existing Google/Apple sync calls.
Setting user_modified
When a calendar event with external_source = 'ics' is updated via PATCH /api/v1/calendar/events/:id, the route sets user_modified = 1 automatically.
3. Frontend
Settings page (public/pages/settings.js)
New card "ICS-Abonnements" in the existing "Kalender" tab, below Apple Calendar:
- List of all visible subscriptions: color dot, name, visibility badge, last sync timestamp
- "Abonnement hinzufügen" button reveals an inline form:
- URL input (required)
- Name input (required)
- Color picker (
<input type="color">) - Toggle "Für alle sichtbar" (default: off)
- Submit / Cancel buttons
- Per-subscription actions: "Jetzt synchronisieren", "Bearbeiten" (inline), "Löschen" (with confirmation)
- Only creator or admin sees edit/delete actions
No new Web Component — rendered inline, consistent with the Apple Calendar form pattern.
Calendar page (public/pages/calendar.js)
- Events with
external_source = 'ics'use their subscription's color for rendering - No special UI indicator for
user_modifiedstatus (keeps UX clean)
i18n
All new strings in public/locales/de.json under:
settings.ics.*— subscription list, form labels, actions, status messagescalendar.ics.*— any calendar-side strings (if needed)
de is the reference locale; other locales fall back gracefully.
4. Error Handling
| Scenario | Behavior |
|---|---|
| ICS URL unreachable | Log warning, keep existing events, leave last_sync unchanged |
| Invalid ICS content | Log warning, skip malformed VEVENTs, continue with valid ones |
| URL returns non-ICS content | Log error, abort sync for this subscription |
| Unauthorized edit/delete | 403 response |
| Duplicate URL | Allowed (user may want same feed with different color/name) |
5. Out of Scope
- CalDAV authentication (Basic/OAuth) — ICS-URL only (public or pre-authenticated URLs)
- Per-event sync conflict resolution UI
- Subscription import/export