From 5c57ed337b7b024236092e3f21a02cd8e207f3fa Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Mon, 10 Aug 2026 18:38:39 +0100 Subject: [PATCH] fix: keep outreach funnel response rollout-compatible --- .../src/features/outreach/outreach-page.tsx | 47 ++----------------- docs/frontend.md | 2 +- packages/api-contracts/src/outreach.ts | 5 -- src/web/routes/outreach.ts | 5 +- tests/dashboard/app.test.tsx | 5 +- tests/web/outreach-route.test.ts | 3 +- 6 files changed, 12 insertions(+), 55 deletions(-) diff --git a/apps/dashboard/src/features/outreach/outreach-page.tsx b/apps/dashboard/src/features/outreach/outreach-page.tsx index c6c0cd0..f6efa4c 100644 --- a/apps/dashboard/src/features/outreach/outreach-page.tsx +++ b/apps/dashboard/src/features/outreach/outreach-page.tsx @@ -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({
Attractiveness{preparation.attractivenessScore === null ? 'Not scored' : `${preparation.attractivenessScore}/100`}Used in the opener prompt
-
Physique{preparation.physiqueScore === null ? 'Not scorable' : `${preparation.physiqueScore}/10`}{targetingLabel(currentTargeting, targeting)}
+
Physique{preparation.physiqueScore === null ? 'Not scorable' : `${preparation.physiqueScore}/10`}{targeting?.enabled ? `Current target ${targeting.minScore}–${targeting.maxScore}` : 'Current filter off'}
Detected gender{preparation.detectedGender}Image analysis
@@ -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({
{candidate.note ?

Latest note{candidate.note}

: null} {candidate.owner ? : null} - + ); @@ -230,14 +200,7 @@ export function OutreachPage() {
The active queue is clear.New potential posts will appear here as discovery finds them.
) : (
    - {data.candidates.map((candidate) => ( - - ))} + {data.candidates.map((candidate) => )}
)} diff --git a/docs/frontend.md b/docs/frontend.md index 8fd5bae..9fac89e 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -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 diff --git a/packages/api-contracts/src/outreach.ts b/packages/api-contracts/src/outreach.ts index ed2c8ec..d29b4b3 100644 --- a/packages/api-contracts/src/outreach.ts +++ b/packages/api-contracts/src/outreach.ts @@ -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({ diff --git a/src/web/routes/outreach.ts b/src/web/routes/outreach.ts index 986d608..3023169 100644 --- a/src/web/routes/outreach.ts +++ b/src/web/routes/outreach.ts @@ -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 }, diff --git a/tests/dashboard/app.test.tsx b/tests/dashboard/app.test.tsx index 4d0dd26..6158172 100644 --- a/tests/dashboard/app.test.tsx +++ b/tests/dashboard/app.test.tsx @@ -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'); diff --git a/tests/web/outreach-route.test.ts b/tests/web/outreach-route.test.ts index 77d9dc3..e8c1c32 100644 --- a/tests/web/outreach-route.test.ts +++ b/tests/web/outreach-route.test.ts @@ -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', },