Missing conversations: recipient Reddit accounts that are deleted or suspended left conversations unaccounted for. A new RecipientAccountStatusService polls Reddit for the users behind cached conversations and records their account status durably, with progressive backfill and delta-media fixes in ChatService so history and images are no longer dropped mid-sync. Media: attachMedia now surfaces download failures as MediaAttachmentError so the sync cursor can be retried instead of silently losing an image, recognizes image bytes when Matrix returns a generic content type, and keeps separate gallery occurrences when the same asset appears in more than one event. Manual inbox tab: the conversations list now carries the reply-loop status, so the inbox can separate conversations the operator is driving by hand from the ones automation is still handling. A Manual filter pill and a per-row chip explain why each conversation was handed over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.5 KiB
Dashboard architecture
Stack and boundary
The operator console is a React 19 + TypeScript + Vite application under apps/dashboard. Express serves its production build, private media, API, authentication endpoints, and SSE streams from the same origin. TanStack Query owns server state; React Router owns navigation and route-addressable state.
Shared HTTP/SSE schemas live in packages/api-contracts. The server and client should not duplicate transport interfaces by hand.
Routes
| Route | Responsibility |
|---|---|
/ |
Health, per-account delivery/cooldown state, timers, workload, and recent activity. |
/attention |
Human handoffs, delivery failures, account risks, and other actionable items. |
/outreach |
Durable candidate funnel and active candidate detail. |
/conversations |
Searchable account-aware conversation inbox. |
/conversations/:conversationId |
Conversation history, context, suggestions, and guarded sending. |
/media |
Authenticated, privacy-conscious media gallery and sync controls. |
/media-library |
Upload, classify, correct, enable, preview, and delete shared exchange images. |
/performance |
Mature outcome trends, strategy/subreddit results, and model usage. |
/operations |
Background jobs, sync state, events, rate limits, and technical logs. |
/settings |
Validated runtime configuration and account/session management. |
Navigation is declared in app/navigation.ts; route elements are composed in app/router.tsx.
State model
- Remote state: TanStack Query snapshots parsed by shared Zod schemas.
- Route/filter/selection state: URL/search params when it should survive navigation or be shareable.
- Ephemeral UI state: component/provider state, such as dialogs and notification presentation.
- Persistent presentation state: guarded browser storage only when explicitly useful (for example notification history).
Do not introduce a general global state store unless a demonstrated cross-feature need cannot fit these boundaries.
HTTP and contracts
api/request.ts owns authenticated request mechanics and failure conversion. api/client.ts/api/queries.ts expose typed calls and query keys. Parse every response at the boundary. Strict schemas are deliberate: when the server adds a field, update the shared contract and its consumers in the same change.
Mutations should display the confirmed server result. Start/stop, config, sync, import, and send actions must not use optimistic success for operational state.
Account delivery state is reported per runtime (ready, stopped/failed/login-required, Matrix cooldown, replies prioritized, or pacing). Preserve account attribution when presenting cooldowns and failures; a blocked account does not imply every account is blocked.
Realtime model
RealtimeProvider connects to /api/events/stream. Event IDs support bounded replay/resume. Each event family either updates a narrowly safe cache value or invalidates the relevant HTTP snapshot. Reconnect and stale detection live in the app layer, not individual feature pages.
Rules:
- HTTP remains authoritative.
- Duplicate/replayed events must be harmless.
- A disconnected stream marks data stale but does not blank useful cached content.
- Invalid event payloads are contained and repaired with a snapshot refresh.
- User-triggered outcomes may create toasts; durable/history-worthy events also appear in the notification center/activity views.
- Raw technical logs belong in Operations, not as general user notifications.
Authentication and privacy
The login form exchanges the administrator token for an HttpOnly, SameSite=Strict cookie. API clients may use Bearer tokens. Express applies auth before static dashboard assets, APIs, SSE, and /media; /healthz is the intentional public exception.
Media may be private conversation content. Preserve authenticated URLs, blur/privacy defaults, explicit reveal behavior, safe new-tab links, and no token leakage into query strings or local storage. A conversation's source-post thumbnail becomes a keyboard-accessible full-screen preview only after the operator reveals it; the preview closes by button, backdrop, or Escape and restores focus to its trigger.
Exchange-library uploads require an explicit consent checkbox before the raw image request is enabled. Previews remain blurred by default. The management card exposes AI tags, safety level, confidence/review state, enablement, and a two-step delete action. Its progression rail mirrors the actual nonspecific-request selection tiers rather than implying that explicit anatomy requests follow the same sequence.
Conversation reply composers include a collapsed saved-image picker. It lists only server-approved assets for the latest inbound request, keeps thumbnails blurred until revealed, and requires selection plus a dedicated confirmation dialog before sending. The server remains authoritative and may reject a choice if the conversation changes before the rate-limited send executes.
The conversation inbox has a system-derived Account deleted bucket alongside the operator-maintained open, snoozed, and done queues. A deleted recipient is excluded from the default, unread, needs-reply, waiting, manual, and image buckets and is labelled in both the list and detail views. Suspended profiles are not treated as deleted.
Feature structure
Each route is a slice under features/<name>/ with page/route components, local styles, and feature-specific helpers. Cross-feature visual/domain components live under components/; application lifecycle belongs under app/; transport belongs under api/.
Keep feature components focused:
- pages compose queries and sections;
- presentational components render typed props;
- mutation hooks own cache invalidation;
- parsing/formatting lives in small pure helpers;
- destructive or external-message actions require explicit confirmation and accessible focus restoration.
Accessibility and presentation
- All actions must be keyboard accessible and have visible focus.
- Dialogs label purpose, trap/restore focus, and distinguish cancel from destructive confirmation.
- Status must not rely on color alone.
- Relative times include an exact timestamp where operationally useful.
- Lists are explicitly sorted after filters rather than relying on backend/incidental order.
- Loading, empty, stale, partial, and error states are distinct.
- Show sample sizes and low-confidence context for performance metrics.
Testing
Dashboard behavior is tested with Vitest, jsdom, Testing Library, and controlled HTTP boundaries. Focus on observable behavior:
- representative payloads parse and malformed payloads fail;
- route/search state and navigation;
- loading/error/empty/stale views;
- mutation confirmation and server read-back;
- SSE ordering, replay, reconnect, duplicate handling, and snapshot repair;
- authentication/private media boundaries;
- keyboard/focus behavior for dialogs and controls.
Run focused npx vitest run tests/dashboard/<file>, then npm run dashboard:typecheck, npm run dashboard:build, and the full suite before publishing a frontend change.
Adding a dashboard capability
- Define/extend the shared Zod contract.
- Make the server route return that contract and add a route test.
- Add a typed client/query key.
- Build the feature slice and observable UI states.
- Map relevant realtime events to cache updates/invalidations.
- Add component/realtime/contract coverage.
- Update
api.mdwhen the operation catalog changed and update this or the relevant product/flow document.