Commit Graph

270 Commits

Author SHA1 Message Date
Scarriffle
19258096e2 Tidy settings layout (web)
- constrain settings panels to a 680px reading column so fields no longer
  stretch edge-to-edge on wide screens (main "looks bad" cause)
- fix the app-password "Erstellen" button rendering centred below the input:
  .app-pw-create inherited flex-direction:column from .form-group; force row so
  it sits inline to the right of the field

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 04:31:12 +02:00
Scarriffle
6baa07379f Rework birthdays to a single calendar + UI fixes (web)
- one dedicated birthday calendar per user; enable it in Settings > Calendars
  (or from the New-birthday dialog) — it then shows in the sidebar like any
  calendar (colour/visibility)
- New-birthday dialog: no target picker; day/month selects + a year field that
  hides when "year unknown"; fixed the year-unknown checkbox layout
- create split-button is now one seamless pill; the caret opens the menu
- Settings > Calendars gains a Birthdays section (enable + notify-days-before)
- removed the birthday toggle from the generic new-calendar modal
- profile settings: right-align the section save button for consistency
- bump service-worker cache to v25 so assets refresh

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 04:20:49 +02:00
Scarriffle
eb0684b99c Add birthday feature (web)
- views render display_title (age) and a cake icon for is_birthday events
  (month/week/agenda + quarter tooltip) via shared eventTitle/birthdayIconSvg
- create split-button caret opens a menu: new event / new birthday
- new birthday modal: name + date + "year unknown" + target birthday calendar,
  saved as an all-day FREQ=YEARLY local event with birth_year
- local-calendar modal gains a "birthday calendar" toggle + "notify N days before"
- i18n de/en strings

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:10:52 +02:00
Scarriffle
ca09538971 Add birthday calendar backend support
Birthday calendars are ordinary local calendars flagged is_birthday, so they
flow to all clients via the merge read and inherit sharing/colors/reminders.

- models: LocalCalendar.is_birthday + birthday_notify_days_before;
  LocalEvent.external_uid (Contacts dedup) + birth_year
- build_local_event_dict: server-computed display_title "Name (age)" per
  occurrence, is_birthday flag for the client cake icon, and a reminder injected
  from birthday_notify_days_before so mobile schedulers fire it
- groups combined view keeps the birthday display_title instead of overwriting it
- local_router: calendar flags + event fields on create/update, plus
  GET /calendars/{id}/birthdays for importer reconcile by external_uid
- additive SQLite migrations

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:38:37 +02:00
Scarriffle
d426f8985c style(web): dark-mode the share modal's add-user search + permission select
The user-search input and permission dropdown in the share modal were raw white
browser controls. Style them like the rest of the app (dark bg, border, radius,
focus ring) and give the user checkboxes the primary accent colour. v80.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:25:03 +02:00
Scarriffle
89a1355149 fix(web): inverted condition broke hiding shared calendars
The per-device hide branch was gated on owned!==false (my own calendars) instead
of owned===false (shared with me), so unchecking a shared calendar hit the
owner-only enabled PUT and 404'd. Swap to owned===false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:32:34 +02:00
Scarriffle
f844ded57d fix(web): hide edit/delete for others' events; persistent hide for shared calendars
Bug 1 — a calendar shared with me stayed visible after unchecking it: the hide
was a one-shot cache filter the server undid on refetch. Add a per-device
hidden set (localStorage 'hiddenLocalCalendars'), honoured in filterEvents
(normal view) and used to drive the checkbox state, so it survives refetch/reload.

Bug 2 — in the group combined view, other members' events showed edit/delete and
403'd on save. The combined endpoint now emits read_only (editable = the group
calendar OR my own events), via a read_only param threaded through
build_local_event_dict/expand_recurring_local. The event popup and edit modal now
treat read_only events as read-only (copy still allowed). Test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:11:46 +02:00
Scarriffle
ec85a5b5f3 fix(web): show dialogs opened from settings on top of the settings page
The settings modal is a full-screen OPAQUE overlay at z-index 500. Dialogs
opened from within it (share, add-account, color picker) share z-index 500 but
sit earlier in the DOM, so they rendered BEHIND the opaque settings page — the
Share button appeared to "do nothing". Drop the settings page to z-index 400 so
real modals stack above it. v79.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 22:31:03 +02:00
Scarriffle
af64e191ec fix(web): reset checkbox size inside .form-group so the label isn't squeezed
The global ".form-group input" rule (width:100% + padding + border) also hit the
directory-hidden checkbox, blowing it up to a full-width field that pushed the
label text into a narrow wrapping column. Reset the checkbox to a small native
box (16px, no padding/border/background) so the label reads normally beside it.
v78.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 22:23:28 +02:00
Scarriffle
52bc7066db fix(web): settings modal — table crash, tab restore, checkbox layout
Three separate settings bugs (all pre-existing, exposed once the modal actually
opened):

1. renderCalendarTable() referenced a bare `owned` variable (undefined in that
   scope; should be cal.owned) → ReferenceError on the first owned local calendar
   → renderAllAccounts threw → the calendar table never rendered. This was the
   ROOT cause of "settings won't open" (it threw out of openSettingsModal); the
   earlier try/catch only masked it. Fixed to cal.owned.

2. On reload, writeUrlState() (via fetchAndRender) ran before openSettingsModal
   activated the saved tab and wrote the HTML-default (Profile) tab into the URL,
   so every reload landed on Profile. Now the stab is only rewritten once the
   modal is actually shown; otherwise the saved one is preserved.

3. The directory-hidden checkbox label inherited the global ".form-group label"
   uppercase/spaced/12px styling, stretching the text into a broken column. A
   higher-specificity .checkbox-row rule restores normal inline layout.

v77.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 22:16:10 +02:00
Scarriffle
7d5530e51c fix(web): open settings modal before populating it
openSettingsModal() populated every field and ran render helpers
(renderGroupVisibleList, renderAllAccounts, initAppPasswords) BEFORE the final
openModal() call, so an error in any of them left the modal unopened — the
"settings button does nothing" report. Now the modal opens first and the
populate step runs in try/catch (errors logged, not fatal). v76.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:51:56 +02:00
Scarriffle
90bc4b1531 fix(web): bind UI handlers before first fetch so a data error can't kill the UI
initCalendar() awaited fetchAndRender() BEFORE binding the topbar/settings/menu
handlers, so any error from /caldav/events (e.g. a transient 500) threw out of
init and left the buttons dead — the reported "settings won't open". Same root
cause as the reload-logout bug: app wiring must not depend on event data loading.

Bind all handlers first, then fetch inside try/catch (errors are logged + shown
as a toast, no longer fatal). v75.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:44:32 +02:00
Scarriffle
1eecf834a3 fix(sharing): per-user calendar colour works for every sharing path
The recipient colour was stored on calendar_shares, so it only worked for
DIRECT shares. A calendar made visible through a group (a co-member's
group_visible_calendar_id) has no CalendarShare row, so the colour endpoint
returned 403 and nothing was saved — the reported "colour picker opens but the
colour stays the same" for a group-shared calendar.

Replace the share-scoped colour with a general per-user override table
(calendar_color_prefs, keyed by user+calendar). PUT /calendars/{id}/color now
accepts any calendar the user can read (readable_local_calendar_ids covers
direct shares, group calendars AND group-visible), and the merge read + calendar
list apply the override for all of them. Owners still set the shared colour.
The new table is created by create_all; the old calendar_shares.color is unused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:38:16 +02:00
Scarriffle
b8a578cf53 refactor(web): use server per-user colour for shared calendars
Replace the per-device localStorage colour override with the server-backed
per-user colour: the colour dot on a shared calendar now PUTs /calendars/{id}/color
(stores the recipient's own colour server-side, synced across devices) and the
list/events already carry share.color from the server, so the localStorage
re-apply layer is removed. Owners and recipients share one code path. v74.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:22:33 +02:00
Scarriffle
572172c424 fix(web): stay logged in when app initialisation fails after reload
boot() ran token validation and launchApp() inside the SAME try/catch, so ANY
error during app init (a failed calendar/event fetch, a render error) cleared
the token and bounced a validly-authenticated user to the login screen — the
"logged out on every F5" bug. Now /auth/me validates the token alone; launchApp()
runs outside that catch, so a data/render error can no longer log the user out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:22:33 +02:00
Scarriffle
343f7a5e7b feat(sharing): recipients can recolour a shared calendar (per-user, no rename)
A share recipient couldn't change anything on a shared calendar (update_calendar
is owner-only → 404), yet the clients still showed a colour picker for it.

Add a per-recipient colour: new nullable calendar_shares.color column (+ migration).
New PUT /calendars/{id}/color endpoint sets the calendar colour for the owner
(global) or, for a recipient, only their own share colour — never the name, so
recipients can recolour but not rename. The merge read and the calendar list now
prefer the recipient's share colour over the owner's (NULL = owner's colour).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:43:10 +02:00
Scarriffle
cad48efcc6 feat(web): recolour calendars shared with me (per-device local override)
Shared calendars belong to someone else, so a server colour PUT would 403.
Instead store the recipient's chosen colour locally (localStorage 'sharedCalColors',
keyed by calendar id) and re-apply it to events on every fetch. The colour dot
on a shared calendar now opens the picker instead of showing an error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:49:07 +02:00
Scarriffle
2ae8499247 fix(web): can't edit calendars shared with me; struck-pencil read-only marker
- Sidebar: don't start an inline rename on a calendar I don't own (owned=false)
  — the save would only 403. Colour picker was already gated.
- Sidebar: show a struck-through pencil icon on read-only shared calendars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:38:01 +02:00
Scarriffle
79fdf4f54a feat(sharing): group-shared calendars in every member's sidebar, person share picker, hidden profiles
- Backend: co-member group_visible calendars now surface in /local/calendars
  (owned=false, shared_by=owner, read-only, group_shared) and in the normal
  /caldav/events merge (via readable_local_calendar_ids), deduped against
  direct shares / group calendars so nothing appears twice.
- Backend: new User.directory_hidden — a user can hide from sharing/group
  pickers (/users/directory), while admin user management (/users/) still lists
  them. Migration + profile GET/PUT.
- Backend: /groups/{id} members carry shares_calendar so clients can drop
  phantom rows for members who share nothing.
- Frontend: reachable "Teilen" button on owned local calendars; share modal is
  now a checkbox multi-select of users (checked = shared). Hidden-profile toggle
  in Settings → Profile. Group member filter only lists members who actually
  share (phantom fix). Calendars shared with me moved to a dedicated read-only
  "shared with me" section in the manage table.
- Tests: group_visible propagation, no-share absence, dedup, directory_hidden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:35:11 +02:00
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
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
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
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
Scarriffle
c515e9d7e1 feat: safe HTML descriptions, custom reminder picker, synced default event duration
- 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>
2026-06-15 10:00:52 +02:00