mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
One expired Google grant on the reporting install had stopped mail for two days and produced ~5 000 log lines, and the app knew: OAuthTokenManager wrote `invalid_grant` to Account::$oauthLastRefreshError, CalendarSyncService wrote the calendar failures, IntegrationTokenManager already composed "X needs to be reconnected". Nothing read any of it back — grep for oauthLastRefreshError over templates/ and src/Controller/ returned nothing, and no template contained the word "reconnect". The condition was detected, stored, and discarded. WHAT THIS ADDS A health model (AccountHealthInspector) that reads that stored state and answers "what is wrong, and what would fix it". It reads; it never probes. Five problem classes: dead OAuth grant, calendar sync failing, degraded push, integration needing reconnection, and abandoned queue work. Consequences are attributed to their cause. One dead grant taking three calendars and 500 jobs down with it is one thing to do, not nine, so the calendars are listed under the account that explains them and the indicator counts root causes. Severity is drawn by consequence, not by how alarming the error looks. Degraded push is a Notice and deliberately does NOT light the indicator: mail still arrives, and a page that paints "slightly delayed" the same red as "stopped" is a page people learn to close. RECONNECT IN PLACE OAuthAccountLinker::relink() writes a fresh grant onto the existing Account row. Mail, threads, labels, rules, aliases, calendars and settings all key on that id and all survive — the lighter alternative to delete-and-re-add, which is what people resort to today and what costs them their mail. Guarded on identity: the authorised mailbox must be the same address and the same provider, compared case-insensitively, and a mismatch is refused outright rather than resolved. Signing in as the wrong Google account is a mistake anyone makes, and writing those tokens here would file a stranger's mail into these threads with no undo. The repair intent travels in the session, never through the provider redirect, so no account id is user-editable. STOPPING THE LOG FLOOD A calendar that cannot sync never updates lastSyncedAt, so findDueForSync() returned it on every sweep forever. Calendar now carries syncFailureCount and syncBackoffUntil; the sweep skips a calendar inside its window, and the handler logs only when the failure is news. Not a mute button, and not a reclassification — GoogleCalendarApiClient::token() is right to refuse to call a refused refresh permanent. The first failure always reports, a CHANGED message always reports, and the delay is capped at a day so a condition that heals is retried within one. SURFACING Its own settings section rather than a block on Accounts: the problems span accounts, calendars, connections and the queue, and a variable-height alert stack would push the real account controls below the fold exactly when something is wrong. Plus a topbar badge, following LogAlertGlobal's shape including ResetInterface (worker mode) — but not dismissible, because "seen" is the right model for a log and the wrong one for a condition that is still true. Every string is translated (en/de/en_PI) and written for a person: what broke, what it means for their mail, and what the button will do before they press it. Provider error codes stay behind a disclosure. Verified: phpunit 3411 tests green, composer stan clean, 5 Playwright specs green, both themes at 1440px and 393px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>