diff --git a/src/backend/core/tests/api/test_message_import_api.py b/src/backend/core/tests/api/test_message_import_api.py index ccd20d61..2a0e174e 100644 --- a/src/backend/core/tests/api/test_message_import_api.py +++ b/src/backend/core/tests/api/test_message_import_api.py @@ -343,7 +343,7 @@ class TestImportUpdate: def test_patch_continuous_with_pause_persists_mode(self, api_client, mailbox, user): """{mode: continuous, is_active: false} means "arm as a poller but start it paused": the mode must be persisted (not silently dropped) - and no run dispatched, so a later is_active=true can resume it.""" + and no run dispatched.""" channel = self._imap_import(mailbox, user) with patch("core.api.viewsets.imports.run_import_task.delay") as mock_delay: response = api_client.patch( @@ -355,7 +355,16 @@ class TestImportUpdate: assert response.data["mode"] == enums.ImportMode.CONTINUOUS.value assert response.data["is_active"] is False mock_delay.assert_not_called() - # And the paused poller can now actually be resumed. + + def test_patch_resumes_paused_continuous_import(self, api_client, mailbox, user): + """A poller armed but paused via {mode: continuous, is_active: false} can + later be resumed by is_active=true, which dispatches exactly one run.""" + channel = self._imap_import(mailbox, user) + api_client.patch( + self._url(mailbox, channel), + {"mode": "continuous", "is_active": False}, + format="json", + ) with patch("core.api.viewsets.imports.run_import_task.delay") as mock_delay: response = api_client.patch( self._url(mailbox, channel), {"is_active": True}, format="json" diff --git a/src/frontend/src/features/layouts/components/main/header/authenticated.tsx b/src/frontend/src/features/layouts/components/main/header/authenticated.tsx index 4a39c712..727546eb 100644 --- a/src/frontend/src/features/layouts/components/main/header/authenticated.tsx +++ b/src/frontend/src/features/layouts/components/main/header/authenticated.tsx @@ -124,7 +124,7 @@ const ImportIndicator = () => { // importer modal's invalidations wake this query up on a new run. refetchInterval: (query) => { const rows = (query.state.data?.data as ImportRun[] | undefined) ?? []; - return rows.some((r) => !isTerminal(r.status)) ? 60000 : false; + return rows.some((r) => r.is_active && !isTerminal(r.status)) ? 60000 : false; }, }, // Background status poll: let foreground requests win the wire. @@ -134,21 +134,29 @@ const ImportIndicator = () => { const activeRuns = useMemo( () => ((data?.data as ImportRun[] | undefined) ?? []).filter( - (r) => !isTerminal(r.status), + (r) => r.is_active && !isTerminal(r.status), ), [data], ); if (!selectedMailbox || activeRuns.length === 0) return null; - // Average across the runs that already know their total; indeterminate until - // at least one does. Capped below 100 — the button disappears on completion. + // Weighted average across the runs that already know their total, so a small + // run can't dominate the badge; indeterminate until at least one knows its + // total. Capped below 100 — the button disappears on completion. const withTotal = activeRuns.filter((r) => (r.total_messages ?? 0) > 0); + const totalMessages = withTotal.reduce( + (sum, r) => sum + (r.total_messages ?? 0), + 0, + ); const progress = withTotal.length ? Math.min( 99, Math.round( - withTotal.reduce((sum, r) => sum + (r.progress ?? 0), 0) / withTotal.length, + withTotal.reduce( + (sum, r) => sum + (r.progress ?? 0) * (r.total_messages ?? 0), + 0, + ) / totalMessages, ), ) : null;