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',
},