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() {