- Shipped
- September 5, 2026 at 4:14 PM UTC
- Author
- Kamo
- Commit
- b46d317
PersonalProviderSection was scaffolding that nothing rendered, and it would not have worked if it had: it read GET /member/{id}, a route that did not exist, so it failed on open; it offered an Add button for Google and Microsoft that POSTed a row with no credentials, producing a connection that could never sync; its two-way toggle sent bidirectionalSync where the column is bidirectional, so the switch moved and nothing was stored; and it had a "Unify with organisation" switch for a field ContactIntegration does not have, which the service dropped on every write. Now it lists what a member has connected with its real sync state, connects Google and Microsoft through the OAuth flow, takes a URL and password for iCloud and custom servers, and says when a connection holds no credential -- which the switches alone cannot, and which is the difference between "off" and "cannot work". The unify switch is gone rather than faked: there is nowhere to store it, and a switch that silently persists nothing is worse than no switch. DavOAuthConnect gains a scope rather than being copied. Everything hard about it -- a callback on another origin, a popup that cannot report its own result, the re-read that is the only trustworthy signal -- is identical for a member and for an organization, and a second copy would drift on exactly the parts nobody re-reads. In member scope it appends ?member=true and the server takes the member from the session, so the panel cannot address anybody else. It lives in the calendar's own preferences drawer. The email settings topic is gated on MANAGE_EMAIL_SETTINGS, so putting personal calendars there would have hidden them from nearly everyone whose calendars they are. Last in the drawer and behind a divider: every other block there is a preference the panel owns, while this one authorizes a third party and can fail in ways the member has to act on. Key paths are literal and the namespace is inline. check-i18n-keys resolves useTranslations('<literal>') and gives up on useTranslations(SOMETHING) -- and giving up means skipping every key in the file, not just one, so a const there would have left all thirty unchecked. Reaching t through a ref to keep it out of the loader's dependencies hides a call site from the guard the same way, so the loader sets a flag and the render translates it.