fix: keep outreach funnel response rollout-compatible
This commit is contained in:
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
@@ -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
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user