Hiding or showing a calendar on the web sets enabled=false + sidebar_hidden
server-side, so the server stops returning that calendar's events entirely.
The app only reconciled these per-calendar flags when the filter sheet or the
accounts screen happened to be opened — never on launch, resume or the
periodic pull. So a calendar re-enabled on the web stayed invisible (still
banished locally, and its events weren't in the cache) until a manual sync or
relaunch — the "hide it on the web and the app won't bring it back" bug.
Add CalendarStore.reconcileCalendarVisibility(api:) which pulls the account
lists, updates the banished set from the server's sidebar_hidden flags, and
returns whether anything changed. CalendarHostView now runs it (via
syncFromServer) on launch, on scenePhase .active, in the periodic loop, and on
manual sync — forcing a refetch when the set changed so a re-enabled calendar's
events actually reappear without user intervention.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mergeIntoCache() unconditionally evicted all cached events in the fetched
range, so a transient sync failure (expired CalDAV credentials, an HA/Google
token-refresh failure) wiped the affected calendar until the next clean fetch.
Home Assistant was the most visible victim: a token failure aborts the whole
account fetch and the error arrives without a calendar_id.
Now sync errors are split into per-calendar keys (error carries a calendar_id)
and whole sources (account-level error without one). mergeIntoCache() keeps
matching cached events, and only extends the cached range on a fully clean
fetch so failed calendars are retried on the next load. Mirrors the iOS fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a Home Assistant or Google token refresh fails, the whole account fetch
aborts. Previously this raised, so the outer handler in caldav_router emitted
a single sync error WITH NO calendar_id. Clients that preserve cached events
per calendar (iOS) couldn't attribute it and wiped the affected calendars.
Now both get_ha_events and get_google_events catch the token failure and emit
one error per enabled calendar, each carrying its calendar_id — so every
client can pin the failure to a specific calendar and keep its cached data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The per-calendar preservation added in the previous fix only kicks in when a
sync error carries a calendar_id. But account-level failures — most commonly
a Home Assistant or Google token-refresh failure — abort the entire account
fetch and arrive WITHOUT a calendar_id. Those events were still being evicted,
so HA/Google calendars kept vanishing on a transient token hiccup.
failedCalendarKeys() now returns both per-calendar keys and whole sources:
an error without a calendar_id protects every cached calendar of that source.
mergeIntoCache() keeps events whose source is in keepSourcesInRange, and the
cached range is only extended when neither set is populated (auto-retry).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mergeIntoCache() previously extended cachedStart/End even when sync errors
occurred — so isCached() returned true on the next call and the failed
calendar was silently left empty (especially bad on first launch or after
forceReload when there's nothing to preserve).
Now the cached range is only extended on a fully clean fetch. When
keepKeysInRange is non-empty (sync errors present), we leave cachedStart/End
unchanged, forcing isCached() to return false and triggering an automatic
retry on the next loadEvents call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: mergeIntoCache() unconditionally evicted every event in the
fetched date range, even for calendars whose sync failed. Those calendars'
events were removed and never restored, so the calendar appeared to vanish.
Fix:
- SyncError now decodes calendarId from the server's "calendar_id" field
(added server-side in e0ea16f but not yet read by iOS).
- mergeIntoCache() gains a keepKeysInRange parameter: events from failed
calendars are retained (not evicted) even within the fetch window.
- loadEvents() and prefetchBackground() compute the failed keys from the
syncErrors list and pass them to mergeIntoCache.
Result: when CalDAV / Google / HA sync fails for a specific calendar, the
user sees stale-but-correct events alongside the existing error banner,
instead of a completely empty calendar with no explanation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The server now includes calendar_id in per-calendar sync-error entries.
Match CalendarFilterSheet's warning icon on (source, calendarId) like
calendarKey() already does everywhere else, instead of a fragile
name-suffix comparison that could false-positive across accounts whose
calendar names happen to overlap.
Events already carry calendar_id, but the corresponding sync-error
entries didn't, forcing clients to match errors to calendars by
fragile name-suffix comparison. Account-level failures (whole account
unreachable) still omit calendar_id since no single calendar is at fault.
loadVisible(force = true) (used by the error-banner retry button) was
still funneling into loadRange with an implicit force = false, so the
isCached short-circuit was never actually bypassed on retry.
- TimeGridView: remember Instant.now() (keyed on the day) and the day-header
DateTimeFormatter instead of rebuilding both on every recomposition/frame —
fixes flicker in the past-event dimming and avoids repeated allocation.
- CalendarViewModel.refreshFromCache: short-circuit the non-group filter pass
when both hiddenKeys and banishedKeys are empty, mirroring the existing
group-mode short-circuit.
- CalendarViewModel.setCalendarHiddenSynced: wrap the server PATCH in
runCatching and surface failures via state.error instead of silently
dropping them (the local toggle would otherwise diverge from the server
unnoticed).
- CalendarViewModel.afterMutation: refetch only the already-cached range
(force-bypassing loadRange's cache check) instead of invalidateCache() +
initialLoad(), which reloaded the entire ±cacheMonths window and froze the
UI on every single event save.
- CalendarrRoot: replace calendarVm!! with a null-safe ?.let, falling back to
the splash screen instead of crashing if the VM isn't ready yet.
- GroupsScreen: replace existing!!.icon!! / g.icon!! double force-unwraps
with safe-call chains (existing?.icon?.takeIf { GroupIcons.isKey(it) }).
Deliberately out of scope: R8/minify settings, MonthView's cache window
size (MONTHS_BACK/MONTHS_AHEAD), and mergeIntoCache's merge logic (already
reviewed as correct). No debounce added to setCalendarHiddenSynced — would
need new machinery beyond this fix's scope.
/api/caldav/events now returns an "errors" array alongside "events" for
calendars that failed to sync (e.g. expired credentials) while still
reporting success overall. Previously such failures were silently
swallowed, so a calendar could appear enabled/visible while showing zero
events with no explanation.
- CalendarRepository.fetchEvents now returns EventsResult(events, errors),
parsing the new "errors" array the same way importIcs's Triple return
handles multi-value results.
- CalendarUiState gains syncErrors, populated in loadRange's onSuccess.
- CalendarScreen shows a second error-style banner for sync errors,
additive to the existing network-failure banner.
- CalendarFilterSheet shows a warning icon next to calendar rows whose
source+name matches a sync error (best-effort suffix match, since the
server's error name is "<account> – <calendar name>").
The server now reports per-calendar sync failures (e.g. expired
credentials) alongside an otherwise-successful events response, via an
"errors" array. Previously such failures were silently swallowed, so a
calendar could appear enabled while showing zero events with no
explanation.
- Decode the new "errors" field into SyncError (source/name/message),
following the existing JSONSerialization + from(json:) convention.
- CalendarStore.syncErrors captures these on successful personal
fetches (loadEvents, prefetchBackground); left untouched on a hard
fetch failure so it isn't conflated with lastError. Group overlay
fetches never populate it (the endpoint doesn't apply there).
- CalendarHostView shows a second red warning banner alongside the
existing lastError banner, reusing the same visual style.
CalDAV and Home Assistant sync failures were previously only logged
server-side, leaving clients unable to distinguish an empty calendar
from a broken sync. Unify error reporting across CalDAV, Home
Assistant, and Google into a single errors list on GET
/api/caldav/events, shaped as {source, name, message}. Messages are
fixed generic strings, never raw exception text, to avoid leaking
URLs or credential fragments. get_ha_events and get_google_events now
return (events, errors) tuples so per-calendar failures propagate to
the caller in addition to account-level failures. Frontend toast now
picks its label from err.source instead of assuming Google/err.email.
Login names are stored lowercase and the web login already compares with
func.lower(); CalDAV Basic Auth used an exact match, so "Scarriffle" failed to
authenticate while "scarriffle" worked. Compare case-insensitively too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app-password UI was in the user-menu profile modal, but users manage CalDAV
in Settings, so it went unnoticed. Move the section into the Settings → Profile
panel (next to account/privacy) and drive it from openSettingsModal via a
top-level initAppPasswords().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CalDAV clients send only user+password over Basic Auth and can't provide a TOTP
code, so account passwords would bypass 2FA. Add revocable app passwords:
- models: AppPassword table (bcrypt hash, label, last_used); auto-created via
create_all
- profile_router: GET/POST/DELETE /profile/app-passwords (plaintext shown once)
- dav_router: Basic Auth accepts any app password; the account password is
accepted only when 2FA is disabled
- frontend: "App-Passwörter (CalDAV)" section in the profile modal (create/show-
once/copy/revoke) + i18n (de/en); login hint now says app password
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- dav_router: add Basic-Auth principal-discovery tree at /caldav/ (and
/.well-known/caldav) so clients can add a CalDAV account with server URL +
username + password; lists all published calendars. Token URL /dav/{token}/
still works without login. Handlers generalised over a base href.
- dav_util: derive the public origin from X-Forwarded-Proto/-Host (or
PUBLIC_BASE_URL) so published URLs are https, not internal http:8080.
- local_router: expose caldav_login_url alongside caldav_url.
- frontend/i18n: show both the no-login token URL and the login URL + hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- app.js: reload once on SW controllerchange (guarded against loops / first
install) and poll reg.update() hourly for long-open tabs
- sw.js: bump cache to v24 so the new (network-first) worker replaces any stale
cache-first worker and cleans old caches on activate
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fresh commit so a git pull visibly advances the deployed version and the PWA
service worker refreshes its cached assets. No functional change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The multi-day mini-calendar and short-event week-view fixes were already
committed but not visible because the PWA served cached assets. Bump the
version so clients pick up the new JS/CSS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Mini calendar: mark every day an event spans (not just the start day) so
multi-day events (Urlaub/Ferien) show a dot across the whole range
- Week view: render short events on one line (time next to title) so the title
stays visible; drop the location line when there's no room
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agenda lists the whole cached range, so clicking Today only reset scroll
to the top (earliest past event). Scroll the agenda to the current date (or the
next day with events) after rendering, so Today/mini-cal clicks land correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bug fix: publishWidgetSnapshot() now guards against activeGroup != nil,
so group view events/colors never contaminate the widget cache.
Feature: widgets can now be configured via long-press → Edit Widget.
- WidgetCalendar struct + writeCalendars/readCalendars in WidgetData.swift
- calendarKey added to WidgetEvent (backward-compatible decoder)
- CalendarIntent.swift: CalendarAppEntity + CalendarEntityQuery + CalendarSelectionIntent
- CalendarrTimelineProvider migrated from TimelineProvider to AppIntentTimelineProvider
- All 13 StaticConfiguration widgets changed to AppIntentConfiguration
- publishWidgetSnapshot() builds + writes the calendar list for the intent
An empty selection (default) shows all calendars; selecting specific
calendars filters the widget events accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Don't start a header drag when the pointer goes down on a button/link, so
edit/copy/delete/close work again
- Make the popup body text selectable (user-select: text) for copying
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace group-category icons with 8 sharing-specific icons in the
Darstellung picker: share (3 nodes), link, send, eye, upload, wifi,
person_add, ios_share. Uses new shareIconSvg() + SHARE_ICON_KEYS/PATHS
constants, separate from group icons. Default changes from 'people' to 'share'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename: Add google and homeassistant branches to the dblclick-rename
save() handler; backend already accepted name updates for both sources
- Group icon: Pass groupIconForLocalCal(cal.id) into sidebar entries so
the group's custom icon (home/heart/work/…) shows instead of always
defaulting to the people icon
- Share icon: New share_calendar_icon field in UserSettings (+ migration)
replaces the hardcoded SHARE_ICON SVG; users pick from GROUP_ICON_KEYS
in Settings → Darstellung via a new icon-picker row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The filter-sheet Switch now calls setCalendarHiddenSynced(), which
updates local hiddenKeys immediately AND fires a sidebar_hidden PATCH
to the server for caldav/google/homeassistant calendars. This makes
the quick-hide consistent with the web sidebar across all devices.
CalendarFilterEntry gains a source field so the sheet knows which
backend to target.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- groupVisible calendars get a share icon instead of the people icon;
people icon is reserved for actual group calendars
- sidebar scrollbar hidden (scrollbar-width: none + webkit)
- modal-body scrollbar hidden — fixes Impressum scroll indicator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of absolute positioning (which caused icons to overlay text),
use min-height:40px on .cal-item so the row is always tall enough for
28px mini-btns. Buttons stay in the flex flow with display:none when
hidden — no space reserved, no text truncation, no height shift on hover.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Buttons (.cal-item-remove, .cal-item-bell) are now position:absolute so
they never participate in flex layout — names get full available width at
all times. Buttons fade in over the right edge of the item on hover with
no layout shift and no row-height change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- modal-card: overflow:hidden + flex column; modal-body scrolls instead,
so native scrollbar never overlaps the 20px border-radius corners
- cal-item-remove / cal-item-bell: display:none→flex replaced with
opacity:0/pointer-events:none→1/auto, eliminating reflow on hover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
URL gains &stab=<panel> when settings are open (e.g. #...&settings=1&stab=accounts).
Tab switches update the URL; reload reopens settings on the same tab.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notifications now show "in 30 Min. · Ort" instead of just the location,
giving users immediate context about how soon the event starts.
Default event duration picker moved from the View section to right below
the profile/privacy group, consistent with the iOS app arrangement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notifications now show "in 30 Min. · 10:00" instead of just the time,
giving users immediate context about how soon the event is.
Default event duration picker moved from the View section to right below
the privacy setting, grouped under the calendar/profile settings area.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Force resp.encoding='utf-8' in _fetch_ics so umlauts (ö/ä/ü) from sources
like Ferienwiki.de are decoded correctly instead of showing as ö/ä/ü.
Add default_event_duration_minutes to UserSettings (nullable int, same pattern
as default_reminder_minutes) with DB migration, GET/PUT in settings_router.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reminder editor: presets + custom number+unit (minutes/hours/days/weeks)
- Grey out + footer hint when the selected calendar's reminders are muted;
reminders are kept, scheduler already skips them
- New synced setting defaultEventDurationMinutes for new events
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add reminder editor (presets + custom number+unit) wired through save
- Per-calendar reminder mute: filter-sheet toggle + greyed-out editor hint;
reminders are kept, never deleted
- New AlarmManager-based NotificationScheduler + channel + POST_NOTIFICATIONS;
skips muted calendars
- New synced setting default_event_duration_minutes for new events
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Render event descriptions as sanitized HTML (links/line breaks) instead of
raw escaped text; no script execution
- Reminder picker: presets + custom number+unit (minutes/hours/days/weeks)
- Grey out + hint the reminder editor when a calendar's notifications are off
(reminders are kept, just not fired)
- New synced setting default_event_duration_minutes (default 60) for new events
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Transparent background caused Pixel launcher to inject its own tinted
ring. Replace with #16703D (sampled from iOS icon background) so the
launcher shape-clips cleanly without any visible halo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without mipmap-anydpi-v26 XML, Pixel launcher wraps legacy PNGs in a
white ring template. Adding adaptive-icon XML with the iOS-matching
foreground PNG at correct per-density sizes (108dp canvas) eliminates
the nested-ring appearance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use Modifier.size(20.dp) instead of height(20.dp) so the spinner is
constrained on both axes and doesn't get cut off horizontally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Material 3 menus use shapes.extraSmall (4dp default) which looked boxy;
bump to 14dp so all dropdown popups (view switcher, group switcher, ...)
get rounded corners matching the iOS look.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Long-press on a day now opens a preview popup (ported from the iOS
DayContextPreviewView): all-day events as colored bars with tapered
ends when they continue beyond the day, timed events as dot+time+title
rows, plus quick actions (new event, week view, day view)
- Month scroll performance: stable item keys + contentType in the
LazyColumn, remembered Instant.now()/theme colors, remembered callbacks
so visible week rows stay skippable during recomposition
- Fix "+N" and "KW" labels overlapping the lowest event lane (row bottom
padding 6dp -> 16dp, mirrors the iOS fix)
- Event detail: delete is now a quiet red text button instead of a
full-width filled container
- Top bar: hairline divider so the bar/calendar boundary is visible
- FAB: explicit circular shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- DayPreviewAllDayBar: use custom ChevronBarShape (polygon clip) instead of
chevron SF Symbols; bars have tapered pointed ends for continuation
- MonthView rowHeight: increase bottom padding from +4 to +16 so the
overflow "+N" count no longer overlaps the last event bar lane
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>