Rich apply summary with per-job details (title, company, link)

Each job now shows title @ company with a clickable link, grouped by
status category. Only non-empty categories are shown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 13:30:58 -08:00
parent 615899e9a9
commit 82fa2b3697
2 changed files with 32 additions and 23 deletions

View File

@@ -53,7 +53,8 @@ async function main() {
const results = {
submitted: 0, failed: 0, needs_answer: 0, total: 0,
skipped_recruiter: 0, skipped_external: 0, skipped_no_apply: 0, skipped_other: 0,
already_applied: 0, atsCounts: {}
already_applied: 0, closed: 0, atsCounts: {},
jobDetails: [], // { title, company, url, status } per job for summary
};
lock.onShutdown(() => {
@@ -245,6 +246,9 @@ async function handleResult(job, result, results, settings, profile, apiKey) {
const title = meta?.title || job.title || '?';
const company = meta?.company || job.company || '?';
// Track per-job detail for summary
results.jobDetails.push({ title, company, url: job.url, status });
switch (status) {
case 'submitted':
console.log(` ✅ Applied!`);

View File

@@ -93,34 +93,39 @@ export function formatSearchSummary(added, skipped, platforms) {
}
export function formatApplySummary(results) {
const { submitted, failed, needs_answer, total,
skipped_recruiter, skipped_external, skipped_no_apply,
skipped_other, already_applied, closed, atsCounts } = results;
const { total, jobDetails = [] } = results;
const lines = [
`✅ *Apply Run Complete* — ${total} jobs processed`,
``,
`📬 Applied: ${submitted}`,
const lines = [`✅ *Apply Run Complete* — ${total} job${total !== 1 ? 's' : ''} processed`];
// Group jobs by display category
const categories = [
{ key: 'submitted', emoji: '📬', label: 'Applied' },
{ key: 'needs_answer', emoji: '💬', label: 'Needs Answer' },
{ key: 'closed', emoji: '🚫', label: 'Closed' },
{ key: 'incomplete', emoji: '⚠️', label: 'Incomplete' },
{ key: 'stuck', emoji: '⚠️', label: 'Stuck' },
{ key: 'skipped_honeypot', emoji: '⚠️', label: 'Honeypot' },
{ key: 'skipped_no_apply', emoji: '⏭️', label: 'No Apply Button' },
{ key: 'no_modal', emoji: '⏭️', label: 'No Modal' },
{ key: 'skipped_recruiter_only', emoji: '🚫', label: 'Recruiter Only' },
{ key: 'skipped_external_unsupported', emoji: '🌐', label: 'External ATS' },
];
// Only show non-zero categories to keep the summary clean
if (needs_answer) lines.push(`💬 Needs your answer: ${needs_answer}`);
if (failed) lines.push(`❌ Failed: ${failed}`);
if (closed) lines.push(`🚫 Closed: ${closed}`);
if (skipped_no_apply) lines.push(`⏭️ No apply button: ${skipped_no_apply}`);
if (skipped_recruiter) lines.push(`🚫 Recruiter-only: ${skipped_recruiter}`);
if (already_applied) lines.push(`🔁 Already applied: ${already_applied}`);
if (skipped_other) lines.push(`⚠️ Other skips (honeypot/stuck/incomplete): ${skipped_other}`);
if (skipped_external > 0 && atsCounts) {
const sorted = Object.entries(atsCounts).sort((a, b) => b[1] - a[1]);
lines.push(``, `🌐 *External ATS — ${skipped_external} jobs* (saved for later):`);
for (const [platform, count] of sorted) {
lines.push(`${platform}: ${count}`);
for (const { key, emoji, label } of categories) {
const jobs = jobDetails.filter(j => j.status === key);
if (jobs.length === 0) continue;
lines.push('');
lines.push(`${emoji} *${label}:*`);
for (const j of jobs) {
const shortUrl = (j.url || '').replace(/^https?:\/\/(?:www\.)?/, '').replace(/\/$/, '');
lines.push(`${j.title} @ ${j.company}`);
if (shortUrl) lines.push(` ${shortUrl}`);
}
}
if (needs_answer > 0) lines.push(``, `💬 Check Telegram — questions waiting for your answers`);
if (jobDetails.some(j => j.status === 'needs_answer')) {
lines.push('', '💬 Check Telegram — questions waiting for your answers');
}
return lines.join('\n');
}