Commit Graph
486 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 4.8 f76d2783d9 feat(web): show shared calendars under the owner's name
A calendar shared with another person now appears to the recipient under the
OWNER's name (Guido's "Persönlich" shows as "Guido"), matching the iOS filter
sheet. The original calendar name moves to the sub-label / settings source
column so it stays identifiable. Sidebar list + settings table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:41:31 +02:00
ScarriffleandClaude Opus 4.8 3f5de5cdfa fix(security): store auth token in Keychain and restrict ATS to local networking
- Move the bearer token from UserDefaults to the Keychain (accessible after
  first unlock), with a one-time migration so existing logins survive.
- Replace NSAllowsArbitraryLoads=YES with NSAllowsLocalNetworking=YES so ATS
  still permits cleartext to LAN/self-hosted servers but enforces TLS for
  public hosts (no arbitrary cleartext/MITM).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:32:01 +02:00
ScarriffleandClaude Opus 4.8 e539508bec fix(security): scope CalDAV PUT to its calendar, block iCal SSRF, auth avatar endpoint
- dav_router: PUT now looks up the event within the authenticated calendar only
  (local_events.uid is globally unique), so a CalDAV client can no longer
  overwrite another user's/calendar's event; a cross-calendar UID clash returns
  409 instead of a 500 from the UNIQUE constraint.
- ical_router: _fetch_ics validates the URL (http/https only), resolves the host
  and rejects private/loopback/link-local/reserved targets, follows redirects
  manually re-validating each hop, and caps the response size — closing an
  authenticated SSRF into internal services / cloud metadata.
- profile_router: GET /profile/avatar/{user_id} now requires authentication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:17:24 +02:00
ScarriffleandClaude Opus 4.8 784c9013eb feat(web): mark read-only shared calendars and hide them from the event editor
- Sidebar: a calendar shared with read access shows "· Nur lesen" next to the
  "shared with me · <owner>" label.
- Event editor calendar picker: exclude read-only shared calendars (own +
  read_write, incl. group calendars, stay) so a save can't 403.

Parity with the server/iOS/Android sharing changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:02:12 +02:00
ScarriffleandClaude Opus 4.8 9db04361cc feat(sharing): show shared calendars read-only (parity with server/Android)
- Parse the server's new read_only flag on events.
- Filter sheet: a calendar shared with me is listed under the owner's name
  (sharedBy) with a lock icon when I only have read access.
- loadWritableCalendars: exclude read-only shared calendars so they no longer
  appear in the event editor (a save would 403).

The group-title prefix removal is server-side and needs no iOS change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:56:32 +02:00
Guido SchmitandClaude Opus 4.8 993d5d83e1 feat: clearer shared calendars + move group switch into the menu
Three Android UX fixes reported from on-device testing:

1. A calendar shared with the user now shows read-only affordances: parse the
   server's new read_only flag on events, show a lock icon in the filter sheet,
   and exclude read-only shared calendars from the event editor's calendar
   picker (getWritableCalendars) so saves no longer 403. The calendar's display
   name (owner's name) comes from the server.

2. (server-side) group event name prefix removed — no client change needed.

3. Move the personal/group view switcher out of the crowded top bar into the
   burger MenuSheet as a "group view" section (personal + each group with a
   check mark), mirroring iOS. The GroupSwitcher pill and its top-bar wiring are
   gone; the active-group banner stays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:52:57 +02:00
ScarriffleandClaude Opus 4.8 e3844294ae feat(sharing): label shared personal calendars by owner + drop group title prefix
Two group/sharing display fixes, both server-side so every client benefits:

1. A personal calendar shared WITH a user showed only its raw name
   ("Persönlich"), indistinguishable from the user's own. The merge read now
   relabels a shared *personal* calendar under the owner's display name (so
   Guido's "Persönlich" reads as "Guido" for recipients) and adds read_only:true
   when the share isn't read_write. Group calendars are excluded — they keep
   their own name and stay writable for members.

2. The combined group view prefixed every foreign event with the owner's first
   name ("Guido: …"). Each member already has a distinct display_color, so the
   prefix was redundant. _decorate_title now returns the raw title; display_title
   stays non-empty so clients' legacy prefix fallback never triggers. The change
   takes effect on already-installed clients with no app update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:42:26 +02:00
Guido SchmitandClaude Opus 4.8 e9d58d8239 feat(visibility): separate local quick-hide from server-synced banish
The filter-sheet eye was the only hide control on Android and it hard-synced
to the server (sidebar_hidden/enabled) — so a quick, per-device hide leaked to
the web and other devices. Now Android matches iOS' two-tier model:

- Quick-hide (eye toggle) is device-local only (setCalendarHidden → hiddenKeys),
  no server call.
- Banish ("permanently hide", new archive action per row) syncs sidebar_hidden
  to the server and moves the calendar into Settings; a new "Hidden calendars"
  section there re-enables it (server + auto-refetch).
- reconcileCalendarVisibility now drives banishedKeys (not hiddenKeys) from the
  server's sidebar_hidden, so the reconcile never overwrites the local quick-hide.
- Filter sheet lists calendars from the unfiltered cache (minus banished), so a
  locally hidden calendar still appears and can be toggled back on.

Colours and reminders stay server-synced as before. iOS already had this model
and is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 11:09:45 +02:00
Guido SchmitandClaude Sonnet 4.6 ca4c875659 fix(visibility): auto-reconcile server sidebar_hidden on launch/resume/sync
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 synced the filter toggle TO the server (setCalendarHiddenSynced) but
never pulled the server's sidebar_hidden flags back, so a calendar hidden or
re-enabled on the web (or another device) was never reflected until a manual
sync or relaunch — the "hide it on the web and the app won't bring it back" bug.

Add reconcileCalendarVisibility(): pulls the account lists and updates the
local hidden set from the server's sidebar_hidden flags for external calendars
(local/ical keys untouched). It runs before the initial load, on manual sync,
and on a new ON_RESUME lifecycle hook — forcing a refetch when the set changed
so a re-enabled calendar's events reappear on their own. Mirrors the iOS fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 22:05:08 +02:00
ScarriffleandClaude Sonnet 4.6 325f357357 fix(visibility): auto-reconcile server sidebar_hidden on launch/resume/sync
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>
2026-07-03 22:04:58 +02:00
Guido SchmitandClaude Sonnet 4.6 80bdb91acf fix(cache): preserve events from calendars/sources that failed to sync
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>
2026-07-03 21:35:39 +02:00
ScarriffleandClaude Sonnet 4.6 444772c959 fix: attribute account-level token failures to each calendar
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>
2026-07-03 21:31:26 +02:00
ScarriffleandClaude Sonnet 4.6 61237134f0 fix(cache): preserve whole source on account-level sync errors (HA/Google tokens)
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>
2026-07-03 21:31:05 +02:00
ScarriffleandClaude Sonnet 4.6 fb22aeb090 fix(cache): retry failed-calendar ranges instead of marking them as cached
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>
2026-07-03 20:55:57 +02:00
ScarriffleandClaude Sonnet 4.6 1cbab3f3ec fix: preserve cached events when a calendar has a per-source sync error
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>
2026-07-03 20:06:11 +02:00
Guido Schmit 382cb717a4 fix: match sync errors to calendars by id, not name suffix
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.
2026-07-02 19:00:49 +02:00
Scarriffle e0ea16f2ef fix: include calendar_id in per-calendar sync errors
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.
2026-07-02 18:36:46 +02:00
Guido Schmit a3568fb6f3 fix: actually pass force through from loadVisible to loadRange
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.
2026-07-02 18:34:08 +02:00
Guido Schmit 9dd41420b6 fix: safe performance and crash-safety fixes
- 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.
2026-07-02 18:30:21 +02:00
Guido Schmit 3137abcef2 fix: surface per-calendar sync errors from server
/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>").
2026-07-02 18:27:00 +02:00
Scarriffle 8ed474663e feat: surface per-calendar sync errors from /api/caldav/events
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.
2026-07-02 18:25:47 +02:00
Scarriffle 9fb350eb29 fix: surface all calendar sync failures, not just Google
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.
2026-07-02 18:22:11 +02:00
ScarriffleandClaude Opus 4.8 94655ce7c5 fix(caldav): match Basic Auth username case-insensitively
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>
2026-07-01 14:38:21 +02:00
ScarriffleandClaude Opus 4.8 9306d638ad fix(caldav): move app-password section to Settings → Profile where users look
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>
2026-07-01 14:17:25 +02:00
ScarriffleandClaude Opus 4.8 f662163185 feat(caldav): app-specific passwords so MFA accounts can use CalDAV
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>
2026-07-01 13:15:35 +02:00
ScarriffleandClaude Opus 4.8 fb32f0424f feat(caldav): username/password (Basic Auth) access with discovery + https URLs
- 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>
2026-07-01 12:44:16 +02:00
ScarriffleandClaude Opus 4.8 34e701fa78 feat(pwa): auto-update service worker so new releases take effect without manual cache clearing
- 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>
2026-07-01 11:24:05 +02:00
ScarriffleandClaude Opus 4.8 661a8ea579 chore: bump APP_VERSION to v65 (cache-bust / deploy verification)
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>
2026-07-01 11:12:21 +02:00
ScarriffleandClaude Opus 4.8 47944ed952 feat: publish local calendars via two-way CalDAV (opt-in, secret token URL)
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>
2026-07-01 10:19:01 +02:00
ScarriffleandClaude Opus 4.8 a591eb51d3 chore: bump APP_VERSION to v63 to ship mini-cal + week-view fixes
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>
2026-07-01 10:15:41 +02:00
ScarriffleandClaude Opus 4.8 f791160d1a fix: mini-cal marks full multi-day span; short week events show title inline
- 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>
2026-07-01 09:42:02 +02:00
ScarriffleandClaude Opus 4.8 906241a743 fix: agenda view scrolls to current date on render
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>
2026-06-18 17:13:18 +02:00
ScarriffleandClaude Sonnet 4.6 4e9ae83299 feat(widget): calendar filter config + fix group-view data leak
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>
2026-06-17 18:41:43 +02:00
ScarriffleandClaude Opus 4.8 2e76e63ba2 fix: event popup — allow toolbar button clicks and text selection while draggable
- 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>
2026-06-17 12:15:38 +02:00
ScarriffleandClaude Sonnet 4.6 6e8efb2ed5 feat(sidebar): dedicated share-icon set for group-visible indicator
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>
2026-06-15 22:02:54 +02:00
ScarriffleandClaude Sonnet 4.6 d6159f0639 feat: calendar rename for all sources, group icon in sidebar, configurable share icon
- 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>
2026-06-15 21:56:24 +02:00
Guido SchmitandClaude Sonnet 4.6 d2d5e2b19c feat(filter): sync calendar visibility to server for CalDAV/Google/HA
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>
2026-06-15 21:48:36 +02:00
ScarriffleandClaude Sonnet 4.6 be3d4057b3 feat(sidebar): distinct share icon for groupVisible, hide scrollbars
- 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>
2026-06-15 21:36:36 +02:00
ScarriffleandClaude Sonnet 4.6 eb9440c2b2 feat(sidebar): move group icon before calendar name
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 21:30:31 +02:00
ScarriffleandClaude Sonnet 4.6 34777c78c9 fix(sidebar): min-height approach for action buttons, no overlap or jump
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>
2026-06-15 21:24:57 +02:00
ScarriffleandClaude Sonnet 4.6 0427e1189d fix(sidebar): absolute-position action buttons to prevent name truncation
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>
2026-06-15 21:18:04 +02:00
ScarriffleandClaude Sonnet 4.6 c1fdfe72d0 fix: modal scrollbar clips rounded corners + sidebar hover jump (v56)
- 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>
2026-06-15 20:58:44 +02:00
ScarriffleandClaude Sonnet 4.6 6365f99ac0 feat: persist active settings tab in URL hash (v55)
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>
2026-06-15 20:52:14 +02:00
ScarriffleandClaude Sonnet 4.6 d04e2952cb fix: replace reminders checkbox with bell icon in calendar table (v54)
Bell-on = Benachrichtigungen aktiv, Bell-off (durchgestrichen) = stumm.
Gleiche Logik wie Auge-Icon für Sichtbarkeit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:48:12 +02:00
ScarriffleandClaude Sonnet 4.6 c5fd5b4e55 fix: calendar table visibility column uses eye icon instead of checkbox (v53)
Replace misleading "Ausgeblendet" checkbox with eye-open/eye-off icon button.
Sichtbar column: eye-open = shown, eye-off = hidden (dimmed). Clicking toggles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:45:33 +02:00
ScarriffleandClaude Sonnet 4.6 d1c5b66230 feat: web settings restructure + popup drag + calendar management table (v52)
- Remove "Ansicht" tab; move Kalenderansicht to top of Darstellung
- Reorder Darstellung: Kalenderansicht → Sprache → Stundenhöhe → Farben
- Move Standard-Termindauer to Profil tab as contrast-button selector
- Replace per-source account sections in Kalender tab with unified
  calendar management table (Name/Herkunft/Ausgeblendet/Benachrichtigungen/
  Export·Import/Sync/Delete) covering all calendar sources
- Add "Konto hinzufügen" buttons (Lokal/CalDAV/iCal/HA/Google) to Kalender tab
- Popup drag-to-move via pointer capture on .popup-header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:36:09 +02:00
Guido SchmitandClaude Sonnet 4.6 740367b0b9 feat: show relative time in notifications + move default duration to profile settings
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>
2026-06-15 19:40:46 +02:00
ScarriffleandClaude Sonnet 4.6 52040fff53 feat: show relative time in notifications + move default duration to profile settings
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>
2026-06-15 19:38:20 +02:00
ScarriffleandClaude Sonnet 4.6 2c59d873f3 fix: UTF-8 encoding for iCal subscriptions + default_event_duration_minutes setting
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>
2026-06-15 19:34:43 +02:00
ScarriffleandClaude Opus 4.8 cc3d16ddce feat: custom reminder picker, muted-calendar hint, synced default duration
- 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>
2026-06-15 10:03:24 +02:00