Files
RedditBot/docs/frontend.md
T
Jamie BruntonandClaude Opus 5 ac12a3146a feat: recover missing conversations, fix media attachment, add manual inbox tab
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>
2026-08-10 11:03:33 +01:00

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

  1. Define/extend the shared Zod contract.
  2. Make the server route return that contract and add a route test.
  3. Add a typed client/query key.
  4. Build the feature slice and observable UI states.
  5. Map relevant realtime events to cache updates/invalidations.
  6. Add component/realtime/contract coverage.
  7. Update api.md when the operation catalog changed and update this or the relevant product/flow document.