dd16078f7a
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>
151 lines
5.7 KiB
Markdown
151 lines
5.7 KiB
Markdown
# 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_MINUTES` setting
|
|
- 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`
|
|
|
|
```sql
|
|
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 subscription
|
|
- `user_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:
|
|
|
|
```sql
|
|
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_modified` status (keeps UX clean)
|
|
|
|
### i18n
|
|
|
|
All new strings in `public/locales/de.json` under:
|
|
- `settings.ics.*` — subscription list, form labels, actions, status messages
|
|
- `calendar.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
|