The settings modal's own left nav and header were on --bg-app; point them at
--bg-sidebar / --bg-topbar so they track surface_color like the main sidebar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- new user_settings.surface_color (nullable, device-local default, not synced):
drives the web sidebar / top bar / surfaces; NULL derives from bg_color as
before. Added to schema, migration, GET/PUT, NULLABLE_OVERRIDES, DEFAULT_SYNC.
- web: surface colour row in the settings table; applyTheme derives the whole
surface family from it
- web: per-row sync icon now has real styling — ON shows the primary-tinted
glyph on a pill, OFF shows a slashed, muted glyph (was previously unstyled,
so synced/not-synced looked identical)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generalise the two-tier SettingsSync into a server-driven flag map: each
syncable key has an account-wide flag (fetched from the server); pull applies
only on-flagged keys, push (read-modify-write) sends only on-flagged keys plus
the flag map. Per-row link icon toggles a setting's sync; a global switch flips
all. Enabling a flag adopts this device's current value as the shared one.
- AppSettings: fix bg_color mapping (was background_color), add cache_months,
month_view_paged, sync_flags
- SettingsView: uniform rows (sync icon | name | value), ContrastSelector and
the button pickers replaced by menu dropdowns, every colour gets a reset to a
canonical default, global "sync all" toggle; account settings (privacy,
group calendar, hide profile) kept in their own block; language and text/line
contrast kept device-local (iOS-specific)
- unify default line colour to #3A3A52 across views
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- style the settings-table dropdowns for the dark theme (were white)
- share-icon picker on one even row (no lopsided wrap)
- more breathing room between colour hex and swatch
- user management: promote/demote admin (new PUT /users/{id}/admin,
guarded against self-change and removing the last admin)
- calendar management table: clip/ellipsis cells so narrow columns no
longer overlap (full-width detail rows still wrap)
- centralise default colours: utils.applyTheme now derives every fallback
from settings-sync.DEFAULT_COLORS (single place for coders to edit)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the ad-hoc appearance panel with a uniform table (sync icon | name |
value): dropdowns instead of button groups, every colour with a reset to a
canonical default, a per-row sync toggle, and a global "sync all" master
switch.
- new settings-sync.js: canonical default colours, declarative table
definition, browser-local value copy, effective-value resolution
- calendar.js: generalised sync engine (pull applies only synced keys from the
server, save pushes only synced keys + the flag map), unified the two save
buttons into one, dropped the dead text/line-contrast code, moved default
duration into the appearance table, effective UI language on load
- account settings (private visibility, group calendar, hide profile) stay in a
dedicated profile block, not the sync table
- app.css: settings table / sync toggle / sync icon styles
- i18n: sync-all / sync-this / share-icon strings; sw cache bump
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shared reference for Web/iOS/Android: canonical keys, default flags, the
GET/PUT sync_flags API, and the pull/push rules each client implements.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce an account-wide sync-flag map that is the single server-side
authority for which settings each client sends/fetches, plus value columns
for two newly-syncable device-local prefs.
- models.UserSettings: sync_flags (JSON), cache_months, month_view_paged
- main._migrate(): idempotent ALTER TABLE for the three new columns
- settings_router: DEFAULT_SYNC map + _resolve_sync_flags(); GET returns
fully-resolved sync_flags (+ cache_months, month_view_paged); PUT accepts
and merges a partial sync_flags map and the two new value fields
Rollout defaults: settings that already lived on the server sync ON; the
four newly-syncable prefs (language, share icon, cache range, month paging)
sync OFF until the user opts in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- add "Geburtstagskalender" to the sidebar +calendar dropdown and a "+ Geburtstage"
button to Settings > Calendars; both call ensureBirthdayCalendar (server enforces
one per account) and toast "already exists" if present
- Settings birthday section: when none exists, just a hint pointing to "+ Geburtstage"
(creation lives in the add area now, not a separate activate button)
- resizable calendar table: sensible default column widths instead of raw measured
- i18n de/en
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Contacts sync fills only an EXISTING birthday calendar (birthdayCalendar),
never auto-creates it — deleting it no longer resurrects it on sync
- Accounts + menu gains "Geburtstagskalender" (only when none exists) to create
the single birthday calendar explicitly
- the Birthdays-from-Contacts section shows a "create one first" hint when there
is no birthday calendar; enabling sync only requests Contacts access
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- POST /local/calendars with is_birthday returns the existing birthday calendar
if one exists (idempotent, no second created)
- PUT /local/calendars rejects (422) marking a second calendar as is_birthday
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Column widths are draggable via a handle on each header's right edge and
persisted in localStorage (re-applied on re-render). Uses fixed table layout
locked to measured px widths so resizing one column doesn't reflow the others.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repeated Contacts syncs must never create duplicate birthdays. Enforce it on
the server, independent of the client's reconcile:
- POST /local/events is now idempotent on external_uid: if an event with the
same (calendar_id, external_uid) exists, update it in place instead of
inserting a new row
- startup cleanup removes existing duplicates sharing the same
(calendar_id, external_uid), keeping the earliest — auto-heals old data
- unique DB index on (calendar_id, external_uid) as a hard guarantee
external_uid is the stable "contact:<deviceId>:<contactId>", so the same contact
from the same device always maps to exactly one event.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reading-column max-width added for the profile panel also constrained the
wide calendar table, forcing a half-width horizontal scroll. Exempt
#settings-panel-accounts so the table uses the full width.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Play requires targeting a recent API level for new apps; bump compileSdk/
targetSdk to 35 and set the release version name to 1.0.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- one birthday calendar per user: BirthdayDialog targets the single calendar and
offers activation when none exists; VM birthdayCalendar()/ensureBirthdayCalendar()
- removed the birthday toggle from the generic new-calendar dialog
- exclude the birthday calendar from the group-visible ("shared calendar") picker
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- one birthday calendar per user; BirthdaysImporter auto-creates/targets it
(no picker) and reconciles per-device (external_uid contact:<deviceId>:<id>)
so multiple devices don't clash
- Contacts birthday sync now runs on every "Sync with server" (syncFromServer),
not just app launch and the manual button
- report the device after each sync (POST /api/birthdays/sync-report) for the
web device list
- Accounts: Birthdays-from-Contacts section drops the target picker, keeps the
reminder picker; removed the birthday toggle from the generic new-calendar sheet
- New-birthday sheet targets the single calendar, offers activation if none
- exclude the birthday calendar from the group-visible ("shared calendar") picker
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- new BirthdaySyncDevice table + /api/birthdays/sync-report & /devices so the
web can show "birthdays come from these devices" (iOS reports its device on
each Contacts sync)
- settings: reject setting a birthday calendar as the group-visible calendar
(it can still be shared directly)
- web: Settings > Calendars > Birthdays shows the device list; exclude birthday
calendars from the group-visible picker
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CalEvent parses is_birthday and exposes renderTitle (server age)
- EventLabel renders a cake icon + render title in month/week/day/agenda/preview
- LocalCalendar model + repository/api gain is_birthday, notify-days, rrule,
birth_year; AccountsScreen can create a birthday calendar with a notify picker
- FAB long-press menu -> "New birthday" opens a minimal name+date dialog
(year-unknown option) targeting a birthday calendar
- L10n strings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- 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>
- 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>
- LocalCalendar/CalEvent gain is_birthday + birthday fields; API sends
rrule/external_uid/birth_year and can create/update birthday calendars
- BirthdaysImporter: mirrors Contacts birthdays into a chosen birthday
calendar, reconciling by external_uid (leaves manual entries untouched)
- AccountsView: "birthday calendar" toggle + notify-days when creating a
calendar, and a "Birthdays from Contacts" section (enable + target + sync now)
- FAB long-press menu -> "New birthday" opens a minimal name+date mask
(BirthdayEditorSheet) targeting birthday calendars, with year-unknown option
- Event bars render display_title (age) and a cake icon via EventLabel
- NSContactsUsageDescription added to both app build configs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
New device-local setting "Monatsansicht seitenweise wischen" (@AppStorage, no
server sync). Off keeps the continuous vertical scroll feed; on switches the
month view to a paged TabView — one month per screen, six height-filling week
rows, swipe left/right to change month (bounded ±monthsBack/Ahead range to keep
the TabView light). WeekRow gains a fillHeight flag so the same row renders at a
fixed height (scroll) or fills the page evenly (paged); its inner cells/divider
now size to the actual geometry. Paged selection is two-way bound to the store's
current date so prev/next/today and the title stay in sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New device-local setting "Monatsansicht seitenweise wischen" (SettingsStore,
no server sync). When off, the month view stays the continuous vertical scroll
feed. When on, it becomes a HorizontalPager — one month per screen, six
height-filling week rows, swipe left/right to change month. The prev/next/today
signals route to the pager (animateScrollToPage) instead of the list, and the
visible month is derived from pagerState.currentPage. WeekRow now takes a
rowModifier so the same row renders at a fixed height (scroll) or weight(1f)
(paged). Setting is re-read when the settings screen closes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1) Burger menu (and other bottom sheets) had rows hidden behind the system
navigation bar: MainActivity never called enableEdgeToEdge(), so the sheets'
navigationBarsPadding resolved to 0. Enable edge-to-edge and place the insets
ourselves — top bar statusBarsPadding, content/FAB navigationBarsPadding,
auth screens systemBarsPadding — and add navigationBarsPadding (+ imePadding
for the editor) to the Filter/Groups/Accounts/Editor sheets.
2) The calendar filter only listed calendars that had events in the loaded
range, so empty calendars couldn't be toggled. Load the full calendar list
from all sources (loadAllCalendars) and merge it with the event-derived
entries — matching iOS, which already shows the complete list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The colour dot for a shared (non-owned) local calendar was disabled and the
colour write hit the owner-only PUT /calendars/{id} (404 for recipients). The
dot is now always editable and colour changes go through the dedicated
PUT /calendars/{id}/color endpoint, storing a recipient's own per-user colour
server-side (synced across devices) without granting write or rename. Renaming
stays owner-only. Parity with Android + server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The colour dot for a shared (non-owned) local calendar was disabled, and the
colour write hit the owner-only PUT /calendars/{id} (404 for recipients). Now
the dot is always editable and colour changes go through the dedicated
PUT /calendars/{id}/color endpoint, which stores a recipient's own per-user
colour server-side (synced across devices) without granting write or rename.
Renaming stays owner-only (no rename UI for shared calendars).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
- UserProfile decodes directory_hidden; updateProfile() sends it; Settings has
a "hide my profile" toggle (excludes you from share/group pickers).
- GroupMember decodes shares_calendar; the group filter list now only shows
members who actually share a calendar (no phantom empty rows).
- Robust custom decoders (decodeIfPresent) for the new fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- 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>
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>
- 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>
- 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>