Calendarr-Android wird mit vollstaendiger Historie als Unterverzeichnis
android/ eingebunden, damit Server, Web und die mobilen Apps in einem
Repo liegen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Seite trug zweimal autocomplete="one-time-code": im Login und im Feld
zum Einrichten von 2FA in den Einstellungen. Fuer einen Passwortmanager ist
damit nicht eindeutig, zu welchem Feld sein Inline-Symbol gehoert.
Das Einrichtungsfeld bekommt data-bwignore (plus die Entsprechungen fuer
1Password und LastPass) — dort tippt man den Code aus der Authenticator-App
beim Anlegen eines neuen Geheimnisses ab, ein Ausfuellen ergibt da ohnehin
keinen Sinn. Die Passwortfelder bleiben unangetastet, die sollen weiter
befuellbar sein.
Ob das die fehlplatzierte Bitwarden-Anzeige behebt, ist offen: hier laesst
sich keine Erweiterung installieren. Der vorherige Versuch ueber das
Fokus-Timing hat es nicht geloest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Benutzername und Passwort liegen ab Seitenaufbau im Layout, das 2FA-Feld
taucht erst aus display:none auf — und der Fokus wurde im selben Tick
gesetzt. Passwortmanager oeffnen ihr Inline-Overlay beim Fokus und messen die
Feldposition in genau dem Moment; Bitwarden platzierte sein Symbol dadurch
weit neben dem Feld.
Der Fokus laeuft jetzt ueber setTimeout(0), sodass das Layout vorher steht,
plus ein resize-Event als Anstoss zum Neupositionieren. Bewusst kein
requestAnimationFrame: das feuert in einem nicht sichtbaren Tab nicht und
haette den Fokus ganz verschluckt.
Nicht gegen Bitwarden verifizierbar — hier ist keine Erweiterung installiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Endpunkte gab es schon, die Oberflaeche fehlte — es war schlicht nicht
sichtbar, ob und womit ein Konto verknuepft ist.
Unter Einstellungen > Profil, zwischen App-Passwoertern und Privatsphaere,
steht jetzt "Single Sign-On" mit den verknuepften Konten (Anbieter, E-Mail,
letzte Nutzung) und je einem Knopf zum Aufheben. Ohne Verknuepfung erscheint
ein Hinweis, wie man eine anlegt; ist auf dem Server ueberhaupt kein Provider
konfiguriert, bleibt der ganze Abschnitt unsichtbar.
Bei einem per SSO angelegten Konto ist die letzte Verknuepfung nicht
loesbar — es hat kein brauchbares Passwort und waere danach dauerhaft
unerreichbar. Der Knopf ist in dem Fall deaktiviert und erklaert sich per
Tooltip; der Server lehnt es zusaetzlich ab.
Im Browser durchgespielt: verknuepfen, Eintrag erscheint mit Anbieter und
Datum, aufheben, Bestaetigung, leerer Zustand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Sitzungs-Token trug den Benutzernamen als Subjekt, und get_current_user
suchte danach. Eine Umbenennung machte damit jedes Token auf jedem anderen
Geraet stillschweigend ungueltig — der Nutzer flog ohne Hinweis raus.
Tokens tragen jetzt zusaetzlich uid, die unveraenderliche Zeilen-ID, und die
Aufloesung geht darueber. sub bleibt als Anzeigename erhalten. Tokens ohne
uid werden weiter ueber den Namen aufgeloest, damit vor der Aenderung
ausgestellte Sitzungen gueltig bleiben — mit "Angemeldet bleiben" laufen die
180 Tage.
Fuer SSO war die Zuordnung schon vorher richtig: OIDCIdentity haengt an
(provider_key, sub) des Providers, und bei bekanntem sub wird der lokale
Benutzername nicht angefasst. Ein umbenannter Nutzer in Authentik landet also
unveraendert auf demselben Konto. Vier Tests halten beide Richtungen fest:
Umbenennung beim Provider, lokale Umbenennung mit weiterlaufender Sitzung,
lokale Umbenennung mit erhaltener SSO-Verknuepfung, und die Kompatibilitaet
alter Tokens.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bisher war eine SSO-Anmeldung ohne passendes Konto eine Sackgasse: rote
Fehlermeldung, kein Weg weiter. Die Verknuepfung ging nur ueber
LINK_BY_EMAIL, das der E-Mail-Verwaltung des Providers vertrauen muss.
Jetzt endet der Callback bei einer besitzerlosen Identitaet nicht im Fehler,
sondern parkt sie in einem kurzlebigen signierten HttpOnly-Cookie und leitet
auf /?sso_link=1. Der Login-Screen bittet daraufhin einmalig um das
Calendarr-Passwort; nach erfolgreichem Login loest der Client
POST /api/auth/oidc/link-pending ein und die Identitaet haengt an genau dem
Konto, das sich gerade ausgewiesen hat. Beide Haelften sind damit bewiesen —
das ID-Token haben wir selbst geprueft, das Konto durch das Passwort — und
die Vertrauensannahme ueber die E-Mail entfaellt.
Das greift fuer alle drei Faelle, in denen eine gueltige Identitaet keinen
Besitzer hat: kein Konto vorhanden, Selbstregistrierung aus, oder E-Mail
bereits vergeben.
Ausserdem: unbekannte Fehler-Slugs werden jetzt mit angezeigt statt hinter
"Anmeldung fehlgeschlagen" zu verschwinden — genau die Information, die zur
Diagnose fehlte. Und oidc_token_exchange_failed hat eine eigene Meldung, die
auf Client-Typ und Redirect-URI hinweist, weil das der haeufigste
Konfigurationsfehler ist.
Sechs Tests dazu; Ablauf zusaetzlich im Browser gegen einen echten
OIDC-Stub-Provider durchgespielt: Hinweis statt Fehler, Verknuepfung nach
Passwort-Login, danach Anmeldung allein per SSO.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Fehler, die den Flow gebrochen haetten:
- Ein Abbruch im Browser liefert bei AppAuth ein regulaeres Ergebnis mit
USER_CANCELED_AUTH_FLOW, nicht null. Der bisherige Null-Check lief ins
Leere und "Zurueck" endete in einer roten Fehlermeldung.
- launch(intent) lag ausserhalb von runCatching. Verlaesst der Nutzer den
Screen waehrend der Discovery, ist der Result-Launcher abgemeldet und die
IllegalStateException haette die App beendet.
- Nonce und Provider lagen nur in ViewModel-Feldern. Der Custom Tab laeuft
in eigener Task, dieser Prozess wird dabei routinemaessig getoetet — die
Anmeldung waere nach erfolgreichem Login beim IdP gescheitert. Der Nonce
kommt jetzt aus AuthorizationResponse.request, Provider und Client-ID
werden vor dem Start persistiert.
Weiter: AuthState.update verlangt genau eines von (Response, Exception) und
haette bei null/null geworfen; onCleared() verwarf den @Singleton-Service
fuer die zweite AuthViewModel-Instanz mit.
Neu ist der geforderte Hintergrund-Refresh: laeuft das Calendarr-Token
binnen 24 h ab, wird beim Start ueber den Provider-Refresh-Token ein frisches
ID-Token geholt und getauscht — ohne Browser. Best effort; schlaegt es fehl,
bleibt das vorhandene Token in Kraft.
Ausserdem: Scopes kommen ausschliesslich vom Server (mobile_scopes), Spinner
pro Provider statt global, fehlender Browser als eigener Fehlerfall, Issuer
wird validiert, keine namenlosen Identitaeten mehr.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Authlib wertet "Aufrufer hat eine Nonce uebergeben, das Token traegt keine"
als Mismatch. Das ist fuer den erneuerten Token der mobilen Clients falsch:
ein per Refresh-Token geholtes ID-Token laesst die nonce-Claim regulaer weg,
womit der Hintergrund-Refresh immer an einem 401 gescheitert waere.
Die Nonce wird daher nicht mehr an Authlib durchgereicht, sondern hier
geprueft: vorhandene Nonce muss passen, und der Browser-Flow verlangt sie
zusaetzlich zwingend (require_nonce) — dort erzeugen wir sie selbst, also
muss der Provider sie zurueckspiegeln. Signatur und uebrige Claims bleiben
unveraendert bei Authlib.
Zwei Tests dazu: erneuertes Token ohne Nonce wird angenommen, Browser-Flow
ohne Nonce abgelehnt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zusaetzlicher Anmeldeweg neben Benutzername/Passwort. Der Login-Screen holt
die konfigurierten Provider von GET /api/auth/oidc/providers; ohne Provider
oder gegen einen aelteren Server bleibt der Screen unveraendert.
Die App laeuft als Public Client: Authorization Code Flow mit PKCE in einem
Chrome Custom Tab (nie ein WebView), ohne Client Secret. Das ID-Token wird
anschliessend an POST /api/auth/oidc/exchange gegen ein Calendarr-Token
getauscht; die Antwort hat dieselbe Form wie der Passwort-Login, sodass der
bestehende Session-Pfad unveraendert weiterlaeuft.
- OidcAuthenticator kapselt Discovery, PKCE-Request und Code-Tausch.
- Die angefragten Scopes kommen vom Server (mobile_scopes) — ein Scope, den
der Provider nicht kennt, wuerde die ganze Anfrage scheitern lassen.
- AuthState mit dem Refresh-Token liegt in den EncryptedSharedPreferences,
nie im Klartext, und wird beim Logout mitgeloescht.
- Provider und Client-ID werden vor dem Browser-Start persistiert, weil der
Custom Tab in einer eigenen Task laeuft und dieser Prozess dabei getoetet
werden kann; der Nonce kommt aus AuthorizationResponse.request, das AppAuth
ueber den Redirect zurueckreicht.
- Abbruch im Browser meldet AppAuth als regulaeres Ergebnis mit
USER_CANCELED_AUTH_FLOW und wird still behandelt, nicht als Fehler.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Apps duerfen keine Scopes erfinden: ein Scope, den der Provider nicht
kennt, laesst ihn die gesamte Authorization-Anfrage ablehnen. Daher legt
jetzt der Server fest, was die nativen Clients anfragen.
OIDC_<KEY>_MOBILE_SCOPES ist frei setzbar; ohne Angabe sind es die
Web-Scopes plus offline_access, damit Authentik ab 2024.2 ueberhaupt ein
Refresh-Token ausstellt. Der Browser-Flow bleibt unveraendert, der braucht
kein offline_access. Ausgeliefert wird der Wert als mobile_scopes in
/api/auth/oidc/providers, aber nur wenn eine MOBILE_CLIENT_ID konfiguriert
ist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Platzhalter-URI calendarr://oauth/callback durch die tatsaechlichen
Reverse-DNS-Schemata der beiden Apps ersetzt und vermerkt, dass sie als
Public Clients ohne Secret laufen und ohne MOBILE_CLIENT_ID keinen
SSO-Button zeigen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zusaetzlicher Anmeldeweg per OIDC neben dem bestehenden Passwort-Login. Ohne
gesetztes OIDC_PROVIDERS aendert sich nichts: /api/auth/oidc/providers meldet
enabled:false und der Login-Screen bleibt unveraendert.
Backend
- oidc_config.py: Provider-Registry rein aus ENV. Ein zweiter Provider ist
reine Konfiguration (OIDC_PROVIDERS + OIDC_<KEY>_* Block), keine Codeaenderung.
- oidc_client.py: Discovery, JWKS, Token-Tausch, ID-Token-Pruefung ueber
authlib.jose mit fest gepinnter Signatur-Allowlist (nur asymmetrisch, damit
alg:none und Algorithmus-Confusion ausgeschlossen sind).
- oidc_identity.py: Zuordnung ueber (provider_key, sub). Ein E-Mail-Treffer auf
ein bestehendes Konto wird standardmaessig abgelehnt statt verknuepft — sonst
erbt jeder, der im IdP eine beliebige E-Mail setzen kann, das passende Konto.
Verknuepfen ist eine explizite, authentifizierte Aktion. Auto-Provisioning
nur per ENV-Flag, standardmaessig aus, optional mit Domain-Allowlist.
- routers/oidc_router.py: /providers, /start, /callback, /complete, /exchange,
/link, /identities. PKCE S256; Flow-State als kurzlebiges signiertes JWT in
einem HttpOnly-Cookie mit jti==state statt eines prozesslokalen Dicts, damit
der Flow Neustarts und mehrere Worker uebersteht. Das Calendarr-JWT wird ueber
ein Einmal-Cookie uebergeben, nie ueber URL oder Fragment.
- /exchange als Kontrakt fuer die AppAuth-Clients: die App fuehrt den Flow als
Public Client selbst aus und tauscht das ID-Token; kein Secret im Request.
- models.py: neue Tabelle oidc_identities, users.auth_source; Migration in
main.py im dortigen try/except-Stil.
Nebenbefund, mitbehoben: auth.verify_password liess ValueError aus
bcrypt.checkpw durch. Ein nicht wohlgeformter password_hash wurde damit zu
HTTP 500 auf /api/auth/login und bei jedem CalDAV-Basic-Auth. SSO-Nutzer
bekommen einen echten bcrypt-Hash ueber ein zufaelliges, nirgends gespeichertes
Geheimnis; fuer CalDAV nutzen sie App-Passwoerter wie 2FA-Nutzer.
Web
- SSO-Block auf dem Login-Screen, aus /providers befuellt; Buttons sind Links,
weil /start eine Top-Level-Navigation braucht.
- consumeSsoHandoff() laeuft vor den Setup-/Token-Pruefungen in boot() und
raeumt die Query per replaceState ab, bevor es awaited.
- 401-Auto-Logout um die SSO-Pfade erweitert, sonst dreht ein fehlgeschlagener
Handoff eine Reload-Schleife.
- i18n-Keys in de + en (Fallback-Kette deckt die uebrigen acht Sprachen).
- Service Worker laesst /api/auth/oidc/ unangetastet durch.
37 neue Tests (Gesamtsuite 60 gruen): Signaturpruefung mit echten RSA-Tokens,
alg:none, falscher iss/aud/nonce, unbekannte client_id, Provisioning-Policies,
Username-Kollision, Einmaligkeit des Handoff-Cookies, unerreichbarer Provider.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Statt eines einzelnen Punkts in --primary zeigt der Mini-Kalender jetzt fuer
jeden Kalender, der an dem Tag mindestens einen Termin hat, einen Punkt in
dessen Farbe (max. 4, stabil nach erstem Auftreten sortiert). Die Farbquelle
ist dieselbe wie in Monats-/Wochenansicht: ev.color || ev.calendarColor.
Ausserdem laeuft der Mini-Kalender jetzt ueber filterEvents(), damit
ausgeblendete Gruppenmitglieder keinen Punkt mehr erzeugen. Auf dem
"heute"-Kreis bekommen die Punkte einen duennen weissen Ring, damit sie auf
der farbigen Flaeche lesbar bleiben.
Cherry-pick von 7a2085b (master) auf beta; die selected-Day-Regel aus beta
(color-mix) bleibt erhalten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds Swiss German, French, Italian, Dutch, Danish, Norwegian, Swedish and
Finnish — ten selectable languages, each complete at 418/418 keys with
matching placeholders and date arrays.
A language used to be registered in four unrelated places (dictionary,
settings dropdown, date-picker locale ternary, resolver fallback), so
adding one meant touching all of them. LANGUAGES in i18n.js is now the
single source of truth: adding a language is one entry plus one
dictionary, and the dropdown derives from it.
The registry also separates the stored code from the BCP-47 tag used for
formatting. That distinction is what makes 'ch' safe — "ch" is a country,
not a language, and would throw if handed to Intl; it maps to de-CH.
Fixes locale handling along the way: fmtTime/fmtDatetime in calendar.js,
month.js and week.js hardcoded 'de', so English users saw German-formatted
times, and two toLocaleDateString() calls silently used the browser locale
instead of the app language. All formatting now goes through getLocale().
Missing keys fall back language → English → German rather than straight to
German, and the dead per-dictionary `locale` key is gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the stale blue calendar icon with the real green Calendarr app
icon (matching iOS/Android) for the installable web app:
- icon-192/512.png regenerated from the branded 1024 app icon
- add dedicated maskable icons (192/512) with brand-green safe-zone
padding so Android/Chrome render a proper adaptive icon (no white box)
- manifest: maskable purpose entries, id, description, lang/dir,
categories; theme_color -> brand green (#16713d)
- index.html: theme-color -> green, add modern mobile-web-app-capable
- favicon.svg + icon.svg recolored to brand green
Bump APP_VERSION v89 -> v90.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Equal rights for all calendar types: the sidebar remove button is now a uniform
"hide" (eye) for every calendar instead of delete-for-some / nothing-for-shared.
- Owned local (incl. birthday) and iCal now hide via the server sidebar_hidden
flag (delete stays available in Settings' calendar table).
- Calendars shared with me get a per-device sidebar hide (they can't carry a
server flag); their events are filtered out while hidden, and the Settings
calendar table's "shared with me" rows gain an un-hide eye toggle.
- External caldav/google/ha unchanged.
Group calendars intentionally left as-is for now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The version line in the Impressum footer ("Calendarr vNN") is now a
subtle link to the Calendarr Gitea repo (git.scarriffle.com), opening
in a new tab. Styled to stay muted at rest and reveal as a link on
hover. Bump APP_VERSION v87 -> v88.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Admin default-theme editor: "Theme importieren" button loads a .theme.json
into the editor (colours only, live preview) so an imported theme can be set
as the instance default (still confirmed via "Standard speichern").
- Branding: drop the "Standard" placeholder; show pixel-dimension hints instead
(logo shown at max 150×40, favicon 128×128). Logo preview box now mirrors its
real topbar footprint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the blue defaults with the provided green theme (primary #58B900,
accent #45A148, today #6FB669, greens for month/selected/today-bg, etc.). These
apply when no admin instance theme is set. THEME.md defaults table updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Admin settings tab (admin-only) now holds user management plus a server-wide
default theme editor (live preview + discard) and logo/favicon branding.
- New singleton InstanceSettings model + /api/instance router (public GET for
the login screen; admin-gated theme PUT and logo/favicon upload/delete,
reusing the avatar PIL/validation pattern).
- Instance default theme is the base users inherit and the target a per-colour
"Reset" returns to (baseColor: instance default -> built-in).
- Custom favicon overrides the primary-colour tinting; custom logo replaces the
top-left glyph+text, hard-capped so it only scales down and never breaks layout.
Branding + defaults load before login (public endpoint).
- Theme export now uses the recognised .theme.json extension (old .theme still
imports); import stays partial/unknown-key aware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Theme:
- Export/import themes via UI as <date>_<time>.theme (JSON, readable keys,
link to THEME.md). Import is partial-aware: only present params are written
(server accepts partial); prompts before importing files with unknown params.
- Dynamic favicon + theme-color tinted to the primary colour on load/save.
- New themable colours: general hover-highlight, day hover/selected/bg,
today background, plus two unified sidebar action-icon colours
(inactive/active) covering bell, hide, delete and read-only icons.
- All new colours are per-setting syncable; documented in THEME.md.
UX:
- Styled confirm dialog (#modal-confirm) replaces window.confirm() for
calendar delete and account disconnect.
- Birthday/local calendars and iCal subscriptions can now be hidden from the
sidebar via Settings (new sidebar_hidden column + hide toggle).
Backend: additive nullable columns + idempotent migrations for user_settings
colours and local_calendars/ical_subscriptions.sidebar_hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- LICENSE: GNU GPLv3
- fastlane/metadata (de-DE + en-US): title, descriptions, changelog, icon —
used by both the own F-Droid repo and the official listing
- fdroid/com.scarriffle.calendarr.yml: ready-to-submit fdroiddata build recipe
- FDROID.md: how to run the own repo (fdroid.scarriffle.com) and submit to
official F-Droid, incl. the signing-key caveat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lay out all-day events as bars across their start→end day columns, packed into
lanes, instead of listing a multi-day event separately in each day's column
(mirrors the month view and iOS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_cal_dict now labels shared (non-owned) calendars by the sharer/group name
(the value already computed for shared_by) instead of the owner's real calendar
name. Fixes recipients seeing the true name in calendar-management lists (and
anywhere else the name is shown); all clients pick it up automatically.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- gear now opens a full-screen Menu screen (Profile / Appearance / Accounts /
Groups / Sync / Switch server / Logout), replacing the bottom-sheet menu that
sat partly under the system nav buttons. Each entry opens its own full-screen
page and returns to the menu on close.
- settings 'Darstellung' is the appearance table only again (no long scroll to
reach navigation)
- much more compact settings: dropdowns use a small outlined chip instead of a
full-height OutlinedTextField, smaller sync icons, bodyMedium row labels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- move the top-bar hamburger to the LEFT (drawer opens from the left)
- much denser drawer: compact header, and calendar rows now use a small eye
toggle instead of the bulky Switch, with tighter padding/text
- tapping the gear now opens the full-screen Settings directly (no bottom-sheet
menu that sat under the system nav buttons, no redundant sync — that's in the
drawer header). Accounts / Groups / Profile / Switch server / Logout moved
into a 'More' section at the bottom of Settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the iOS Contacts birthday importer: read the address book's birthday
events and reconcile them into the user's birthday calendar, scoped to this
device via external_uid 'contact:<deviceId>:<contactId>' (add/update/delete),
then report the device.
- READ_CONTACTS permission + ContactsReader (Event TYPE_BIRTHDAY, handles
year/no-year date formats)
- API: GET /birthdays, POST /birthdays/sync-report; local-event create/update
now carry external_uid (+ update carries rrule/birth_year)
- CalendarViewModel.syncContactBirthdays reconciles like iOS; device-local
enable flag + stable device id in SettingsStore
- birthday dialog gains an 'Import from contacts' action (requests permission);
auto-syncs on launch when enabled
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the iOS side drawer on Android with a ModalNavigationDrawer opened by the
hamburger or an edge-swipe (replaces the calendar filter bottom sheet):
- header with avatar/name/server + manual sync, full menu (gear), and close
- view + group switcher chips
- flat calendar list: tap toggles visibility, long-press opens a context menu
(hide/banish + reminders), and a Sort toggle reveals up/down reordering.
Order is device-local (SettingsStore.calendarOrder, mirrors the web).
- month-switching is now a named dropdown (scroll vs pages) like web/iOS
- new device-local 'Hide menu button' setting (drawer opens via edge-swipe)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New 'Monatswechsel' setting (synced flag already existed as month_view_paged):
- Continuous scroll (week): current behaviour — wheel scrolls by a week,
buttons/swipe jump 4 weeks.
- Page by page (month): wheel navigation is disabled; buttons and horizontal
swipe jump a whole calendar month.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Redesign the settings screen into uniform rows (sync icon | name | value):
dropdowns replace the chip pickers, every colour gets a reset to a canonical
default, a per-row sync icon toggles that setting's cross-device sync, and a
global switch flips all. Enabling a flag adopts this device's current value.
- AppSettings/SettingsStore: add text/bg/line colours, cache_months,
month_view_paged, and the account-wide sync_flags map (+ DEFAULT_SYNC,
effective-merge on pull)
- CalendarRepository.updateSettings: partial PUT of only the synced values plus
the flag map (unsynced columns stay untouched server-side)
- Theme: fully dynamic — primary/accent tint, background/text/line drive
background/onBackground/outline (matching web); today/divider/label unchanged
- add hide-profile (directory_hidden) to the profile block (parity with web/iOS)
- language + text/line contrast stay device-local
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- 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>
- 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>
- 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>