import { useEffect, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { dashboardQueryKeys, useSetModeMutation, useStatsQuery, useTimersQuery } from '../../api/queries.js'; import type { BotMode, BotStats, Timers } from '../../api/contracts.js'; import type { AccountStatus } from '../../../../../packages/api-contracts/src/index.js'; import { ConfirmationDialog } from '../../components/confirmation-dialog.js'; import { RedditUserText } from '../../components/reddit-user-text.js'; import { OriginPostButton } from '../../components/origin-post.js'; import { RecentMessagesPanel } from './recent-messages.js'; import { AccountOwnerBadge } from '../../components/account-owner-badge.js'; const MODE_ORDER: readonly BotMode[] = ['running', 'scan', 'stopped']; const modeCopy: Record = { running: { label: 'Running', action: 'Start bot', dot: 'running', buttonTone: 'button primary' }, scan: { label: 'Scanning', action: 'Scan mode', dot: 'scanning', buttonTone: 'button secondary' }, stopped: { label: 'Stopped', action: 'Stop bot', dot: 'stopped', buttonTone: 'button danger' }, }; function errorMessage(error: Error | null): string { return error?.message ?? 'Something went wrong.'; } function formatTime(timestamp: number): string { return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(timestamp); } function formatMetricValue(value: number): string { return value.toLocaleString(); } function formatCountdown(remainingMs: number): string { const totalSeconds = Math.max(0, Math.ceil(remainingMs / 1_000)); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; } /** Ticks a client clock so time-derived UI flips without a refetch. */ function useNow(intervalMs = 1_000): number { const [now, setNow] = useState(() => Date.now()); useEffect(() => { const timer = globalThis.setInterval(() => setNow(Date.now()), intervalMs); return () => globalThis.clearInterval(timer); }, [intervalMs]); return now; } function MetricCard({ label, value, detail, metric }: { label: string; value: string | number; detail: string; metric: string }) { return (
{label} {value} {detail}
); } function TimersPanel({ timers }: { timers: Timers }) { const now = useNow(); const isReady = timers.nextMessageAvailableAt === null || timers.nextMessageAvailableAt <= now; return (
Rate limits

Sending window

{isReady ? 'Ready' : 'Cooling down'}
Next message
{isReady ? 'Available now' : `Ready in ${formatCountdown((timers.nextMessageAvailableAt ?? now) - now)} (${formatTime(timers.nextMessageAvailableAt ?? now)})`}
Hourly allowance
{timers.messagesRemaining} messages remaining
Allowance resets
{formatTime(timers.hourlyResetAt)}
); } const sendingStateCopy: Record = { ready: { label: 'Ready', tone: 'success', detail: 'Available for the next outbound candidate.' }, 'bot-stopped': { label: 'Bot stopped', tone: 'neutral', detail: 'Global sending is stopped.' }, 'scan-only': { label: 'Scan only', tone: 'neutral', detail: 'Conversations stay in sync, but nothing is sent.' }, 'runtime-stopped': { label: 'Runtime stopped', tone: 'warning', detail: 'This account runtime is not running.' }, 'runtime-failed': { label: 'Runtime failed', tone: 'warning', detail: 'This account runtime needs attention.' }, 'login-required': { label: 'Login required', tone: 'warning', detail: 'Refresh this account session before it can send.' }, 'matrix-cooldown': { label: 'Matrix cooldown', tone: 'warning', detail: 'Only this account is paused by Reddit chat throttling.' }, 'replies-prioritized': { label: 'Replies first', tone: 'neutral', detail: 'Inbound replies are being handled before new outreach.' }, pacing: { label: 'Pacing', tone: 'neutral', detail: 'Waiting for the account sending window.' }, unestablished: { label: 'Not established', tone: 'warning', detail: 'Reddit will not accept chat invitations from this account yet.' }, }; function AccountStatusPanel({ accounts }: { accounts: readonly AccountStatus[] }) { return (
Multi-account delivery

Account status

{accounts.length} configured
{accounts.length === 0 ?
No accounts configured.Add an account under Settings → Access.
: (
{accounts.map((account) => { const state = sendingStateCopy[account.sendingState]; return (
{state.label}

{state.detail}

Runtime
{account.runtimeState}
Authentication
{account.authenticated ? 'Authenticated' : 'Unavailable'}
Hourly usage
{account.messagesThisHour} of {account.maxMessagesPerHour}
Remaining
{account.messagesRemaining}
Allowance resets
{formatTime(account.hourlyResetAt)}
Pending replies
{account.actionableReplies}
{account.nextMessageAvailableAt && account.nextMessageAvailableAt > Date.now() && !account.invitationCooldown ?

Next send: {formatTime(account.nextMessageAvailableAt)}

: null} {account.invitationCooldown ? (
Cooldown until {formatTime(account.invitationCooldown.until)} {account.invitationCooldown.reason} {account.invitationCooldown.redditCode ? {account.invitationCooldown.redditCode} : null}
) : null} {account.failure ?
Runtime failure{account.failure}
: null}
); })}
)}
); } function ActivityPanel({ stats }: { stats: BotStats }) { return (
Latest signals

Recent activity

{stats.recentActivity.length === 0 ? (
No recent activity yet. New bot events will appear here.
) : (
    {stats.recentActivity.map((item, index) => (
  1. ))}
)}
); } export function OverviewPage() { const queryClient = useQueryClient(); const statsQuery = useStatsQuery(); const timersQuery = useTimersQuery(); const controlMutation = useSetModeMutation(); const [confirmationAction, setConfirmationAction] = useState(null); const isRefreshing = (statsQuery.isFetching || timersQuery.isFetching) && !statsQuery.isPending; async function refreshOverview() { await Promise.all([ queryClient.invalidateQueries({ queryKey: dashboardQueryKeys.stats }), queryClient.invalidateQueries({ queryKey: dashboardQueryKeys.timers }), ]); } function confirmControl() { if (!confirmationAction) return; controlMutation.mutate(confirmationAction, { onSuccess: () => setConfirmationAction(null), }); } if (statsQuery.isPending) { if (statsQuery.isPaused) { return (
You appear to be offline The overview will load once the connection returns.
); } return
Loading overview…
; } if (statsQuery.isError) { return (
Overview unavailable {errorMessage(statsQuery.error)}
); } const stats = statsQuery.data; // Offer the two modes the bot is not currently in, in a stable order. const offeredModes = MODE_ORDER.filter((mode) => mode !== stats.mode); return (
Control room

Overview

System health, sending limits, and the signals that need attention.

{offeredModes.map((mode) => ( ))}
{stats.dryRunMode ? Dry run : null} {stats.messagesThisHour} of {stats.maxMessagesPerHour} messages used this hour
{stats.invitationCooldown ? (
{stats.invitationCooldown.redditUsername ? `u/${stats.invitationCooldown.redditUsername} invitation cooldown` : 'Reddit invitation cooldown'} {stats.invitationCooldown.reason}
) : null}
{timersQuery.isPending ? (
Loading sending window…
) : timersQuery.isError ? (
Sending window unavailable {errorMessage(timersQuery.error)}
) : ( )}
{confirmationAction ? ( { setConfirmationAction(null); controlMutation.reset(); }} onConfirm={confirmControl} /> ) : null}
); }