diff --git a/apps/dashboard/src/features/accounts/accounts-page.tsx b/apps/dashboard/src/features/accounts/accounts-page.tsx index 3e4e66f..473ffe0 100644 --- a/apps/dashboard/src/features/accounts/accounts-page.tsx +++ b/apps/dashboard/src/features/accounts/accounts-page.tsx @@ -99,6 +99,7 @@ function AccountRow({ account }: { account: RedditAccountSummary }) { const removeMutation = useRemoveRedditAccountMutation(); const [confirmRemove, setConfirmRemove] = useState(false); const isRemoved = account.status === 'removed'; + const isSuspended = account.status === 'suspended'; useEffect(() => { if (!confirmRemove) return; @@ -106,7 +107,10 @@ function AccountRow({ account }: { account: RedditAccountSummary }) { return () => globalThis.clearTimeout(timer); }, [confirmRemove]); - const rowClass = isRemoved ? 'account-row is-removed' : account.isRunning ? 'account-row is-active' : 'account-row'; + const rowClass = isRemoved + ? 'account-row is-removed' + : isSuspended ? 'account-row is-suspended' + : account.isRunning ? 'account-row is-active' : 'account-row'; return (
@@ -114,13 +118,15 @@ function AccountRow({ account }: { account: RedditAccountSummary }) { {isRemoved ? 'Removed — its conversations and media are kept, read-only' + : isSuspended + ? 'Suspended by Reddit — all bot work is paused' : account.isRunning ? 'Running independently' : 'Waiting for a refreshed session'}
- {isRemoved ? null : establishmentPill(account.establishment)} + {isRemoved || isSuspended ? null : establishmentPill(account.establishment)} - {isRemoved ? 'Removed' : account.isRunning ? 'Running' : 'Paused'} + {isRemoved ? 'Removed' : isSuspended ? 'Suspended' : account.isRunning ? 'Running' : 'Paused'}
@@ -162,6 +168,12 @@ function AccountRow({ account }: { account: RedditAccountSummary }) { {!isRemoved && account.establishment && account.establishment.state !== 'unknown' ? ( ) : null} + {isSuspended ? ( +
+ Account suspended + Reddit or Matrix confirmed this account is suspended. It cannot send, scan, or sync until you review it and import a newly verified session. +
+ ) : null}
); } @@ -213,7 +225,8 @@ export function AccountsPage() { const accounts = accountsQuery.data ?? []; // Removed accounts are not paused work — nothing about them needs attention. const liveAccounts = accounts.filter((account) => account.status !== 'removed'); - const pausedCount = liveAccounts.filter((account) => !account.isRunning).length; + const suspendedAccounts = liveAccounts.filter((account) => account.status === 'suspended'); + const pausedCount = liveAccounts.filter((account) => !account.isRunning && account.status !== 'suspended').length; const heldAccounts = liveAccounts.filter((account) => account.establishment?.sendingHeld); return ( @@ -240,6 +253,17 @@ export function AccountsPage() {
) : null} + {suspendedAccounts.length > 0 ? ( +
+ + {suspendedAccounts.length === 1 + ? `u/${suspendedAccounts[0].redditUsername} is suspended by Reddit.` + : `${suspendedAccounts.length} accounts are suspended by Reddit.`} + + All work is paused for these accounts. Review the suspension before importing any new session. +
+ ) : null} + {heldAccounts.length > 0 ? (
diff --git a/apps/dashboard/src/features/accounts/accounts.css b/apps/dashboard/src/features/accounts/accounts.css index 83d004f..25f5dac 100644 --- a/apps/dashboard/src/features/accounts/accounts.css +++ b/apps/dashboard/src/features/accounts/accounts.css @@ -6,12 +6,15 @@ .account-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; padding: .85rem; background: var(--surface-raised); border: 1px solid var(--border); border-radius: .75rem; } .account-row.is-active { border-color: var(--accent); } .account-row.is-removed { opacity: .72; } +.account-row.is-suspended { border-color: var(--danger-border); } .account-row > div { display: grid; gap: .2rem; min-width: 0; } .account-row span { color: var(--muted); font-size: .82rem; } .account-row .pill { font-size: .7rem; } .account-row > .account-row-pills { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; } .account-row > .account-row-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: .4rem; margin-left: auto; } .account-row .account-remove-warning { flex-basis: 100%; text-align: right; color: var(--danger-text, var(--muted)); font-size: .78rem; } +.account-suspension { flex-basis: 100%; display: grid; gap: .25rem; padding-top: .7rem; color: var(--danger-text); border-top: 1px solid var(--danger-border); } +.account-suspension span { color: inherit; } /* Establishment evidence sits on its own row beneath the account summary. */ .account-establishment { flex-basis: 100%; display: grid; gap: .4rem; padding-top: .7rem; border-top: 1px solid var(--border); } diff --git a/apps/dashboard/src/features/attention/attention-page.tsx b/apps/dashboard/src/features/attention/attention-page.tsx index 430e09b..0c29598 100644 --- a/apps/dashboard/src/features/attention/attention-page.tsx +++ b/apps/dashboard/src/features/attention/attention-page.tsx @@ -18,6 +18,8 @@ function actionHint(kind: AttentionItem['kind']): string { switch (kind) { case 'account-login-required': return 'No linked conversation — check Settings.'; + case 'account-suspended': + return 'No linked conversation — review the account suspension.'; case 'account-unestablished': return 'No linked conversation — check Accounts.'; case 'runtime-failed': diff --git a/apps/dashboard/src/features/overview/overview-page.tsx b/apps/dashboard/src/features/overview/overview-page.tsx index 95d1caa..4e88d94 100644 --- a/apps/dashboard/src/features/overview/overview-page.tsx +++ b/apps/dashboard/src/features/overview/overview-page.tsx @@ -95,6 +95,7 @@ const sendingStateCopy: Record { expect(screen.getByText('Waiting for a refreshed session')).toBeInTheDocument(); }); + it('shows a confirmed Reddit suspension separately from a refreshable session pause', async () => { + server.use(http.get('/api/accounts', () => HttpResponse.json([ + { ...account, isRunning: false, status: 'suspended' }, + ]))); + renderAccounts(); + + expect(await screen.findByText('u/example-account is suspended by Reddit.')).toBeInTheDocument(); + expect(screen.getAllByText('Account suspended')).toHaveLength(1); + expect(screen.getByText(/cannot send, scan, or sync/i)).toBeInTheDocument(); + expect(screen.queryByText('One account is paused.')).not.toBeInTheDocument(); + }); + it('explains a held account with what Reddit actually said, plus the profile evidence', async () => { server.use(http.get('/api/accounts', () => HttpResponse.json([{ ...account, establishment }]))); renderAccounts();