- Shipped
- September 5, 2026 at 6:32 AM UTC
- Author
- Kamo
- Commit
- bac56fe
The button raised alert('OAuth flow not yet implemented - configure credentials manually via the API'). It now opens the real Google Workspace / Microsoft 365 consent flow and reports what came back. The trust boundary is the interesting part. The popup completes on the callback registered in the provider's own console, which is served on the API host -- a different origin from this window -- so its postMessage usually fails the origin check and the outcome is learned from the popup simply closing. A signal that carries no result must never be read as a success, so every path ends by asking the server what it actually stored: a member who closes the consent screen is shown no connection. The panel also embeds OrgOAuthAppSection, so the app this connects through is visible and editable here, and it is the SAME registration the Provider Setup tab reads. Three fixes found while wiring it, all in this tab: - A render loop. The status loader depended on `t`; useTranslations() does not promise a stable identity between renders, so the loader was recreated, its effect refired, it set state, and round again. Nothing here holds `t` across a callback now -- a problem is carried either as wording the service chose or as one of four named keys. - The tab errored on every load. GET /org answers 204 for an org with no integration, and contact_integrations is empty cluster-wide, so res.json() threw on the empty body and every visit raised "Failed to load integration settings". - The two-way-sync switch never reflected what was stored: it read `bidirectionalSync` where the row carries `bidirectional`. Reads both, sends the one the column is named after. The sync bar beside it printed its service enum raw (IDLE / SYNCING / ERROR) with a hard-coded "Last sync:", and never showed the row's error message at all -- so an ERROR chip told a member something was wrong and gave them nothing to act on. It now names the states and shows the reason, and Sync now re-reads the row it just kicked off. The tab's remaining hard-coded English is translated too. Key paths are spelled out in full at every call site rather than built from a shared prefix const: scripts/check-i18n-keys.mjs resolves a literal t('a.b.c') and a template whose static part comes first, but cannot see through t(`${KEYS}.c`) -- verified by breaking a key behind such a prefix and watching the guard still report clean. Behind one of those, all 15 new keys would have shipped unguarded, and a typo renders as a raw key path on screen.