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 runNow = useRunBrowsingSimulationMutation(account.accountId);
|
||||||
const { settings, stats, todayTarget, recentDecisions } = account.browsing;
|
const { settings, stats, todayTarget, recentDecisions } = account.browsing;
|
||||||
const selected = recentDecisions.filter((decision) => decision.decision === 'simulated-upvote');
|
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 [gapMinutes, setGapMinutes] = useState(String(settings.minimumGapMs / 60_000));
|
||||||
const [selectionsPerSession, setSelectionsPerSession] = useState(String(settings.selectionsPerSession));
|
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(() => setGapMinutes(String(settings.minimumGapMs / 60_000)), [settings.minimumGapMs]);
|
||||||
useEffect(() => setSelectionsPerSession(String(settings.selectionsPerSession)), [settings.selectionsPerSession]);
|
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 (
|
return (
|
||||||
<section aria-labelledby="browsing-simulation-heading" className="panel settings-card">
|
<section aria-labelledby="browsing-simulation-heading" className="panel settings-card">
|
||||||
<div className="settings-card-heading">
|
<div className="settings-card-heading">
|
||||||
@@ -212,6 +227,40 @@ function BrowsingSimulationSection({ account }: { account: GrowthAccountDetail }
|
|||||||
label="Submit selected upvotes"
|
label="Submit selected upvotes"
|
||||||
onChange={(autoVoteEnabled) => update.mutate({ autoVoteEnabled })}
|
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">
|
<div className="growth-pacing">
|
||||||
<NumberField
|
<NumberField
|
||||||
hint={`Shortest gap between sessions. The system adds a random 0–${minutes(settings.jitterMs)} on top.`}
|
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.
|
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
|
## 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.
|
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.
|
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 () => {
|
it('shows an empty state when there are no accounts', async () => {
|
||||||
overviewPayload = [];
|
overviewPayload = [];
|
||||||
renderGrowth();
|
renderGrowth();
|
||||||
|
|||||||
Reference in new issue
Block a user