"rejected" isn't a thing. it's "skipped"

This commit is contained in:
DaKheera47 2026-01-15 14:06:14 +00:00
parent b5eebfcf49
commit 717987c5cc
15 changed files with 49 additions and 49 deletions

View File

@ -89,7 +89,7 @@ job-ops/
- `jobs` - `jobs`
- from crawl: `title`, `employer`, `jobUrl`, `applicationLink`, `deadline`, `salary`, `location`, `jobDescription`, `source` (gradcracker/indeed/linkedin/ukvisajobs), etc. - from crawl: `title`, `employer`, `jobUrl`, `applicationLink`, `deadline`, `salary`, `location`, `jobDescription`, `source` (gradcracker/indeed/linkedin/ukvisajobs), etc.
- enrichments: `status` (`discovered` -> `processing` -> `ready` -> `applied`/`rejected`), `suitabilityScore`, `suitabilityReason`, `tailoredSummary`, `pdfPath`, `notionPageId` - enrichments: `status` (`discovered` -> `processing` -> `ready` -> `applied`/`skipped`), `suitabilityScore`, `suitabilityReason`, `tailoredSummary`, `pdfPath`, `notionPageId`
- `pipeline_runs`: audit log of runs (`running`/`completed`/`failed`, counts, error) - `pipeline_runs`: audit log of runs (`running`/`completed`/`failed`, counts, error)
## Running (Docker) ## Running (Docker)
@ -148,7 +148,7 @@ Dev URLs:
## Key endpoints ## Key endpoints
- Jobs: `GET /api/jobs`, `POST /api/jobs/:id/process`, `POST /api/jobs/:id/apply`, `POST /api/jobs/:id/reject`, `POST /api/jobs/process-discovered` - Jobs: `GET /api/jobs`, `POST /api/jobs/:id/process`, `POST /api/jobs/:id/apply`, `POST /api/jobs/:id/skip`, `POST /api/jobs/process-discovered`
- Pipeline: `POST /api/pipeline/run`, `GET /api/pipeline/status`, `GET /api/pipeline/progress` (SSE) - Pipeline: `POST /api/pipeline/run`, `GET /api/pipeline/status`, `GET /api/pipeline/progress` (SSE)
- Webhook: `POST /api/webhook/trigger` (optional auth via `WEBHOOK_SECRET`) - Webhook: `POST /api/webhook/trigger` (optional auth via `WEBHOOK_SECRET`)
- Ops: `DELETE /api/database` (wipes DB) - Ops: `DELETE /api/database` (wipes DB)

View File

@ -61,7 +61,7 @@ orchestrator/
| PATCH | `/api/jobs/:id` | Update job | | PATCH | `/api/jobs/:id` | Update job |
| POST | `/api/jobs/:id/process` | Generate resume for job | | POST | `/api/jobs/:id/process` | Generate resume for job |
| POST | `/api/jobs/:id/apply` | Mark as applied + sync to Notion | | POST | `/api/jobs/:id/apply` | Mark as applied + sync to Notion |
| POST | `/api/jobs/:id/reject` | Mark as rejected | | POST | `/api/jobs/:id/skip` | Mark as skipped |
### Pipeline ### Pipeline

View File

@ -89,8 +89,8 @@ export async function markAsApplied(id: string): Promise<Job> {
}); });
} }
export async function rejectJob(id: string): Promise<Job> { export async function skipJob(id: string): Promise<Job> {
return fetchApi<Job>(`/jobs/${id}/reject`, { return fetchApi<Job>(`/jobs/${id}/skip`, {
method: 'POST', method: 'POST',
}); });
} }

View File

@ -31,7 +31,7 @@ import { StatusBadge } from "./StatusBadge";
interface JobCardProps { interface JobCardProps {
job: Job; job: Job;
onApply: (id: string) => void | Promise<void>; onApply: (id: string) => void | Promise<void>;
onReject: (id: string) => void | Promise<void>; onSkip: (id: string) => void | Promise<void>;
onProcess: (id: string) => void | Promise<void>; onProcess: (id: string) => void | Promise<void>;
onEditDescription?: (id: string) => void; onEditDescription?: (id: string) => void;
isProcessing: boolean; isProcessing: boolean;
@ -78,7 +78,7 @@ const safeFilenamePart = (value: string) => value.replace(/[^a-z0-9]/gi, "_");
export const JobCard: React.FC<JobCardProps> = ({ export const JobCard: React.FC<JobCardProps> = ({
job, job,
onApply, onApply,
onReject, onSkip,
onProcess, onProcess,
onEditDescription, onEditDescription,
isProcessing, isProcessing,
@ -95,7 +95,7 @@ export const JobCard: React.FC<JobCardProps> = ({
const hasPdf = !!job.pdfPath; const hasPdf = !!job.pdfPath;
const canApply = job.status === "ready"; const canApply = job.status === "ready";
const canProcess = ["discovered", "ready"].includes(job.status); const canProcess = ["discovered", "ready"].includes(job.status);
const canReject = ["discovered", "ready"].includes(job.status); const canSkip = ["discovered", "ready"].includes(job.status);
const jobLink = job.applicationLink || job.jobUrl; const jobLink = job.applicationLink || job.jobUrl;
const pdfHref = `/pdfs/resume_${job.id}.pdf?v=${encodeURIComponent(job.updatedAt)}`; const pdfHref = `/pdfs/resume_${job.id}.pdf?v=${encodeURIComponent(job.updatedAt)}`;
@ -164,7 +164,7 @@ export const JobCard: React.FC<JobCardProps> = ({
</div> </div>
</CardHeader> </CardHeader>
{(job.suitabilityReason || canApply || canReject || canProcess || hasPdf) && ( {(job.suitabilityReason || canApply || canSkip || canProcess || hasPdf) && (
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{job.suitabilityReason && ( {job.suitabilityReason && (
<p className="text-sm italic text-muted-foreground"> <p className="text-sm italic text-muted-foreground">
@ -246,8 +246,8 @@ export const JobCard: React.FC<JobCardProps> = ({
</Button> </Button>
)} )}
{canReject && ( {canSkip && (
<Button variant="destructive" size="sm" onClick={() => onReject(job.id)}> <Button variant="destructive" size="sm" onClick={() => onSkip(job.id)}>
<XCircle className="mr-2 h-4 w-4" /> <XCircle className="mr-2 h-4 w-4" />
Skip Skip
</Button> </Button>

View File

@ -39,7 +39,7 @@ import { TailoringEditor } from "./TailoringEditor";
interface JobListProps { interface JobListProps {
jobs: Job[]; jobs: Job[];
onApply: (id: string) => void | Promise<void>; onApply: (id: string) => void | Promise<void>;
onReject: (id: string) => void | Promise<void>; onSkip: (id: string) => void | Promise<void>;
onProcess: (id: string) => void | Promise<void>; onProcess: (id: string) => void | Promise<void>;
onUpdate: () => void | Promise<void>; onUpdate: () => void | Promise<void>;
processingJobId: string | null; processingJobId: string | null;
@ -87,7 +87,7 @@ const statusRank: Record<JobStatus, number> = {
processing: 1, processing: 1,
ready: 2, ready: 2,
applied: 3, applied: 3,
rejected: 4, skipped: 4,
expired: 5, expired: 5,
}; };
@ -194,7 +194,7 @@ const stripHtml = (value: string) => value.replace(/<[^>]*>/g, " ").replace(/\s+
export const JobList: React.FC<JobListProps> = ({ export const JobList: React.FC<JobListProps> = ({
jobs, jobs,
onApply, onApply,
onReject, onSkip,
onProcess, onProcess,
onUpdate, onUpdate,
processingJobId, processingJobId,
@ -204,7 +204,7 @@ export const JobList: React.FC<JobListProps> = ({
const [sourceFilter, setSourceFilter] = useState<JobSource | "all">("all"); const [sourceFilter, setSourceFilter] = useState<JobSource | "all">("all");
const [sort, setSort] = useState<JobSort>(DEFAULT_SORT); const [sort, setSort] = useState<JobSort>(DEFAULT_SORT);
const [selectedJobIds, setSelectedJobIds] = useState<Set<string>>(() => new Set()); const [selectedJobIds, setSelectedJobIds] = useState<Set<string>>(() => new Set());
const [batchAction, setBatchAction] = useState<null | "process" | "reject" | "apply">(null); const [batchAction, setBatchAction] = useState<null | "process" | "skip" | "apply">(null);
const [highlightedJobId, setHighlightedJobId] = useState<string | null>(null); const [highlightedJobId, setHighlightedJobId] = useState<string | null>(null);
const [isHighlightVisible, setIsHighlightVisible] = useState(false); const [isHighlightVisible, setIsHighlightVisible] = useState(false);
const [isEditingDescription, setIsEditingDescription] = useState(false); const [isEditingDescription, setIsEditingDescription] = useState(false);
@ -369,7 +369,7 @@ export const JobList: React.FC<JobListProps> = ({
const selectedCount = selectedJobIds.size; const selectedCount = selectedJobIds.size;
const runBatch = async (action: "process" | "reject" | "apply") => { const runBatch = async (action: "process" | "skip" | "apply") => {
if (selectedJobs.length === 0) return; if (selectedJobs.length === 0) return;
const eligible = selectedJobs.filter((job) => { const eligible = selectedJobs.filter((job) => {
@ -389,7 +389,7 @@ export const JobList: React.FC<JobListProps> = ({
for (const job of eligible) { for (const job of eligible) {
if (action === "process") await Promise.resolve(onProcess(job.id)); if (action === "process") await Promise.resolve(onProcess(job.id));
if (action === "apply") await Promise.resolve(onApply(job.id)); if (action === "apply") await Promise.resolve(onApply(job.id));
if (action === "reject") await Promise.resolve(onReject(job.id)); if (action === "skip") await Promise.resolve(onSkip(job.id));
} }
setSelectedJobIds(new Set()); setSelectedJobIds(new Set());
@ -440,7 +440,7 @@ export const JobList: React.FC<JobListProps> = ({
<JobCard <JobCard
job={highlightedJob} job={highlightedJob}
onApply={onApply} onApply={onApply}
onReject={onReject} onSkip={onSkip}
onProcess={onProcess} onProcess={onProcess}
isProcessing={processingJobId === highlightedJob.id} isProcessing={processingJobId === highlightedJob.id}
highlightedJobId={highlightedJobId} highlightedJobId={highlightedJobId}
@ -713,7 +713,7 @@ export const JobList: React.FC<JobListProps> = ({
<Button <Button
size="sm" size="sm"
variant="destructive" variant="destructive"
onClick={() => runBatch("reject")} onClick={() => runBatch("skip")}
disabled={batchAction !== null} disabled={batchAction !== null}
> >
Skip Skip
@ -746,7 +746,7 @@ export const JobList: React.FC<JobListProps> = ({
selectedJobIds={selectedJobIds} selectedJobIds={selectedJobIds}
onSelectedJobIdsChange={setSelectedJobIds} onSelectedJobIdsChange={setSelectedJobIds}
onApply={onApply} onApply={onApply}
onReject={onReject} onSkip={onSkip}
onProcess={onProcess} onProcess={onProcess}
processingJobId={processingJobId} processingJobId={processingJobId}
highlightedJobId={highlightedJobId} highlightedJobId={highlightedJobId}
@ -762,7 +762,7 @@ export const JobList: React.FC<JobListProps> = ({
key={job.id} key={job.id}
job={job} job={job}
onApply={onApply} onApply={onApply}
onReject={onReject} onSkip={onSkip}
onProcess={onProcess} onProcess={onProcess}
isProcessing={processingJobId === job.id} isProcessing={processingJobId === job.id}
highlightedJobId={highlightedJobId} highlightedJobId={highlightedJobId}

View File

@ -57,7 +57,7 @@ export interface JobTableProps {
selectedJobIds: Set<string>; selectedJobIds: Set<string>;
onSelectedJobIdsChange: (ids: Set<string>) => void; onSelectedJobIdsChange: (ids: Set<string>) => void;
onApply: (id: string) => void | Promise<void>; onApply: (id: string) => void | Promise<void>;
onReject: (id: string) => void | Promise<void>; onSkip: (id: string) => void | Promise<void>;
onProcess: (id: string) => void | Promise<void>; onProcess: (id: string) => void | Promise<void>;
onEditDescription?: (id: string) => void; onEditDescription?: (id: string) => void;
processingJobId: string | null; processingJobId: string | null;
@ -145,7 +145,7 @@ export const JobTable: React.FC<JobTableProps> = ({
selectedJobIds, selectedJobIds,
onSelectedJobIdsChange, onSelectedJobIdsChange,
onApply, onApply,
onReject, onSkip,
onProcess, onProcess,
onEditDescription, onEditDescription,
processingJobId, processingJobId,
@ -228,7 +228,7 @@ export const JobTable: React.FC<JobTableProps> = ({
const canApply = job.status === "ready"; const canApply = job.status === "ready";
const canProcess = ["discovered", "ready"].includes(job.status); const canProcess = ["discovered", "ready"].includes(job.status);
const canReject = ["discovered", "ready"].includes(job.status); const canSkip = ["discovered", "ready"].includes(job.status);
const isProcessing = processingJobId === job.id; const isProcessing = processingJobId === job.id;
const isSelected = selectedJobIds.has(job.id); const isSelected = selectedJobIds.has(job.id);
const isHighlighted = highlightedJobId === job.id; const isHighlighted = highlightedJobId === job.id;
@ -342,7 +342,7 @@ export const JobTable: React.FC<JobTableProps> = ({
</> </>
)} )}
{(canProcess || canReject || canApply) && <DropdownMenuSeparator />} {(canProcess || canSkip || canApply) && <DropdownMenuSeparator />}
{canProcess && ( {canProcess && (
<DropdownMenuItem <DropdownMenuItem
@ -358,9 +358,9 @@ export const JobTable: React.FC<JobTableProps> = ({
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{canReject && ( {canSkip && (
<DropdownMenuItem <DropdownMenuItem
onSelect={() => onReject(job.id)} onSelect={() => onSkip(job.id)}
> >
<XCircle className="mr-2 h-4 w-4" /> <XCircle className="mr-2 h-4 w-4" />
Skip Skip

View File

@ -28,7 +28,7 @@ const statConfig: Array<{
{ key: "processing", label: "Processing", Icon: Loader2 }, { key: "processing", label: "Processing", Icon: Loader2 },
{ key: "ready", label: "Ready", Icon: Sparkles }, { key: "ready", label: "Ready", Icon: Sparkles },
{ key: "applied", label: "Applied", Icon: CheckCircle2 }, { key: "applied", label: "Applied", Icon: CheckCircle2 },
{ key: "rejected", label: "Rejected", Icon: XCircle }, { key: "skipped", label: "Skipped", Icon: XCircle },
{ key: "expired", label: "Expired", Icon: Clock }, { key: "expired", label: "Expired", Icon: Clock },
]; ];

View File

@ -18,7 +18,7 @@ const statusLabels: Record<JobStatus, string> = {
processing: "Processing", processing: "Processing",
ready: "Ready", ready: "Ready",
applied: "Applied", applied: "Applied",
rejected: "Rejected", skipped: "Skipped",
expired: "Expired", expired: "Expired",
}; };
@ -30,7 +30,7 @@ const statusStyles: Record<
processing: { variant: "secondary" }, processing: { variant: "secondary" },
ready: { variant: "default" }, ready: { variant: "default" },
applied: { variant: "outline", className: "text-emerald-400 border-emerald-500/30" }, applied: { variant: "outline", className: "text-emerald-400 border-emerald-500/30" },
rejected: { variant: "destructive" }, skipped: { variant: "destructive" },
expired: { variant: "outline", className: "text-muted-foreground" }, expired: { variant: "outline", className: "text-muted-foreground" },
}; };

View File

@ -93,8 +93,8 @@ const statusTokens: Record<
badge: "border-emerald-500/30 bg-emerald-500/10 text-emerald-200", badge: "border-emerald-500/30 bg-emerald-500/10 text-emerald-200",
dot: "bg-emerald-400", dot: "bg-emerald-400",
}, },
rejected: { skipped: {
label: "Rejected", label: "Skipped",
badge: "border-rose-500/30 bg-rose-500/10 text-rose-200", badge: "border-rose-500/30 bg-rose-500/10 text-rose-200",
dot: "bg-rose-400", dot: "bg-rose-400",
}, },
@ -292,7 +292,7 @@ export const OrchestratorPage: React.FC = () => {
processing: 0, processing: 0,
ready: 0, ready: 0,
applied: 0, applied: 0,
rejected: 0, skipped: 0,
expired: 0, expired: 0,
}); });
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -430,13 +430,13 @@ export const OrchestratorPage: React.FC = () => {
} }
}; };
const handleReject = async (jobId: string) => { const handleSkip = async (jobId: string) => {
try { try {
await api.rejectJob(jobId); await api.skipJob(jobId);
toast.message("Job skipped"); toast.message("Job skipped");
await loadJobs(); await loadJobs();
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Failed to reject job"; const message = error instanceof Error ? error.message : "Failed to skip job";
toast.error(message); toast.error(message);
} }
}; };
@ -586,7 +586,7 @@ export const OrchestratorPage: React.FC = () => {
const selectedDiscoveredAt = selectedJob ? formatDateTime(selectedJob.discoveredAt) : null; const selectedDiscoveredAt = selectedJob ? formatDateTime(selectedJob.discoveredAt) : null;
const canApply = selectedJob?.status === "ready"; const canApply = selectedJob?.status === "ready";
const canProcess = selectedJob ? ["discovered", "ready"].includes(selectedJob.status) : false; const canProcess = selectedJob ? ["discovered", "ready"].includes(selectedJob.status) : false;
const canReject = selectedJob ? ["discovered", "ready"].includes(selectedJob.status) : false; const canSkip = selectedJob ? ["discovered", "ready"].includes(selectedJob.status) : false;
const showReadyPdf = activeTab === "ready"; const showReadyPdf = activeTab === "ready";
const showGeneratePdf = activeTab === "discovered"; const showGeneratePdf = activeTab === "discovered";
const isProcessingSelected = const isProcessingSelected =
@ -716,7 +716,7 @@ export const OrchestratorPage: React.FC = () => {
{ label: "Processing", value: stats.processing }, { label: "Processing", value: stats.processing },
{ label: "Ready", value: stats.ready }, { label: "Ready", value: stats.ready },
{ label: "Applied", value: stats.applied }, { label: "Applied", value: stats.applied },
{ label: "Rejected", value: stats.rejected }, { label: "Skipped", value: stats.skipped },
{ label: "Expired", value: stats.expired }, { label: "Expired", value: stats.expired },
].map((item, index) => ( ].map((item, index) => (
<div <div
@ -1060,11 +1060,11 @@ export const OrchestratorPage: React.FC = () => {
</DropdownMenuItem> </DropdownMenuItem>
</> </>
)} )}
{canReject && ( {canSkip && (
<> <>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onSelect={() => handleReject(selectedJob.id)} onSelect={() => handleSkip(selectedJob.id)}
className="text-destructive focus:text-destructive" className="text-destructive focus:text-destructive"
> >
<XCircle className="mr-2 h-4 w-4" /> <XCircle className="mr-2 h-4 w-4" />

View File

@ -906,7 +906,7 @@ export const SettingsPage: React.FC = () => {
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="text-sm font-medium">Clear Discovered Jobs</div> <div className="text-sm font-medium">Clear Discovered Jobs</div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
Delete all jobs with the status "discovered". Ready, applied, and rejected jobs are kept. Delete all jobs with the status "discovered". Ready, applied, and skipped jobs are kept.
</div> </div>
</div> </div>
<AlertDialog> <AlertDialog>

View File

@ -105,7 +105,7 @@ apiRouter.get('/jobs/:id', async (req: Request, res: Response) => {
* PATCH /api/jobs/:id - Update a job * PATCH /api/jobs/:id - Update a job
*/ */
const updateJobSchema = z.object({ const updateJobSchema = z.object({
status: z.enum(['discovered', 'processing', 'ready', 'applied', 'rejected', 'expired']).optional(), status: z.enum(['discovered', 'processing', 'ready', 'applied', 'skipped', 'expired']).optional(),
jobDescription: z.string().optional(), jobDescription: z.string().optional(),
suitabilityScore: z.number().min(0).max(100).optional(), suitabilityScore: z.number().min(0).max(100).optional(),
suitabilityReason: z.string().optional(), suitabilityReason: z.string().optional(),
@ -241,11 +241,11 @@ apiRouter.post('/jobs/:id/apply', async (req: Request, res: Response) => {
}); });
/** /**
* POST /api/jobs/:id/reject - Mark a job as rejected * POST /api/jobs/:id/skip - Mark a job as skipped
*/ */
apiRouter.post('/jobs/:id/reject', async (req: Request, res: Response) => { apiRouter.post('/jobs/:id/skip', async (req: Request, res: Response) => {
try { try {
const job = await jobsRepo.updateJob(req.params.id, { status: 'rejected' }); const job = await jobsRepo.updateJob(req.params.id, { status: 'skipped' });
if (!job) { if (!job) {
return res.status(404).json({ success: false, error: 'Job not found' }); return res.status(404).json({ success: false, error: 'Job not found' });

View File

@ -65,7 +65,7 @@ const migrations = [
degree_required TEXT, degree_required TEXT,
starting TEXT, starting TEXT,
job_description TEXT, job_description TEXT,
status TEXT NOT NULL DEFAULT 'discovered' CHECK(status IN ('discovered', 'processing', 'ready', 'applied', 'rejected', 'expired')), status TEXT NOT NULL DEFAULT 'discovered' CHECK(status IN ('discovered', 'processing', 'ready', 'applied', 'skipped', 'expired')),
suitability_score REAL, suitability_score REAL,
suitability_reason TEXT, suitability_reason TEXT,
tailored_summary TEXT, tailored_summary TEXT,

View File

@ -54,7 +54,7 @@ export const jobs = sqliteTable('jobs', {
// Orchestrator enrichments // Orchestrator enrichments
status: text('status', { status: text('status', {
enum: ['discovered', 'processing', 'ready', 'applied', 'rejected', 'expired'] enum: ['discovered', 'processing', 'ready', 'applied', 'skipped', 'expired']
}).notNull().default('discovered'), }).notNull().default('discovered'),
suitabilityScore: real('suitability_score'), suitabilityScore: real('suitability_score'),
suitabilityReason: text('suitability_reason'), suitabilityReason: text('suitability_reason'),

View File

@ -165,7 +165,7 @@ export async function getJobStats(): Promise<Record<JobStatus, number>> {
processing: 0, processing: 0,
ready: 0, ready: 0,
applied: 0, applied: 0,
rejected: 0, skipped: 0,
expired: 0, expired: 0,
}; };

View File

@ -7,7 +7,7 @@ export type JobStatus =
| 'processing' // Currently generating resume | 'processing' // Currently generating resume
| 'ready' // PDF generated, waiting for user to apply | 'ready' // PDF generated, waiting for user to apply
| 'applied' // User marked as applied (added to Notion) | 'applied' // User marked as applied (added to Notion)
| 'rejected' // User rejected this job | 'skipped' // User skipped this job
| 'expired'; // Deadline passed | 'expired'; // Deadline passed
export type JobSource = export type JobSource =