"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`
- 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)
## Running (Docker)
@ -148,7 +148,7 @@ Dev URLs:
## 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)
- Webhook: `POST /api/webhook/trigger` (optional auth via `WEBHOOK_SECRET`)
- Ops: `DELETE /api/database` (wipes DB)

View File

@ -61,7 +61,7 @@ orchestrator/
| PATCH | `/api/jobs/:id` | Update 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/reject` | Mark as rejected |
| POST | `/api/jobs/:id/skip` | Mark as skipped |
### Pipeline

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ const statusLabels: Record<JobStatus, string> = {
processing: "Processing",
ready: "Ready",
applied: "Applied",
rejected: "Rejected",
skipped: "Skipped",
expired: "Expired",
};
@ -30,7 +30,7 @@ const statusStyles: Record<
processing: { variant: "secondary" },
ready: { variant: "default" },
applied: { variant: "outline", className: "text-emerald-400 border-emerald-500/30" },
rejected: { variant: "destructive" },
skipped: { variant: "destructive" },
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",
dot: "bg-emerald-400",
},
rejected: {
label: "Rejected",
skipped: {
label: "Skipped",
badge: "border-rose-500/30 bg-rose-500/10 text-rose-200",
dot: "bg-rose-400",
},
@ -292,7 +292,7 @@ export const OrchestratorPage: React.FC = () => {
processing: 0,
ready: 0,
applied: 0,
rejected: 0,
skipped: 0,
expired: 0,
});
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 {
await api.rejectJob(jobId);
await api.skipJob(jobId);
toast.message("Job skipped");
await loadJobs();
} 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);
}
};
@ -586,7 +586,7 @@ export const OrchestratorPage: React.FC = () => {
const selectedDiscoveredAt = selectedJob ? formatDateTime(selectedJob.discoveredAt) : null;
const canApply = selectedJob?.status === "ready";
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 showGeneratePdf = activeTab === "discovered";
const isProcessingSelected =
@ -716,7 +716,7 @@ export const OrchestratorPage: React.FC = () => {
{ label: "Processing", value: stats.processing },
{ label: "Ready", value: stats.ready },
{ label: "Applied", value: stats.applied },
{ label: "Rejected", value: stats.rejected },
{ label: "Skipped", value: stats.skipped },
{ label: "Expired", value: stats.expired },
].map((item, index) => (
<div
@ -1060,11 +1060,11 @@ export const OrchestratorPage: React.FC = () => {
</DropdownMenuItem>
</>
)}
{canReject && (
{canSkip && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => handleReject(selectedJob.id)}
onSelect={() => handleSkip(selectedJob.id)}
className="text-destructive focus:text-destructive"
>
<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="text-sm font-medium">Clear Discovered Jobs</div>
<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>
<AlertDialog>

View File

@ -105,7 +105,7 @@ apiRouter.get('/jobs/:id', async (req: Request, res: Response) => {
* PATCH /api/jobs/:id - Update a job
*/
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(),
suitabilityScore: z.number().min(0).max(100).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 {
const job = await jobsRepo.updateJob(req.params.id, { status: 'rejected' });
const job = await jobsRepo.updateJob(req.params.id, { status: 'skipped' });
if (!job) {
return res.status(404).json({ success: false, error: 'Job not found' });

View File

@ -65,7 +65,7 @@ const migrations = [
degree_required TEXT,
starting 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_reason TEXT,
tailored_summary TEXT,

View File

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

View File

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

View File

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