From c99ea10585a5881560acac96b46553543ebe5474 Mon Sep 17 00:00:00 2001 From: Matthew Jackson Date: Fri, 6 Mar 2026 13:34:21 -0800 Subject: [PATCH] Richer search and filter summaries Search: show per-track breakdown (found/added per track name) Filter: show top 5 scoring jobs with score, title, company and cost Co-Authored-By: Claude Opus 4.6 --- job_filter.mjs | 17 +++++++++++------ job_searcher.mjs | 9 +++++++-- lib/notify.mjs | 30 ++++++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/job_filter.mjs b/job_filter.mjs index 4f71bf6..31d315c 100644 --- a/job_filter.mjs +++ b/job_filter.mjs @@ -32,7 +32,7 @@ process.stderr.write = (chunk, ...args) => { logStream.write(chunk); return orig import { getJobsByStatus, updateJobStatus, loadConfig, loadQueue, saveQueue, dedupeAfterFilter } from './lib/queue.mjs'; import { loadProfile, submitBatches, checkBatch, downloadResults } from './lib/filter.mjs'; -import { sendTelegram } from './lib/notify.mjs'; +import { sendTelegram, formatFilterSummary } from './lib/notify.mjs'; import { DEFAULT_FILTER_MODEL, DEFAULT_FILTER_MIN_SCORE } from './lib/constants.mjs'; const isStats = process.argv.includes('--stats'); @@ -187,12 +187,17 @@ async function collect(state, settings) { }); writeFileSync(runsPath, JSON.stringify(runs, null, 2)); - const summary = `āœ… Filter complete — ${passed} passed, ${filtered} filtered, ${errors} errors (est. cost: $${totalCost.toFixed(2)})`; - console.log(`\n${summary}`); + // Collect top-scoring jobs for summary + const freshQueue = loadQueue(); + const topJobs = freshQueue + .filter(j => resultMap[j.id] && j.filter_score >= (searchConfig.filter_min_score ?? DEFAULT_FILTER_MIN_SCORE)) + .sort((a, b) => (b.filter_score || 0) - (a.filter_score || 0)) + .slice(0, 5) + .map(j => ({ score: j.filter_score, title: j.title, company: j.company })); - await sendTelegram(settings, - `šŸ” *AI Filter complete*\nāœ… Passed: ${passed}\n🚫 Filtered: ${filtered}\nāš ļø Errors: ${errors}` - ).catch(() => {}); + const summary = formatFilterSummary({ passed, filtered, errors, cost: totalCost, topJobs }); + console.log(`\n${summary.replace(/\*/g, '')}`); + await sendTelegram(settings, summary).catch(() => {}); } // --------------------------------------------------------------------------- diff --git a/job_searcher.mjs b/job_searcher.mjs index 928dbc0..a9fe5c0 100644 --- a/job_searcher.mjs +++ b/job_searcher.mjs @@ -38,6 +38,7 @@ async function main() { let totalAdded = 0, totalSeen = 0; const platformsRun = []; + const trackCounts = {}; // { trackName: { found, added } } const startedAt = Date.now(); const settings = loadConfig(resolve(__dir, 'config/settings.json')); @@ -70,7 +71,7 @@ async function main() { console.log(' Writing partial results to last-run file...'); writeLastRun(false); if (totalAdded > 0) { - const summary = formatSearchSummary(totalAdded, totalSeen - totalAdded, platformsRun.length ? platformsRun : ['LinkedIn']); + const summary = formatSearchSummary(totalAdded, totalSeen - totalAdded, platformsRun.length ? platformsRun : ['LinkedIn'], trackCounts); await sendTelegram(settings, summary + '\n_(partial run — interrupted)_').catch(() => {}); } }); @@ -178,6 +179,8 @@ async function main() { }); console.log(`\r [${search.name}] ${queryFound} found, ${queryAdded} new`); markComplete('linkedin', search.name, { found: queryFound, added: queryAdded }); + const tc = trackCounts[search.name] || (trackCounts[search.name] = { found: 0, added: 0 }); + tc.found += queryFound; tc.added += queryAdded; } platformsRun.push('LinkedIn'); @@ -220,6 +223,8 @@ async function main() { }); console.log(`\r [${search.name}] ${queryFound} found, ${queryAdded} new`); markComplete('wellfound', search.name, { found: queryFound, added: queryAdded }); + const tc = trackCounts[search.name] || (trackCounts[search.name] = { found: 0, added: 0 }); + tc.found += queryFound; tc.added += queryAdded; } platformsRun.push('Wellfound'); @@ -232,7 +237,7 @@ async function main() { } // Summary - const summary = formatSearchSummary(totalAdded, totalSeen - totalAdded, platformsRun); + const summary = formatSearchSummary(totalAdded, totalSeen - totalAdded, platformsRun, trackCounts); console.log(`\n${summary.replace(/\*/g, '')}`); if (totalAdded > 0) await sendTelegram(settings, summary).catch(() => {}); diff --git a/lib/notify.mjs b/lib/notify.mjs index 01cad6c..3a8f677 100644 --- a/lib/notify.mjs +++ b/lib/notify.mjs @@ -87,9 +87,20 @@ export async function replyTelegram(botToken, chatId, replyToMessageId, text) { } catch { /* best effort */ } } -export function formatSearchSummary(added, skipped, platforms) { +export function formatSearchSummary(added, skipped, platforms, trackCounts = {}) { if (added === 0) return `šŸ” *Job Search Complete*\nNo new jobs found this run.`; - return `šŸ” *Job Search Complete*\n${added} new job${added !== 1 ? 's' : ''} added to queue (${skipped} already seen)\nPlatforms: ${platforms.join(', ')}`; + const lines = [ + `šŸ” *Job Search Complete*`, + `${added} new job${added !== 1 ? 's' : ''} added (${skipped} already seen)`, + `Platforms: ${platforms.join(', ')}`, + ]; + if (Object.keys(trackCounts).length > 0) { + lines.push(''); + for (const [track, counts] of Object.entries(trackCounts)) { + lines.push(` • *${track}*: ${counts.added} new / ${counts.found} found`); + } + } + return lines.join('\n'); } export function formatApplySummary(results) { @@ -130,6 +141,21 @@ export function formatApplySummary(results) { return lines.join('\n'); } +export function formatFilterSummary({ passed, filtered, errors, cost, topJobs = [] }) { + const lines = [ + `🧠 *AI Filter Complete*`, + `āœ… Passed: ${passed} | 🚫 Filtered: ${filtered}${errors ? ` | āš ļø Errors: ${errors}` : ''}`, + ]; + if (cost != null) lines.push(`šŸ’° Cost: $${cost.toFixed(2)}`); + if (topJobs.length > 0) { + lines.push('', '*Top scores:*'); + for (const j of topJobs.slice(0, 5)) { + lines.push(` • ${j.score} — ${j.title} @ ${j.company}`); + } + } + return lines.join('\n'); +} + export function formatUnknownQuestion(job, question) { return `ā“ *Unknown question while applying*\n\n*Job:* ${job.title} @ ${job.company}\n*Question:* "${question}"\n\nWhat should I answer? (Reply and I'll save it for all future runs)`; }