Add outreach candidate editing controls

This commit is contained in:
Jamie Brunton committed 2026-08-14 09:12:54 +01:00
1 parent c75e71f26e
commit 79c508be2b
19 files changed
+434 -12

No files matched your search

@@ -64,6 +64,9 @@ export interface CandidateQueueClearResult {
retainedSending: number;
}
export type CandidateEditResult = 'updated' | 'not-found' | 'not-editable';
export type CandidateCancelResult = 'cancelled' | 'not-found' | 'not-cancellable';
const ACTIVE_STATUSES: CandidateStatus[] = ['discovered', 'screening', 'ready', 'sending', 'retryable'];
/** Global, durable funnel from Reddit listing discovery through outbound delivery. */
@@ -288,6 +291,50 @@ export class CandidateQueueRepository {
})();
}
updatePreparedMessage(postId: string, message: string, now: number = Date.now()): CandidateEditResult {
return this.db.transaction(() => {
const row = this.db.prepare(`SELECT status, prepared_json FROM candidate_queue WHERE post_id = ?`)
.get(postId) as { status: CandidateStatus; prepared_json: string | null } | undefined;
if (!row) return 'not-found';
if (!['ready', 'retryable'].includes(row.status) || !row.prepared_json) return 'not-editable';
const prepared = JSON.parse(row.prepared_json) as ProcessedPost;
const edited: ProcessedPost = {
...prepared,
generatedMessage: message,
strategyId: null,
experimentId: null,
promptVersion: null,
selectionReason: null,
modelId: 'operator-edited',
operatorEdited: true,
};
const changed = this.db.prepare(`
UPDATE candidate_queue SET prepared_json = ?, updated_at = ?
WHERE post_id = ? AND status IN ('ready', 'retryable')
`).run(JSON.stringify(edited), now, postId).changes;
if (changed === 1) {
this.db.prepare(`UPDATE processed_posts SET generated_message = ? WHERE id = ?`).run(message, postId);
}
return changed === 1 ? 'updated' : 'not-editable';
})();
}
cancelUnsent(postId: string, now: number = Date.now()): CandidateCancelResult {
return this.db.transaction(() => {
const row = this.db.prepare(`SELECT status FROM candidate_queue WHERE post_id = ?`)
.get(postId) as { status: CandidateStatus } | undefined;
if (!row) return 'not-found';
if (!['discovered', 'screening', 'ready', 'retryable'].includes(row.status)) return 'not-cancellable';
const changed = this.db.prepare(`
UPDATE candidate_queue
SET status = 'rejected', last_error = 'Removed manually by operator', updated_at = ?,
lease_owner = NULL, lease_expires_at = NULL, assigned_account_id = NULL
WHERE post_id = ? AND status IN ('discovered', 'screening', 'ready', 'retryable')
`).run(now, postId).changes;
return changed === 1 ? 'cancelled' : 'not-cancellable';
})();
}
/** Re-evaluates prepared image candidates that have not yet been leased for sending. */
reconcilePhysiqueTargeting(
targeting: PhysiqueTargeting,