133 lines
13 KiB
Markdown
133 lines
13 KiB
Markdown
# 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. |
|
||
| `/growth` | Per-account comment growth plus browsing sessions: switches, communities, pacing/allowance, next session, optional automatic-upvote control, and both audit trails. |
|
||
| `/settings` | Validated runtime configuration and account/session management. |
|
||
|
||
Navigation is declared in `app/navigation.ts`; route elements are composed in `app/router.tsx`.
|
||
|
||
The subreddit editor chooses both a source type and recipient: image sources can target the post author or high-confidence qualifying commenters, while chat/text sources always target the post author. Comment-targeted rows carry a visible `Commenters` badge. In the outreach funnel, commenter candidates are labelled separately from image-post authors and show the qualifying comment beside the prepared opener so the operator can verify that the message responds to the comment rather than attributing the source image to its author.
|
||
|
||
The account-growth page states plainly that comments are AI-written and posted without review, shows the adaptive allowance and trust level rather than a settable rate, and surfaces every posted comment's exact text alongside its score and removal state. A paused account replaces the pause control with a resume control and explains why it stopped, because resuming is the one action that can compound the damage.
|
||
|
||
Its browsing section separates session scheduling from the default-off automatic-upvote switch, lets the operator set the daily target range, minimum gap, and selections per session, and shows whether each selection was submitted or simulated, today's deterministic target, post/comment split, exclusion counts, durable next session, and recent selections with Reddit permalinks. Daily and per-session counts have no product-imposed upper ceiling. The daily range is saved atomically; changing it recalculates today's target, which then remains stable for that range and local day. The displayed minimum gap is followed by bounded random jitter. Scan mode and browser dry-run remain simulation-only. An enabled, running account can also run one immediate session at the configured size; it bypasses the schedule and follows the same automatic-upvote setting. Enabling browsing does not enable comment growth or bot processing.
|
||
|
||
The Monitoring settings section owns the optional physique-targeting toggle and inclusive 1–10 range. It explains that the signal is subjective, applies only to image posts, and screens out unscorable images while enabled.
|
||
|
||
Prepared cards in the authenticated outreach funnel expose the exact opener, grounded image analysis, attractiveness and physique scores, current target range, detected gender, strategy/tone/model metadata, and original source image. The live range is returned through the existing preparation field so cached dashboard bundles remain compatible during a rolling deployment. The durable queue still owns the historical preparation snapshot used for reconciliation. The source image uses the shared `OriginPostCard` privacy behavior: blurred initially, first click reveals it, and a later click opens the full-screen preview. Candidates from older persisted snapshots degrade cleanly when newer optional metadata is absent.
|
||
|
||
The funnel header also offers a destructive, confirmed **Clear funnel** action. Its dialog names the number of cancellable candidates, explains that audit rows remain and running discovery can refill the queue, and separately identifies any in-flight sends that cannot safely be cancelled. Success is reported from the authoritative API response before the funnel query refreshes.
|
||
|
||
Each non-sending candidate has a confirmed **Remove** action. Prepared candidates in ready or retrying state also have **Edit message**: the modal validates the 1–2000 character limit, the server re-applies outbound safeguards, and a successful edit is shown as operator-authored attribution. Buttons are intentionally absent once the candidate is sending, while the server still enforces the state boundary against stale pages.
|
||
|
||
## 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.
|
||
- The notification centre and sticky toasts are reserved for inbound replies/media and attention-worthy failures or runtime/account states. Routine sync completions, successful sends, healthy lifecycle changes, and raw logs remain visible in their owning screens instead of creating notifications. Toasts remain visible until the operator dismisses them.
|
||
- 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 composer also has a separate green-screen creation panel. Its source picker uses consent-controlled exchange-library assets; its target picker is derived server-side from the triggering post image and the attachments already loaded into that conversation. Source, target, and generated preview are blurred independently. Generation returns an authenticated in-memory JPEG preview/download and never adds it to the library or sends it to Matrix, so delivery remains a separate explicit operator action.
|
||
|
||
The conversation inbox assigns every row to exactly one primary workflow queue: **Needs attention**, **Waiting**, **Sellers**, **Snoozed**, **Done**, **Deleted**, or **Archived**. Removed-account history belongs only to Archived, deleted recipients belong only to Deleted, and seller/off-platform outcomes belong only to Sellers, so none can leak into actionable work. Unread state, manual ownership, and received images are independent badges and secondary toggles rather than competing queues. The default queue is Needs attention. 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.
|
||
- The fixed primary sidebar is its own vertical scroll region so every route and the platform controls remain reachable on short or low-resolution screens.
|
||
- 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.
|
||
- `/learning` shows human/autonomous credit totals, confidence, maturity and review state. Operators can include or exclude non-invalidated trajectories; raw private messages and media are intentionally absent.
|
||
Learning uses `GET /api/learning` and `PATCH /api/learning/trajectories/:id`. Shared dashboard schemas live in `packages/api-contracts/src/learning.ts`; the server catalogue mirrors the boundary schemas for generated OpenAPI documentation.
|