Fix entry-level job filtering and perform pre-launch verification
This commit is contained in:
parent
a8dddccbdb
commit
16efc58a5c
12 changed files with 66 additions and 38 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from typing import List, Dict, Any
|
||||
from scrapers.ats_ingestion import determine_experience_level
|
||||
|
||||
def run_ct_jobaps_scrape() -> List[Dict[Any, Any]]:
|
||||
"""
|
||||
|
|
@ -45,7 +46,7 @@ def run_ct_jobaps_scrape() -> List[Dict[Any, Any]]:
|
|||
"location": "Hartford & CT Statewide",
|
||||
"is_remote": False,
|
||||
"department": "Government & Public Services",
|
||||
"experience_level": "Mid-Level",
|
||||
"experience_level": determine_experience_level(title),
|
||||
"description": f"Official State of Connecticut position. Application details available at {full_url}",
|
||||
"salary_min": None,
|
||||
"salary_max": None,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import os
|
|||
import time
|
||||
import pandas as pd
|
||||
from typing import List, Dict, Any
|
||||
from scrapers.ats_ingestion import determine_experience_level, determine_department
|
||||
|
||||
def get_jobspy_proxies():
|
||||
proxies = get_privado_proxy_list()
|
||||
|
|
@ -105,11 +106,14 @@ def run_jobspy_scrapes() -> List[Dict[Any, Any]]:
|
|||
if loc_val == "nan":
|
||||
loc_val = loc
|
||||
|
||||
title_str = str(row.get("title", "Untitled"))
|
||||
collected_jobs.append({
|
||||
"title": str(row.get("title", "Untitled")),
|
||||
"title": title_str,
|
||||
"company": str(row.get("company", "Unknown")),
|
||||
"location": loc_val,
|
||||
"is_remote": False,
|
||||
"department": determine_department(title_str, ""),
|
||||
"experience_level": determine_experience_level(title_str),
|
||||
"description": str(row.get("description", "") or "No description provided."),
|
||||
"salary_min": float(row.get("min_amount")) if pd.notnull(row.get("min_amount")) else None,
|
||||
"salary_max": float(row.get("max_amount")) if pd.notnull(row.get("max_amount")) else None,
|
||||
|
|
@ -133,11 +137,14 @@ def run_jobspy_scrapes() -> List[Dict[Any, Any]]:
|
|||
if not job_url or job_url == "nan":
|
||||
continue
|
||||
|
||||
title_str = str(row.get("title", "Untitled"))
|
||||
collected_jobs.append({
|
||||
"title": str(row.get("title", "Untitled")),
|
||||
"title": title_str,
|
||||
"company": str(row.get("company", "Unknown")),
|
||||
"location": "Remote, USA",
|
||||
"is_remote": True,
|
||||
"department": determine_department(title_str, ""),
|
||||
"experience_level": determine_experience_level(title_str),
|
||||
"description": str(row.get("description", "") or "No description provided."),
|
||||
"salary_min": float(row.get("min_amount")) if pd.notnull(row.get("min_amount")) else None,
|
||||
"salary_max": float(row.get("max_amount")) if pd.notnull(row.get("max_amount")) else None,
|
||||
|
|
|
|||
|
|
@ -759,7 +759,7 @@ export default function AdminPage() {
|
|||
|
||||
{pendingReviews.length === 0 ? (
|
||||
<div className="py-6 text-center text-xs text-zinc-400 font-mono">
|
||||
✓ Moderation queue clean! No pending reviews to approve.
|
||||
Moderation queue clean. No pending reviews to approve.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
|
|
@ -809,7 +809,7 @@ export default function AdminPage() {
|
|||
|
||||
{reportedReviews.length === 0 ? (
|
||||
<div className="py-6 text-center text-xs text-zinc-400 font-mono">
|
||||
✓ No reported reviews flagged for moderation.
|
||||
No reported reviews flagged for moderation.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
|
|
@ -870,7 +870,7 @@ export default function AdminPage() {
|
|||
|
||||
{claims.length === 0 ? (
|
||||
<div className="py-8 text-center text-xs text-zinc-400 font-mono">
|
||||
✓ No company ownership claims submitted.
|
||||
No company ownership claims submitted.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -964,7 +964,7 @@ export default function AdminPage() {
|
|||
|
||||
{auditLogs.length === 0 ? (
|
||||
<div className="py-8 text-center text-xs text-zinc-400 font-mono">
|
||||
✓ No audit log records captured yet.
|
||||
No audit log records captured yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
|
|
|
|||
|
|
@ -150,14 +150,26 @@ export async function GET(req: Request) {
|
|||
{
|
||||
OR: [
|
||||
{ experienceLevel: "Entry Level" },
|
||||
{ experienceLevel: "Mid-Level" },
|
||||
{ title: { contains: "Junior" } },
|
||||
{ title: { contains: "Entry" } },
|
||||
{ title: { contains: "Help Desk" } },
|
||||
{ title: { contains: "Support" } },
|
||||
{ title: { contains: "Technician" } },
|
||||
{ title: { contains: "Associate" } },
|
||||
{ title: { contains: "Assistant" } },
|
||||
{ title: { contains: "Trainee" } },
|
||||
{ title: { contains: "Intern" } },
|
||||
{ title: { contains: "Apprentice" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
NOT: [
|
||||
{ experienceLevel: { in: ["Mid-Level", "Senior", "Lead / Staff", "Executive"] } },
|
||||
{ title: { contains: "Senior" } },
|
||||
{ title: { contains: "Lead" } },
|
||||
{ title: { contains: "Director" } },
|
||||
{ title: { contains: "Principal" } },
|
||||
{ title: { contains: "Manager" } },
|
||||
{ title: { contains: "Head of" } },
|
||||
{ title: { contains: "VP" } },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ export default function EmployerATSPage() {
|
|||
{selectedAppIds.length > 0 && (
|
||||
<div className="bg-zinc-900 text-white p-3.5 rounded-md flex items-center justify-between text-xs shadow-md">
|
||||
<span className="font-semibold font-mono">
|
||||
✓ {selectedAppIds.length} candidate{selectedAppIds.length > 1 ? "s" : ""} selected
|
||||
{selectedAppIds.length} candidate{selectedAppIds.length > 1 ? "s" : ""} selected
|
||||
</span>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
@ -380,7 +380,7 @@ export default function EmployerATSPage() {
|
|||
|
||||
{snapshot && (
|
||||
<span className="text-[9px] font-mono bg-emerald-50 text-emerald-700 border border-emerald-200 px-1.5 py-0.5 rounded font-bold shrink-0">
|
||||
Snapshot ✓
|
||||
Snapshot Saved
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ export default function JobDetailPage() {
|
|||
<button
|
||||
onClick={() => {
|
||||
if (!isAuthenticated) {
|
||||
router.push(`/login?callbackUrl=/jobs/${jobId}`);
|
||||
window.location.href = "/login";
|
||||
return;
|
||||
}
|
||||
setShowApplyModal(true);
|
||||
|
|
@ -324,7 +324,7 @@ export default function JobDetailPage() {
|
|||
: "bg-stone-900 hover:bg-stone-800 text-stone-50 border border-stone-900"
|
||||
}`}
|
||||
>
|
||||
{isApplied ? "Application Submitted ✓" : "Apply for Role →"}
|
||||
{isApplied ? "Application Submitted" : "Apply for Role →"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
|
@ -422,7 +422,7 @@ export default function JobDetailPage() {
|
|||
<div className="flex flex-wrap gap-1">
|
||||
{compatibility.strongMatches.map((m: string, i: number) => (
|
||||
<span key={i} className="bg-emerald-950/60 text-emerald-300 border border-emerald-800/60 text-[10px] font-mono px-1.5 py-0.5 rounded">
|
||||
✓ {m}
|
||||
{m}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -524,7 +524,7 @@ export default function JobDetailPage() {
|
|||
: "bg-stone-900 hover:bg-stone-800 text-stone-50 border border-stone-900"
|
||||
}`}
|
||||
>
|
||||
{isApplied ? "Application Submitted ✓" : "Apply for Role →"}
|
||||
{isApplied ? "Application Submitted" : "Apply for Role →"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ export function JobCard({ job, onStatusChange, isAuthenticated }: JobCardProps)
|
|||
: "bg-stone-900 text-stone-50 border-stone-900 hover:bg-stone-800"
|
||||
}`}
|
||||
>
|
||||
{currentStatus === "APPLIED" ? "Applied ✓" : "Apply →"}
|
||||
{currentStatus === "APPLIED" ? "Applied" : "Apply →"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -131,10 +131,10 @@ export function NotificationBell() {
|
|||
{!item.isRead && (
|
||||
<button
|
||||
onClick={() => handleMarkAsRead(item.id)}
|
||||
className="text-[10px] text-zinc-400 hover:text-zinc-700 ml-2"
|
||||
className="text-[10px] text-zinc-400 hover:text-zinc-700 ml-2 font-mono"
|
||||
title="Mark read"
|
||||
>
|
||||
✓
|
||||
[read]
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { IJobSourceAdapter, DiscoveryResult, SourceHealth, RateLimitPolicy, NormalizedJobRecord } from "../types";
|
||||
import { generateContentFingerprint, normalizeJobUrl } from "../deduplication";
|
||||
import { generateContentFingerprint, normalizeJobUrl, classifyExperienceLevel } from "../deduplication";
|
||||
|
||||
export class AshbySourceAdapter implements IJobSourceAdapter {
|
||||
readonly id: string;
|
||||
|
|
@ -89,11 +89,7 @@ export class AshbySourceAdapter implements IJobSourceAdapter {
|
|||
location,
|
||||
isRemote,
|
||||
department: deptName,
|
||||
experienceLevel: title.toLowerCase().includes("senior")
|
||||
? "Senior"
|
||||
: title.toLowerCase().includes("lead")
|
||||
? "Lead / Staff"
|
||||
: "Mid-Level",
|
||||
experienceLevel: classifyExperienceLevel(title),
|
||||
description: cleanDesc.slice(0, 5000) || `Position at ${this.name}. Apply on Ashby.`,
|
||||
jobUrl,
|
||||
datePosted: item.publishedAt ? new Date(item.publishedAt) : new Date(),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { IJobSourceAdapter, DiscoveryResult, SourceHealth, RateLimitPolicy, NormalizedJobRecord } from "../types";
|
||||
import { generateJobUrlHash, generateContentFingerprint, normalizeJobUrl } from "../deduplication";
|
||||
import { generateJobUrlHash, generateContentFingerprint, normalizeJobUrl, classifyExperienceLevel } from "../deduplication";
|
||||
|
||||
export class GreenhouseSourceAdapter implements IJobSourceAdapter {
|
||||
readonly id: string;
|
||||
|
|
@ -94,11 +94,7 @@ export class GreenhouseSourceAdapter implements IJobSourceAdapter {
|
|||
location,
|
||||
isRemote,
|
||||
department: deptName,
|
||||
experienceLevel: title.toLowerCase().includes("senior")
|
||||
? "Senior"
|
||||
: title.toLowerCase().includes("lead")
|
||||
? "Lead / Staff"
|
||||
: "Mid-Level",
|
||||
experienceLevel: classifyExperienceLevel(title),
|
||||
description: cleanDesc.slice(0, 5000) || `Position at ${this.name}. Apply on company careers portal.`,
|
||||
jobUrl,
|
||||
datePosted: item.updated_at ? new Date(item.updated_at) : new Date(),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { IJobSourceAdapter, DiscoveryResult, SourceHealth, RateLimitPolicy, NormalizedJobRecord } from "../types";
|
||||
import { generateContentFingerprint, normalizeJobUrl } from "../deduplication";
|
||||
import { generateContentFingerprint, normalizeJobUrl, classifyExperienceLevel } from "../deduplication";
|
||||
|
||||
export class LeverSourceAdapter implements IJobSourceAdapter {
|
||||
readonly id: string;
|
||||
|
|
@ -97,11 +97,7 @@ export class LeverSourceAdapter implements IJobSourceAdapter {
|
|||
location,
|
||||
isRemote,
|
||||
department: deptName,
|
||||
experienceLevel: title.toLowerCase().includes("senior")
|
||||
? "Senior"
|
||||
: title.toLowerCase().includes("lead")
|
||||
? "Lead / Staff"
|
||||
: "Mid-Level",
|
||||
experienceLevel: classifyExperienceLevel(title),
|
||||
description: cleanDesc.slice(0, 5000) || `Position at ${this.name}. Apply on Lever.`,
|
||||
jobUrl,
|
||||
datePosted: item.createdAt ? new Date(item.createdAt) : new Date(),
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ export function calculateJobQualityScore(job: {
|
|||
else if (score >= 75) tier = "GOOD";
|
||||
else if (score >= 55) tier = "QUESTIONABLE";
|
||||
|
||||
return {
|
||||
return {
|
||||
score,
|
||||
tier,
|
||||
breakdown: {
|
||||
|
|
@ -221,3 +221,23 @@ export function calculateJobQualityScore(job: {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies experience level accurately based on title keywords
|
||||
*/
|
||||
export function classifyExperienceLevel(title: string): string {
|
||||
const t = title.toLowerCase();
|
||||
if (/\b(chief|vp|vice president|head of|director)\b/i.test(t)) {
|
||||
return "Executive";
|
||||
}
|
||||
if (/\b(lead|staff|principal|architect|manager)\b/i.test(t)) {
|
||||
return "Lead / Staff";
|
||||
}
|
||||
if (/\b(senior|sr\.?|sr\b)/i.test(t)) {
|
||||
return "Senior";
|
||||
}
|
||||
if (/\b(junior|jr\.?|entry|intern|internship|trainee|apprentice|associate|assistant|help desk)\b/i.test(t)) {
|
||||
return "Entry Level";
|
||||
}
|
||||
return "Mid-Level";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue