fix: keep outreach funnel response rollout-compatible

This commit is contained in:
jjbrunton committed 2026-08-10 18:38:39 +01:00
1 parent 1515b3e291
commit 5c57ed337b
6 files changed
+12 -55

No files matched your search

@@ -38,29 +38,7 @@ function formatWaitTime(timestamp: number, now: number): string {
return `in ${Math.ceil(remaining / 86_400_000)}d`;
}
type PhysiqueTargeting = { enabled: boolean; minScore: number; maxScore: number };
function targetingLabel(current: PhysiqueTargeting, prepared: PhysiqueTargeting | null): string {
if (current.enabled) {
const currentLabel = `Current target ${current.minScore}–${current.maxScore}`;
if (!prepared?.enabled) return `${currentLabel} · Prepared with filter off`;
if (prepared.minScore !== current.minScore || prepared.maxScore !== current.maxScore) {
return `${currentLabel} · Prepared for ${prepared.minScore}–${prepared.maxScore}`;
}
return currentLabel;
}
return prepared?.enabled
? `Current filter off · Prepared for ${prepared.minScore}–${prepared.maxScore}`
: 'Current filter off';
}
function PreparedCandidateDetails({
candidate,
currentTargeting,
}: {
candidate: OutreachCandidate;
currentTargeting: PhysiqueTargeting;
}) {
function PreparedCandidateDetails({ candidate }: { candidate: OutreachCandidate }) {
const preparation = candidate.preparation;
if (!preparation) return null;
const targeting = preparation.physiqueTargeting;
@@ -83,7 +61,7 @@ function PreparedCandidateDetails({
<div className="candidate-preparation-copy">
<div className="candidate-score-grid" aria-label="Preparation scoring">
<div><span>Attractiveness</span><strong>{preparation.attractivenessScore === null ? 'Not scored' : `${preparation.attractivenessScore}/100`}</strong><small>Used in the opener prompt</small></div>
<div><span>Physique</span><strong>{preparation.physiqueScore === null ? 'Not scorable' : `${preparation.physiqueScore}/10`}</strong><small>{targetingLabel(currentTargeting, targeting)}</small></div>
<div><span>Physique</span><strong>{preparation.physiqueScore === null ? 'Not scorable' : `${preparation.physiqueScore}/10`}</strong><small>{targeting?.enabled ? `Current target ${targeting.minScore}–${targeting.maxScore}` : 'Current filter off'}</small></div>
<div><span>Detected gender</span><strong>{preparation.detectedGender}</strong><small>Image analysis</small></div>
</div>
<div className="candidate-preparation-metadata">
@@ -106,15 +84,7 @@ function PreparedCandidateDetails({
);
}
function CandidateCard({
candidate,
currentTargeting,
now,
}: {
candidate: OutreachCandidate;
currentTargeting: PhysiqueTargeting;
now: number;
}) {
function CandidateCard({ candidate, now }: { candidate: OutreachCandidate; now: number }) {
const waitingUntil = candidate.availableAt > now;
const isKnownStatus = KNOWN_STATUSES.has(candidate.status);
return (
@@ -144,7 +114,7 @@ function CandidateCard({
</div>
{candidate.note ? <p className="candidate-note"><strong>Latest note</strong>{candidate.note}</p> : null}
{candidate.owner ? <AccountOwnerBadge owner={candidate.owner} /> : null}
<PreparedCandidateDetails candidate={candidate} currentTargeting={currentTargeting} />
<PreparedCandidateDetails candidate={candidate} />
</article>
</li>
);
@@ -230,14 +200,7 @@ export function OutreachPage() {
<div className="empty-state panel"><strong>The active queue is clear.</strong><span>New potential posts will appear here as discovery finds them.</span></div>
) : (
<ol aria-label="Active outreach candidates" className="outreach-candidates">
{data.candidates.map((candidate) => (
<CandidateCard
candidate={candidate}
currentTargeting={data.physiqueTargeting}
key={candidate.postId}
now={now}
/>
))}
{data.candidates.map((candidate) => <CandidateCard candidate={candidate} key={candidate.postId} now={now} />)}
</ol>
)}
</section>
+1 -1
View File
@@ -25,7 +25,7 @@ Navigation is declared in `app/navigation.ts`; route elements are composed in `a
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.
Prepared cards in the authenticated outreach funnel expose the exact opener, grounded image analysis, attractiveness and physique scores, detected gender, strategy/tone/model metadata, and original source image. Physique scoring shows both the currently configured target and, when different, the range snapshot used during preparation; it does not label a historical snapshot as though it were the live setting. The source image uses the shared `OriginPostCard` privacy behavior: blurred initially, first click reveals it, and a later click opens the full-screen preview. Candidates from older persisted snapshots degrade cleanly when newer optional metadata is absent.
Prepared cards in the authenticated outreach funnel expose the exact opener, grounded image analysis, attractiveness and physique scores, current target range, detected gender, strategy/tone/model metadata, and original source image. The live range is returned through the existing preparation field so cached dashboard bundles remain compatible during a rolling deployment. The durable queue still owns the historical preparation snapshot used for reconciliation. The source image uses the shared `OriginPostCard` privacy behavior: blurred initially, first click reveals it, and a later click opens the full-screen preview. Candidates from older persisted snapshots degrade cleanly when newer optional metadata is absent.
## State model
-5
View File
@@ -61,11 +61,6 @@ export const OutreachCandidateSchema = z.strictObject({
export const OutreachFunnelResponseSchema = z.strictObject({
generatedAt: z.number(),
physiqueTargeting: z.strictObject({
enabled: z.boolean(),
minScore: z.number().int().min(1).max(10),
maxScore: z.number().int().min(1).max(10),
}),
activeCount: z.number().int().nonnegative(),
stages: OutreachStageCountsSchema,
outcomes: z.strictObject({
+3 -2
View File
@@ -85,7 +85,9 @@ export function createOutreachRouter(ctx: WebContext): Router {
detectedGender: candidate.prepared.detectedGender,
attractivenessScore: candidate.prepared.attractivenessScore ?? null,
physiqueScore: candidate.prepared.physiqueScore ?? null,
physiqueTargeting: candidate.prepared.physiqueTargeting ?? null,
physiqueTargeting: candidate.sourceType === 'image'
? { ...ctx.runtimeConfig.reddit.physiqueTargeting }
: null,
sourceImageUrl: candidate.prepared.sourceImageUrl ?? null,
strategyId: candidate.prepared.strategyId,
experimentId: candidate.prepared.experimentId,
@@ -112,7 +114,6 @@ export function createOutreachRouter(ctx: WebContext): Router {
};
const response = {
generatedAt: Date.now(),
physiqueTargeting: { ...ctx.runtimeConfig.reddit.physiqueTargeting },
activeCount: ACTIVE_STATUSES.reduce((total, status) => total + stats.byStatus[status], 0),
stages,
outcomes: { sent: stats.byStatus.sent, rejected: stats.byStatus.rejected },
+2 -3
View File
@@ -122,7 +122,6 @@ describe('dashboard foundation', () => {
})),
http.get('/api/outreach', () => HttpResponse.json({
generatedAt: Date.parse('2026-08-09T08:00:00Z'),
physiqueTargeting: { enabled: true, minScore: 1, maxScore: 6 },
activeCount: 3,
stages: { discovered: 1, screening: 0, ready: 1, sending: 0, retryable: 1 },
outcomes: { sent: 12, rejected: 7 },
@@ -138,7 +137,7 @@ describe('dashboard foundation', () => {
message: 'You look incredible in that pose — what inspired this post?',
imageAnalysis: 'VISIBLE_FACTS:\nBODY: VISIBLE - full-body pose',
detectedGender: 'female', attractivenessScore: 68, physiqueScore: 6,
physiqueTargeting: { enabled: false, minScore: 1, maxScore: 10 },
physiqueTargeting: { enabled: true, minScore: 1, maxScore: 6 },
sourceImageUrl: 'https://i.redd.it/candidate-1.jpg',
strategyId: 'direct-observation', experimentId: 'strategies:direct-observation:abc123',
promptVersion: 'abc123', selectionReason: 'exploit', modelId: 'example/image-model',
@@ -163,7 +162,7 @@ describe('dashboard foundation', () => {
expect(preparation).toHaveTextContent('You look incredible in that pose');
expect(preparation).toHaveTextContent('68/100');
expect(preparation).toHaveTextContent('6/10');
expect(preparation).toHaveTextContent('Current target 1–6 · Prepared with filter off');
expect(preparation).toHaveTextContent('Current target 1–6');
expect(preparation).toHaveTextContent('example/image-model');
const sourceImage = within(preparation).getByRole('img', { name: 'Source post: [25F] A potential post' });
expect(sourceImage).toHaveClass('is-blurred');
+1 -2
View File
@@ -73,7 +73,6 @@ describe('outreach route', () => {
expect(response.status).toBe(200);
expect(parsed).toMatchObject({
physiqueTargeting: { enabled: true, minScore: 1, maxScore: 6 },
activeCount: 6,
stages: { discovered: 2, screening: 1, ready: 1, sending: 1, retryable: 1 },
outcomes: { sent: 2, rejected: 1 },
@@ -85,7 +84,7 @@ describe('outreach route', () => {
message: 'Prepared opener for Alice',
attractivenessScore: 68,
physiqueScore: 6,
physiqueTargeting: { enabled: true, minScore: 4, maxScore: 7 },
physiqueTargeting: { enabled: true, minScore: 1, maxScore: 6 },
sourceImageUrl: 'https://i.redd.it/post-1.jpg',
strategyId: 'direct-observation',
},