From 719eeef37872d2f54558394c70da7ecd4ab7bf0b Mon Sep 17 00:00:00 2001 From: Lukas Remis Date: Fri, 7 Aug 2026 07:59:26 +0200 Subject: [PATCH] Backfill start-window indicator, account-delete spinner, app-password Help popup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backfill: show the sidebar spinner + "Preparing…" banner during the brief window after account creation when state=backfilling but total is not yet counted (total===0), so a freshly-added account shows progress immediately instead of looking done until a refresh. The stuck-at-100% guard is preserved. - Settings: a centered spinner overlay ("Removing account…") covers the screen from confirm until the (slow) server cascade + accounts refetch complete, so the row no longer appears frozen before it disappears. - Add-account wizard: the manual password hint now includes Outlook and gains a "Help" link opening a popup with per-provider direct links to enable 2-step verification and generate the app password (Gmail, Outlook, Yahoo, iCloud). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA --- frontend/src/components/BackfillBanner.tsx | 35 +++++--- frontend/src/i18n/en.ts | 12 ++- frontend/src/routes/MailLayout.tsx | 3 +- frontend/src/routes/settings/Settings.tsx | 25 +++++- frontend/src/routes/wizard/AccountWizard.tsx | 87 +++++++++++++++++++- 5 files changed, 144 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/BackfillBanner.tsx b/frontend/src/components/BackfillBanner.tsx index 5ad907d..2b7575d 100644 --- a/frontend/src/components/BackfillBanner.tsx +++ b/frontend/src/components/BackfillBanner.tsx @@ -13,9 +13,12 @@ export function BackfillBanner() { const accounts = useQuery({ queryKey: ["accounts"], queryFn: api.accounts }); const importing = (accounts.data ?? []).filter( - // Also require done < total so a completed account whose state event was missed - // (e.g. a dropped WS) never leaves a stuck "X of X" banner. - (a) => a.state === "backfilling" && a.backfill_total > 0 && a.backfill_done < a.backfill_total, + // Show while backfilling — including the brief start window before the worker has + // counted the total (total === 0). Hide only once done, so a completed account + // whose state event was missed (e.g. a dropped WS) never leaves a stuck banner. + (a) => + a.state === "backfilling" && + (a.backfill_total === 0 || a.backfill_done < a.backfill_total), ); const active = importing.length > 0; @@ -40,8 +43,10 @@ export function BackfillBanner() { }`} > {importing.map((a) => { + // total === 0: the worker hasn't counted yet — show an indeterminate bar. + const preparing = a.backfill_total === 0; const done = Math.min(a.backfill_done, a.backfill_total); - const pct = Math.round((done / a.backfill_total) * 100); + const pct = preparing ? 0 : Math.round((done / a.backfill_total) * 100); return (
{a.email}
- {t("backfill.importing", { - done: done.toLocaleString(), - total: a.backfill_total.toLocaleString(), - })} + {preparing + ? t("backfill.preparing") + : t("backfill.importing", { + done: done.toLocaleString(), + total: a.backfill_total.toLocaleString(), + })}
-
+ {preparing ? ( +
+ ) : ( +
+ )}
); diff --git a/frontend/src/i18n/en.ts b/frontend/src/i18n/en.ts index a6aaa8c..80aa59e 100644 --- a/frontend/src/i18n/en.ts +++ b/frontend/src/i18n/en.ts @@ -236,7 +236,15 @@ export const en = { "wizard.port": "Port", "wizard.security": "Security", "wizard.accountPassword": "Account password", - "wizard.appPasswordHint": "For Gmail/Yahoo/iCloud use an app password.", + "wizard.appPasswordHint": "For Gmail, Yahoo, iCloud, or Outlook use an app password.", + "wizard.help": "Help", + "wizard.appPasswordHelp.title": "Set up an app password", + "wizard.appPasswordHelp.intro": + "These providers need a one-time app password instead of your normal password:", + "wizard.appPasswordHelp.prefix": "Make sure to", + "wizard.appPasswordHelp.twoStep": "turn on 2-step verification", + "wizard.appPasswordHelp.middle": "if you haven't done it yet, and then", + "wizard.appPasswordHelp.generate": "generate the app password", "wizard.test": "Test & continue", "wizard.testing": "Testing connection…", "wizard.testOk": "Connection works.", @@ -303,6 +311,7 @@ export const en = { "settings.downloadAllMail": "Sync remaining mail", "settings.syncedPartially": "Synced partially", "backfill.importing": "Importing messages… {done} of {total}", + "backfill.preparing": "Preparing to import messages…", "settings.appearance": "Appearance", "settings.handle": "Handle", "settings.recoveryEmail": "Recovery email", @@ -376,6 +385,7 @@ export const en = { "settings.addAccount": "Add account", "settings.syncState": "State: {state}", "settings.confirmRemoveAccount": "Remove this account and all its mail?", + "settings.removingAccount": "Removing account…", "settings.editAccount": "Edit account", "settings.color": "Color", "settings.serverSettings": "Change server settings / password", diff --git a/frontend/src/routes/MailLayout.tsx b/frontend/src/routes/MailLayout.tsx index 7339ad5..9d0a04d 100644 --- a/frontend/src/routes/MailLayout.tsx +++ b/frontend/src/routes/MailLayout.tsx @@ -583,8 +583,7 @@ export function MailLayout() { color={a.color} syncing={ a.state === "backfilling" && - a.backfill_total > 0 && - a.backfill_done < a.backfill_total + (a.backfill_total === 0 || a.backfill_done < a.backfill_total) } total={a.backfill_total} done={a.backfill_done} diff --git a/frontend/src/routes/settings/Settings.tsx b/frontend/src/routes/settings/Settings.tsx index ca6e468..645c383 100644 --- a/frontend/src/routes/settings/Settings.tsx +++ b/frontend/src/routes/settings/Settings.tsx @@ -251,9 +251,15 @@ function AccountsSection() { const [editing, setEditing] = useState<{ id: string; mode: "account" | "signature" } | null>( null, ); + // Which account is being deleted — drives the blocking spinner overlay until the + // (slow) server cascade finishes and the refetched list no longer includes it. + const [deletingId, setDeletingId] = useState(null); const remove = useMutation({ mutationFn: (id: string) => api.deleteAccount(id), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["accounts"] }), + onSettled: async () => { + await queryClient.invalidateQueries({ queryKey: ["accounts"] }); + setDeletingId(null); + }, }); const downloadAll = useMutation({ mutationFn: (id: string) => api.updateAccount(id, { sync_window: "all" }), @@ -325,7 +331,10 @@ function AccountsSection() { +

{detailsBlock} {tested &&

{t("wizard.testOk")}

}
@@ -356,6 +391,56 @@ export function AccountWizard({ embedded = false, onDone }: WizardProps = {}) { )} {error &&

{error}

} + + {helpOpen && ( +
setHelpOpen(false)} + > +
e.stopPropagation()} + > +
+

{t("wizard.appPasswordHelp.title")}

+ +
+

{t("wizard.appPasswordHelp.intro")}

+ +
+
+ )}
); }