Searcher: each track independently tracks when it was last searched via data/track_history.json. New tracks get full lookback (90d), existing tracks look back since their last completion. Keyword-level crash resume preserved. Profiles: search tracks can specify profile_overrides (inline) or profile_path (external file) to customize resume, cover letter, and experience per track. Filter and applier both use the track-specific profile. Base profile.json provides shared info (name, contact, etc). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
3.2 KiB
JavaScript
100 lines
3.2 KiB
JavaScript
/**
|
|
* search_progress.mjs — Track which searches have completed
|
|
* Enables resume on restart — skips completed tracks and completed keywords within a track
|
|
*/
|
|
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
|
|
|
|
let progressPath = null;
|
|
let progress = null;
|
|
|
|
export function initProgress(dataDir, maxLookbackDays) {
|
|
progressPath = `${dataDir}/search_progress.json`;
|
|
|
|
if (existsSync(progressPath)) {
|
|
const saved = JSON.parse(readFileSync(progressPath, 'utf8'));
|
|
// Resume if an in-progress run exists (has uncompleted tracks)
|
|
if (saved.started_at && !saved.finished) {
|
|
progress = saved;
|
|
const done = progress.completed?.length ?? 0;
|
|
if (done > 0) {
|
|
console.log(`🔁 Resuming — completed tracks: ${progress.completed.join(', ')}\n`);
|
|
}
|
|
return progress;
|
|
}
|
|
}
|
|
|
|
progress = {
|
|
lookback_days: maxLookbackDays,
|
|
track_lookback: {}, // per-track lookback days, saved on init for crash resume
|
|
started_at: Date.now(),
|
|
completed: [],
|
|
keyword_progress: {},
|
|
};
|
|
save();
|
|
return progress;
|
|
}
|
|
|
|
/** Save per-track lookback so crash resume uses the right value */
|
|
export function saveTrackLookback(trackName, days) {
|
|
if (!progress) return;
|
|
if (!progress.track_lookback) progress.track_lookback = {};
|
|
progress.track_lookback[trackName] = days;
|
|
save();
|
|
}
|
|
|
|
/** Save generated keywords for a track — reused on resume, never regenerated mid-run */
|
|
export function saveKeywords(platform, track, keywords) {
|
|
if (!progress) return;
|
|
if (!progress.keywords) progress.keywords = {};
|
|
progress.keywords[`${platform}:${track}`] = keywords;
|
|
save();
|
|
}
|
|
|
|
/** Get saved keywords for a track, or null if not yet generated */
|
|
export function getSavedKeywords(platform, track) {
|
|
return progress?.keywords?.[`${platform}:${track}`] ?? null;
|
|
}
|
|
|
|
export function isCompleted(platform, track) {
|
|
if (!progress) return false;
|
|
return progress.completed.includes(`${platform}:${track}`);
|
|
}
|
|
|
|
/** Returns the index of the first keyword to run (skips already-completed ones) */
|
|
export function getKeywordStart(platform, track) {
|
|
if (!progress) return 0;
|
|
const key = `${platform}:${track}`;
|
|
const last = progress.keyword_progress?.[key] ?? -1;
|
|
return last + 1; // resume from next keyword after last completed
|
|
}
|
|
|
|
/** Call after each keyword completes */
|
|
export function markKeywordComplete(platform, track, keywordIndex) {
|
|
if (!progress) return;
|
|
const key = `${platform}:${track}`;
|
|
if (!progress.keyword_progress) progress.keyword_progress = {};
|
|
progress.keyword_progress[key] = keywordIndex;
|
|
save();
|
|
}
|
|
|
|
export function markComplete(platform, track, stats) {
|
|
if (!progress) return;
|
|
const key = `${platform}:${track}`;
|
|
if (!progress.completed.includes(key)) progress.completed.push(key);
|
|
// Clean up per-keyword progress for completed track
|
|
if (progress.keyword_progress) delete progress.keyword_progress[key];
|
|
progress[`stats:${key}`] = { ...stats, completed_at: Date.now() };
|
|
save();
|
|
}
|
|
|
|
export function clearProgress() {
|
|
try { if (progressPath) unlinkSync(progressPath); } catch {}
|
|
progress = null;
|
|
}
|
|
|
|
function save() {
|
|
if (progressPath && progress) {
|
|
writeFileSync(progressPath, JSON.stringify(progress, null, 2));
|
|
}
|
|
}
|