169 lines
6.1 KiB
TypeScript
169 lines
6.1 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
/**
|
|
* Server-side schemas for the account-growth routes.
|
|
*
|
|
* These mirror `packages/api-contracts/src/growth.ts`, which the dashboard and
|
|
* the contract tests consume. The duplication is deliberate and matches
|
|
* `agent-api-contracts.ts`: the server's `rootDir` is `src/`, so it cannot
|
|
* import the shared package directly. `tests/web/growth-route.test.ts` parses
|
|
* real responses with the shared schemas so the two cannot drift apart silently.
|
|
*/
|
|
|
|
const DAY_MS = 24 * 60 * 60_000;
|
|
|
|
export const GrowthQuietHoursSchema = z.strictObject({
|
|
enabled: z.boolean(),
|
|
startHour: z.number().int().min(0).max(23),
|
|
endHour: z.number().int().min(0).max(23),
|
|
});
|
|
|
|
export const BrowsingSettingsUpdateRequestSchema = z.strictObject({
|
|
enabled: z.boolean().optional(),
|
|
dailyTargetMin: z.number().int().min(1).max(12).optional(),
|
|
dailyTargetMax: z.number().int().min(1).max(12).optional(),
|
|
postWeight: z.number().min(0).max(100).optional(),
|
|
commentWeight: z.number().min(0).max(100).optional(),
|
|
quietHours: GrowthQuietHoursSchema.optional(),
|
|
minimumGapMs: z.number().int().min(45 * 60_000).max(DAY_MS).optional(),
|
|
jitterMs: z.number().int().min(0).max(6 * 60 * 60_000).optional(),
|
|
allowNsfw: z.boolean().optional(),
|
|
autoVoteEnabled: z.boolean().optional(),
|
|
});
|
|
|
|
export const BrowsingSettingsSchema = z.strictObject({
|
|
enabled: z.boolean(), dailyTargetMin: z.number().int(), dailyTargetMax: z.number().int(),
|
|
postWeight: z.number(), commentWeight: z.number(), quietHours: GrowthQuietHoursSchema,
|
|
minimumGapMs: z.number().int(), jitterMs: z.number().int(), allowNsfw: z.boolean(),
|
|
autoVoteEnabled: z.boolean(),
|
|
nextDueAt: z.number().nullable(), updatedAt: z.number(),
|
|
});
|
|
|
|
export const BrowsingStatsSchema = z.strictObject({
|
|
selected: z.number().int(), posts: z.number().int(), comments: z.number().int(),
|
|
rejected: z.number().int(), skipped: z.number().int(),
|
|
});
|
|
|
|
export const BrowsingDecisionSchema = z.strictObject({
|
|
thingFullname: z.string(), thingKind: z.enum(['post', 'comment']), postId: z.string(),
|
|
subreddit: z.string(), author: z.string(), permalink: z.string(),
|
|
decision: z.enum(['simulated-upvote', 'skipped', 'rejected']), reason: z.string(), voteSubmitted: z.boolean(), decidedAt: z.number(),
|
|
});
|
|
|
|
export const BrowsingSummarySchema = z.strictObject({
|
|
settings: BrowsingSettingsSchema, todayTarget: z.number().int(), stats: BrowsingStatsSchema,
|
|
noVotesSubmitted: z.boolean(),
|
|
});
|
|
|
|
export const BrowsingDetailSchema = BrowsingSummarySchema.extend({
|
|
recentDecisions: z.array(BrowsingDecisionSchema),
|
|
});
|
|
|
|
export const GrowthSettingsUpdateRequestSchema = z.strictObject({
|
|
enabled: z.boolean().optional(),
|
|
minDelayMs: z.number().int().min(0).max(DAY_MS).optional(),
|
|
jitterMs: z.number().int().min(0).max(6 * 60 * 60_000).optional(),
|
|
/** Whole comments per day, or null to hand the allowance back to the ladder. */
|
|
dailyLimit: z.number().int().min(1).max(50).nullable().optional(),
|
|
quietHours: GrowthQuietHoursSchema.optional(),
|
|
allowNsfw: z.boolean().optional(),
|
|
});
|
|
|
|
export const GrowthPauseRequestSchema = z.strictObject({
|
|
reason: z.string().min(1).max(200).optional(),
|
|
});
|
|
|
|
export const GrowthSubredditCreateRequestSchema = z.strictObject({
|
|
subreddit: z.string().min(2).max(64),
|
|
weight: z.number().positive().max(100).optional(),
|
|
dailyCap: z.number().int().min(1).max(50).optional(),
|
|
});
|
|
|
|
export const GrowthSubredditUpdateRequestSchema = z.strictObject({
|
|
enabled: z.boolean().optional(),
|
|
weight: z.number().positive().max(100).optional(),
|
|
dailyCap: z.number().int().min(1).max(50).optional(),
|
|
});
|
|
|
|
export const GrowthSettingsSchema = z.strictObject({
|
|
enabled: z.boolean(),
|
|
trustLevel: z.number().int(),
|
|
/** Comments per day actually enforced: the override when set, else the earned allowance. */
|
|
dailyAllowance: z.number().int(),
|
|
/** What the adaptive ladder alone would permit. */
|
|
adaptiveAllowance: z.number().int(),
|
|
/** Operator override, or null when the allowance is left adaptive. */
|
|
dailyLimit: z.number().int().nullable(),
|
|
minDelayMs: z.number().int(),
|
|
jitterMs: z.number().int(),
|
|
quietHours: GrowthQuietHoursSchema,
|
|
allowNsfw: z.boolean(),
|
|
pausedReason: z.string().nullable(),
|
|
pausedAt: z.number().nullable(),
|
|
consecutiveRemovals: z.number().int(),
|
|
levelChangedAt: z.number().nullable(),
|
|
updatedAt: z.number(),
|
|
});
|
|
|
|
export const GrowthStatsSchema = z.strictObject({
|
|
postedLast24h: z.number().int(),
|
|
postedTotal: z.number().int(),
|
|
removedTotal: z.number().int(),
|
|
medianScore: z.number().nullable(),
|
|
queued: z.number().int(),
|
|
rejectedTotal: z.number().int(),
|
|
});
|
|
|
|
export const GrowthSubredditSchema = z.strictObject({
|
|
subreddit: z.string(),
|
|
enabled: z.boolean(),
|
|
weight: z.number(),
|
|
dailyCap: z.number().int(),
|
|
disabledReason: z.string().nullable(),
|
|
cooldownUntil: z.number().nullable(),
|
|
addedAt: z.number(),
|
|
postedLast24h: z.number().int(),
|
|
});
|
|
|
|
export const GrowthCommentSchema = z.strictObject({
|
|
postId: z.string(),
|
|
subreddit: z.string(),
|
|
status: z.enum(['discovered', 'posting', 'posted', 'verified', 'rejected', 'retryable']),
|
|
threadTitle: z.string(),
|
|
threadUrl: z.string(),
|
|
comment: z.string().nullable(),
|
|
commentPermalink: z.string().nullable(),
|
|
score: z.number().nullable(),
|
|
removed: z.boolean(),
|
|
shadowBanned: z.boolean().nullable(),
|
|
postedAt: z.number().nullable(),
|
|
discoveredAt: z.number(),
|
|
lastError: z.string().nullable(),
|
|
});
|
|
|
|
export const GrowthAccountSummarySchema = z.strictObject({
|
|
accountId: z.string(),
|
|
redditUsername: z.string(),
|
|
settings: GrowthSettingsSchema,
|
|
stats: GrowthStatsSchema,
|
|
subredditCount: z.number().int(),
|
|
activeSubredditCount: z.number().int(),
|
|
browsing: BrowsingSummarySchema,
|
|
});
|
|
|
|
export const GrowthAccountDetailSchema = GrowthAccountSummarySchema.extend({
|
|
subreddits: z.array(GrowthSubredditSchema),
|
|
recentComments: z.array(GrowthCommentSchema),
|
|
browsing: BrowsingDetailSchema,
|
|
});
|
|
|
|
export const GrowthOverviewResponseSchema = z.strictObject({
|
|
generatedAt: z.number(),
|
|
accounts: z.array(GrowthAccountSummarySchema),
|
|
});
|
|
|
|
export const GrowthAccountResponseSchema = z.strictObject({
|
|
success: z.literal(true),
|
|
account: GrowthAccountDetailSchema,
|
|
});
|