Final Slice 2 sub-slice: users on iCloud / Fastmail / Nextcloud /
Radicale / Baikal / SOGo can now create a brand-new calendar from the
Paliad UI with one click; users on Google CalDAV (and any future
no-MKCALENDAR provider) get a clean degrade UX that nudges them to
create the calendar in their provider's app and paste the URL back.
Per m's Q2 pick, the capability lives on user_caldav_config so the
probe runs once per server change, not per modal open.
Schema (mig 108)
- paliad.user_caldav_config.supports_mkcalendar boolean — NULL =
unprobed, TRUE = supported, FALSE = degrade.
- paliad.user_caldav_config.mkcalendar_probed_at timestamptz — used
by the next round of probes after SaveConfig invalidates.
- Idempotent (information_schema column-exists checks) + assertion.
CalDAV client
- ProbeMKCalendar: OPTIONS Allow header first; on absence of
MKCALENDAR, falls back to a synthetic MKCALENDAR against a
random .paliad-probe-XX/ path (with DELETE cleanup) to catch
legacy SOGo / misconfigured Radicale (design §4.2).
- MakeCalendar: issues MKCALENDAR with displayname + VEVENT-only
supported-components; returns ErrCalendarNameTaken on 405 so
the service layer can retry with a disambiguating suffix.
- Sentinel errors ErrCalendarNameTaken, ErrMKCalendarUnsupported.
Service
- CalDAVService.ensureMKCalendarProbed: lazy probe on first
/api/caldav-discover call after credential change; result persisted
via UPDATE on user_caldav_config. DiscoverCalendars response now
carries supports_mkcalendar so the UI can show / hide the create-new
radio.
- CalDAVService.MakeCalendar: re-probes if needed, issues MKCALENDAR
via the client (with 3-try -XX-suffix retry on name collision),
creates the matching binding, kicks off PushBindingNow. Returns
the partial result on push failure so the UI can show "created but
initial sync failed".
- InvalidateDiscoveryCache now also clears supports_mkcalendar so a
re-configured server gets re-probed on next open.
HTTP API
- POST /api/caldav-mkcalendar — {display_name, scope_kind, scope_id?,
include_personal?} → 201 {calendar_path, binding, initial_pushed}.
Errors: 501 supports_mkcalendar=false, 409 name conflict, 5xx
upstream. Partial-success (binding created, push failed) carries
initial_sync_error in the body so the UI can surface both bits.
Frontend
- Add-modal source picker becomes a 3-way radio: "Existierenden
wählen" / "Neuen Kalender erstellen" / "Eigene URL eingeben".
Create radio is visible only when supports_mkcalendar=true;
when false, the bilingual Google-degrade notice is shown
beneath the source picker.
- Submit dispatches to /api/caldav-mkcalendar (create) or
/api/caldav-bindings (existing / custom).
- 6 new i18n keys DE+EN under caldav.bindings.modal.source.*
+ caldav.bindings.error.create_*.
Verification
- mig 108 dry-run against live Supabase: both columns added, nullable,
no constraint surprise.
- go build ./... + go test ./internal/services/ ./internal/handlers/ +
bun run build all clean.
Slice 2 complete (2a + 2b + 2c). Slice 3 (hierarchy scopes:
client/litigation/patent/case) and Slice 4 (drop legacy scalar
caldav_uid/caldav_etag) remain.
68 lines
2.7 KiB
SQL
68 lines
2.7 KiB
SQL
-- t-paliad-212 — Slice 2c of CalDAV multi-calendar.
|
|
--
|
|
-- Adds the MKCALENDAR-capability tri-state to paliad.user_caldav_config:
|
|
-- * supports_mkcalendar = NULL → unprobed (probe runs lazily on
|
|
-- the first /api/caldav-discover or
|
|
-- /api/caldav-mkcalendar call).
|
|
-- * supports_mkcalendar = TRUE → server accepts MKCALENDAR; the
|
|
-- "Create new calendar" affordance
|
|
-- in the picker is visible.
|
|
-- * supports_mkcalendar = FALSE → Google-style degrade; UI hides the
|
|
-- create button and surfaces the
|
|
-- "create it in your provider's UI"
|
|
-- notice with a manual-URL input.
|
|
-- The probed_at timestamp lets us re-probe stale-cached results when
|
|
-- the user changes credentials (SaveConfig invalidates by SetNull in
|
|
-- the Go service layer; the column is here so the next round of
|
|
-- probing has somewhere to land).
|
|
--
|
|
-- Idempotent (column-exists DO block) + assertion at the bottom.
|
|
|
|
SELECT set_config(
|
|
'paliad.audit_reason',
|
|
'mig 108: add user_caldav_config.supports_mkcalendar tri-state for t-paliad-212 Slice 2c capability probe',
|
|
true);
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'paliad'
|
|
AND table_name = 'user_caldav_config'
|
|
AND column_name = 'supports_mkcalendar'
|
|
) THEN
|
|
ALTER TABLE paliad.user_caldav_config
|
|
ADD COLUMN supports_mkcalendar boolean;
|
|
END IF;
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'paliad'
|
|
AND table_name = 'user_caldav_config'
|
|
AND column_name = 'mkcalendar_probed_at'
|
|
) THEN
|
|
ALTER TABLE paliad.user_caldav_config
|
|
ADD COLUMN mkcalendar_probed_at timestamptz;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Assertion — both columns present and nullable.
|
|
DO $$
|
|
DECLARE
|
|
sup_nullable text;
|
|
probed_nullable text;
|
|
BEGIN
|
|
SELECT is_nullable INTO sup_nullable
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'paliad' AND table_name = 'user_caldav_config'
|
|
AND column_name = 'supports_mkcalendar';
|
|
SELECT is_nullable INTO probed_nullable
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'paliad' AND table_name = 'user_caldav_config'
|
|
AND column_name = 'mkcalendar_probed_at';
|
|
IF sup_nullable <> 'YES' OR probed_nullable <> 'YES' THEN
|
|
RAISE EXCEPTION
|
|
'mig 108 assertion failed: expected both columns nullable, got supports=% probed=%',
|
|
sup_nullable, probed_nullable;
|
|
END IF;
|
|
END $$;
|