M4 Calendar: reminders form, invitations, short publish links, settings UX

Calendar settings (Settings > Calendars):
- Reminders are now {method, minutes}: Add-reminder opens a boxed form with a
  method dropdown (Show an alert / Email me) + time dropdown, bell icons.
  Backend normalizes the legacy list[int] transparently.
- New Invitations section (auto-add emailed events): policy radios, "Add new
  events from" (My contacts / Anyone), "Add to calendar", mark-read+file toggle.
  Persisted to user settings (inbound iMIP processing still deferred).
- Time zone options show GMT codes, e.g. "Central European Summer Time (GMT+2)".
- Default calendar now defaults to "Personal" instead of a generic placeholder.
- Colour picker is a circular-swatch-only dropdown (sized so swatches don't overlap).
- Delete calendar / remove subscription now confirm via a modal.
- Create form: dropped the breadcrumb; heading stays "Create calendar" even after
  a Publish toggle auto-creates the calendar.

Publish links: replaced the 244-char vault-sealed token with a 16-char random
slug (secrets.token_urlsafe). New sdk.public_session() (privileged, non-RLS) lets
the public /public/{token} feed resolve the slug directly with explicit filters.

Settings shell: "<- Mail" is now a context-aware "<- Back" via ?from=; Calendar
opens Settings with from=/p/calendar so Back returns to the calendar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
This commit is contained in:
2026-08-02 08:48:33 +02:00
co-authored by Claude Opus 4.8
parent e2bef3c41d
commit 57a1f1562f
10 changed files with 447 additions and 113 deletions
@@ -1199,8 +1199,9 @@ export default function mount(el, ctx) {
state.menuOpen = false;
if (a.to) ctx.navigate(a.to); else render();
});
act("settings", () => ctx.navigate("/settings"));
act("add-calendar", () => ctx.navigate("/settings/calendars?create=1"));
// Pass ?from=/p/calendar so Settings' back button returns here, not to Mail.
act("settings", () => ctx.navigate("/settings?from=%2Fp%2Fcalendar"));
act("add-calendar", () => ctx.navigate("/settings/calendars?from=%2Fp%2Fcalendar&create=1"));
act("signout", async () => { await api.fetch("/auth/logout", { method: "POST" }); ctx.navigate("/login"); });
act("new", () => openForm(null));
act("today", () => { state.anchor = startOfDay(new Date()); state.mini = startOfMonth(state.anchor); reload(); });
@@ -1373,8 +1374,11 @@ export default function mount(el, ctx) {
HOUR12 = s && s.time_format === "12"; // 24h is the default
state.jitsi = (s && s.jitsi_server) || "";
state.defaultCalendar = (s && s.default_calendar) || "";
state.defaultRemTimed = (s && s.default_reminders_timed) || [];
state.defaultRemAllday = (s && s.default_reminders_allday) || [];
// Reminders are stored as {method, minutes}; event reminders only use minutes.
const remMins = (arr) =>
(arr || []).map((x) => (typeof x === "number" ? x : (x && x.minutes) || 0));
state.defaultRemTimed = remMins(s && s.default_reminders_timed);
state.defaultRemAllday = remMins(s && s.default_reminders_allday);
} catch { /* keep defaults (24h, public Jitsi) */ }
try {
const q = await api.fetch("/settings/quota");