Expose browsing daily target range
This commit is contained in:
1 parent
e890f74478
commit
a71ad56f3c
4 files changed
+83
-2
No files matched your search
@@ -178,12 +178,27 @@ function BrowsingSimulationSection({ account }: { account: GrowthAccountDetail }
|
||||
const runNow = useRunBrowsingSimulationMutation(account.accountId);
|
||||
const { settings, stats, todayTarget, recentDecisions } = account.browsing;
|
||||
const selected = recentDecisions.filter((decision) => decision.decision === 'simulated-upvote');
|
||||
const [dailyMinimum, setDailyMinimum] = useState(String(settings.dailyTargetMin));
|
||||
const [dailyMaximum, setDailyMaximum] = useState(String(settings.dailyTargetMax));
|
||||
const [gapMinutes, setGapMinutes] = useState(String(settings.minimumGapMs / 60_000));
|
||||
const [selectionsPerSession, setSelectionsPerSession] = useState(String(settings.selectionsPerSession));
|
||||
|
||||
useEffect(() => setDailyMinimum(String(settings.dailyTargetMin)), [settings.dailyTargetMin]);
|
||||
useEffect(() => setDailyMaximum(String(settings.dailyTargetMax)), [settings.dailyTargetMax]);
|
||||
useEffect(() => setGapMinutes(String(settings.minimumGapMs / 60_000)), [settings.minimumGapMs]);
|
||||
useEffect(() => setSelectionsPerSession(String(settings.selectionsPerSession)), [settings.selectionsPerSession]);
|
||||
|
||||
const parsedDailyMinimum = Number.parseInt(dailyMinimum, 10);
|
||||
const parsedDailyMaximum = Number.parseInt(dailyMaximum, 10);
|
||||
const dailyRangeValid = Number.isInteger(parsedDailyMinimum)
|
||||
&& Number.isInteger(parsedDailyMaximum)
|
||||
&& parsedDailyMinimum >= 1
|
||||
&& parsedDailyMaximum <= 12
|
||||
&& parsedDailyMinimum <= parsedDailyMaximum;
|
||||
const dailyRangeError = dailyMinimum !== '' && dailyMaximum !== '' && !dailyRangeValid
|
||||
? 'Use a range from 1 to 12, with the minimum no greater than the maximum.'
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<section aria-labelledby="browsing-simulation-heading" className="panel settings-card">
|
||||
<div className="settings-card-heading">
|
||||
@@ -212,6 +227,40 @@ function BrowsingSimulationSection({ account }: { account: GrowthAccountDetail }
|
||||
label="Submit selected upvotes"
|
||||
onChange={(autoVoteEnabled) => update.mutate({ autoVoteEnabled })}
|
||||
/>
|
||||
<div className="growth-pacing">
|
||||
<NumberField
|
||||
error={dailyRangeError}
|
||||
hint="The system chooses one stable target inside this range for each account and local day."
|
||||
label="Daily minimum"
|
||||
min={1}
|
||||
max={12}
|
||||
name="browsing-daily-minimum"
|
||||
onChange={setDailyMinimum}
|
||||
suffix="/day"
|
||||
value={dailyMinimum}
|
||||
/>
|
||||
<NumberField
|
||||
error={dailyRangeError}
|
||||
label="Daily maximum"
|
||||
min={1}
|
||||
max={12}
|
||||
name="browsing-daily-maximum"
|
||||
onChange={setDailyMaximum}
|
||||
suffix="/day"
|
||||
value={dailyMaximum}
|
||||
/>
|
||||
<button
|
||||
className="button secondary"
|
||||
disabled={update.isPending || !dailyRangeValid}
|
||||
onClick={() => update.mutate({
|
||||
dailyTargetMin: parsedDailyMinimum,
|
||||
dailyTargetMax: parsedDailyMaximum,
|
||||
})}
|
||||
type="button"
|
||||
>
|
||||
Save daily range
|
||||
</button>
|
||||
</div>
|
||||
<div className="growth-pacing">
|
||||
<NumberField
|
||||
hint={`Shortest gap between sessions. The system adds a random 0–${minutes(settings.jitterMs)} on top.`}
|
||||
|
||||
@@ -105,7 +105,7 @@ A per-community `dailyCap` only distributes the account allowance; caps are not
|
||||
|
||||
Community weights feed a new weighted-random pick for each discovered growth thread. The grower queues only one new thread at a time, drains due retries and existing queue work before polling another community, and exposes an account-scoped queue clear that removes only unposted discovered/retryable rows.
|
||||
|
||||
Browsing sessions are configured separately per account through `/api/growth/{accountId}/browsing` and reuse the enabled growth-community list and weights. Sessions default off and target 3–6 selections per local day with a 60/40 post/comment split, respect quiet hours, exclude NSFW unless enabled, and have a non-overridable 12/day ceiling. The operator chooses 1–12 selections per session and a minimum gap of at least 45 minutes; the service adds a random delay of up to `jitterMs` on top before persisting the next due time. A scheduled session is capped by the remaining daily target, while a manually requested session bypasses that target but remains capped by the remaining hard daily ceiling. `autoVoteEnabled` is a separate, default-off operator setting; it can submit selected upvotes only in running mode when browser dry-run is off. `nextDueAt` is service-owned durable state and cannot be set through the API.
|
||||
Browsing sessions are configured separately per account through `/api/growth/{accountId}/browsing` and reuse the enabled growth-community list and weights. Sessions default off with a configurable 3–6 selections-per-local-day range and a 60/40 post/comment split, respect quiet hours, exclude NSFW unless enabled, and have a non-overridable 12/day ceiling. The service derives one stable target inside the configured daily range for each account and local day. The operator also chooses 1–12 selections per session and a minimum gap of at least 45 minutes; the service adds a random delay of up to `jitterMs` on top before persisting the next due time. A scheduled session is capped by the remaining daily target, while a manually requested session bypasses that target but remains capped by the remaining hard daily ceiling. `autoVoteEnabled` is a separate, default-off operator setting; it can submit selected upvotes only in running mode when browser dry-run is off. `nextDueAt` is service-owned durable state and cannot be set through the API.
|
||||
|
||||
## Runtime-editable subset
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ Navigation is declared in `app/navigation.ts`; route elements are composed in `a
|
||||
|
||||
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 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. The displayed minimum 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, subject to the hard daily ceiling; it bypasses the schedule and follows the same automatic-upvote setting. Enabling browsing does not enable comment growth or bot processing.
|
||||
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. 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, subject to the hard daily ceiling; 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.
|
||||
|
||||
|
||||
@@ -287,6 +287,38 @@ describe('GrowthPage', () => {
|
||||
])));
|
||||
});
|
||||
|
||||
it('updates the browsing daily target range together', async () => {
|
||||
renderGrowth();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const minimum = await screen.findByLabelText('Daily minimum');
|
||||
await user.clear(minimum);
|
||||
await user.type(minimum, '6');
|
||||
const maximum = screen.getByLabelText('Daily maximum');
|
||||
await user.clear(maximum);
|
||||
await user.type(maximum, '9');
|
||||
await user.click(screen.getByRole('button', { name: 'Save daily range' }));
|
||||
|
||||
await waitFor(() => expect(requests).toContainEqual(expect.objectContaining({
|
||||
method: 'PATCH', body: { dailyTargetMin: 6, dailyTargetMax: 9 },
|
||||
})));
|
||||
});
|
||||
|
||||
it('rejects an inverted browsing daily target range in the dashboard', async () => {
|
||||
renderGrowth();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const minimum = await screen.findByLabelText('Daily minimum');
|
||||
await user.clear(minimum);
|
||||
await user.type(minimum, '8');
|
||||
const maximum = screen.getByLabelText('Daily maximum');
|
||||
await user.clear(maximum);
|
||||
await user.type(maximum, '4');
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Save daily range' })).toBeDisabled();
|
||||
expect(screen.getAllByText(/minimum no greater than the maximum/i)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('shows an empty state when there are no accounts', async () => {
|
||||
overviewPayload = [];
|
||||
renderGrowth();
|
||||
|
||||
Reference in new issue
Block a user