Add account browsing simulation

This commit is contained in:
jjbrunton committed 2026-08-11 21:40:31 +01:00
1 parent e2d7dc4b82
commit 27012399ec
31 files changed
+1476 -28

No files matched your search

@@ -5,6 +5,7 @@ import {
GrowthOverviewResponseSchema,
type GrowthAccountDetail,
type GrowthAccountSummary,
type BrowsingSettingsUpdateRequest,
type GrowthSettingsUpdateRequest,
type GrowthSubredditCreateRequest,
type GrowthSubredditUpdateRequest,
@@ -50,6 +51,19 @@ export function updateGrowthSettings(
.then((payload) => payload.account);
}
export function updateBrowsingSettings(
accountId: string,
update: BrowsingSettingsUpdateRequest,
): Promise<GrowthAccountDetail> {
return dashboardRequest(`${accountPath(accountId)}/browsing`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(update),
})
.then((response) => parseResponse(response, GrowthAccountResponseSchema))
.then((payload) => payload.account);
}
export function pauseGrowth(accountId: string, reason?: string): Promise<GrowthAccountDetail> {
return dashboardRequest(`${accountPath(accountId)}/pause`, {
method: 'POST',
@@ -4,6 +4,7 @@ import type {
GrowthAccountSummary,
GrowthComment,
GrowthSubreddit,
BrowsingDecision,
} from '../../../../../packages/api-contracts/src/index.js';
import { NumberField, SwitchField, TextField } from '../../components/form-fields.js';
import { RedditUsernameLink } from '../../components/reddit-username-link.js';
@@ -15,6 +16,7 @@ import {
useRemoveGrowthSubredditMutation,
useResumeGrowthMutation,
useUpdateGrowthSettingsMutation,
useUpdateBrowsingSettingsMutation,
useUpdateGrowthSubredditMutation,
} from './growth-queries.js';
import './growth.css';
@@ -170,6 +172,68 @@ function SubredditRow({
);
}
function BrowsingSimulationSection({ account }: { account: GrowthAccountDetail }) {
const update = useUpdateBrowsingSettingsMutation(account.accountId);
const { settings, stats, todayTarget, recentDecisions } = account.browsing;
const selected = recentDecisions.filter((decision) => decision.decision === 'simulated-upvote');
return (
<section aria-labelledby="browsing-simulation-heading" className="panel settings-card">
<div className="settings-card-heading">
<div>
<span className="eyebrow">Simulation only</span>
<h2 id="browsing-simulation-heading">Browsing simulation</h2>
<p>Tests selection and pacing without submitting votes or any other Reddit write.</p>
</div>
</div>
<div className="settings-card-body">
<div className="state-banner is-warning" role="status">
<strong>No Reddit votes were submitted.</strong>
<span>Every item below is a hypothetical upvote recorded only in RedditBot.</span>
</div>
<SwitchField
checked={settings.enabled}
description={`Selects ${settings.dailyTargetMin}–${settings.dailyTargetMax} eligible posts or comments during waking hours.`}
label="Simulate browsing selections"
onChange={(enabled) => update.mutate({ enabled })}
/>
<dl className="growth-metrics">
<div><dt>Selected today</dt><dd>{stats.selected} / {todayTarget}</dd></div>
<div><dt>Posts</dt><dd>{stats.posts}</dd></div>
<div><dt>Comments</dt><dd>{stats.comments}</dd></div>
<div><dt>Rejected</dt><dd>{stats.rejected}</dd></div>
<div><dt>Skipped</dt><dd>{stats.skipped}</dd></div>
<div>
<dt>Next session</dt>
<dd>{settings.nextDueAt === null ? 'Not scheduled' : new Date(settings.nextDueAt).toLocaleString()}</dd>
</div>
</dl>
{update.isError ? <p className="save-error" role="alert">{errorMessage(update.error)}</p> : null}
{selected.length === 0 ? (
<div className="empty-state">
<strong>No simulated selections yet</strong>
<span>The first session is durably scheduled after this feature is enabled.</span>
</div>
) : (
<ul className="growth-comment-list">
{selected.map((decision: BrowsingDecision) => (
<li className="growth-comment" key={`${decision.thingFullname}-${decision.decidedAt}`}>
<div className="growth-comment-head">
<a href={`https://www.reddit.com${decision.permalink}`} rel="noreferrer noopener" target="_blank">
r/{decision.subreddit} — {decision.thingKind} by u/{decision.author}
</a>
<span className="pill neutral">Simulated upvote</span>
</div>
<p className="field-hint">{new Date(decision.decidedAt).toLocaleString()}</p>
</li>
))}
</ul>
)}
</div>
</section>
);
}
function AccountDetail({ account }: { account: GrowthAccountDetail }) {
const { accountId, settings, stats } = account;
const updateSettings = useUpdateGrowthSettingsMutation(accountId);
@@ -435,6 +499,8 @@ function AccountDetail({ account }: { account: GrowthAccountDetail }) {
</div>
</section>
<BrowsingSimulationSection account={account} />
<section aria-labelledby="growth-history-heading" className="panel settings-card">
<div className="settings-card-heading">
<div>
@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import type {
GrowthAccountDetail,
BrowsingSettingsUpdateRequest,
GrowthSettingsUpdateRequest,
GrowthSubredditCreateRequest,
GrowthSubredditUpdateRequest,
@@ -13,6 +14,7 @@ import {
removeGrowthSubreddit,
resumeGrowth,
updateGrowthSettings,
updateBrowsingSettings,
updateGrowthSubreddit,
} from './growth-api.js';
@@ -63,6 +65,13 @@ export function useUpdateGrowthSettingsMutation(accountId: string) {
);
}
export function useUpdateBrowsingSettingsMutation(accountId: string) {
return useGrowthMutation(
accountId,
(update: BrowsingSettingsUpdateRequest) => updateBrowsingSettings(accountId, update),
);
}
export function usePauseGrowthMutation(accountId: string) {
return useGrowthMutation(accountId, (reason?: string) => pauseGrowth(accountId, reason));
}