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
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
from scrapers.ats_ingestion import determine_experience_level
|
||||||
|
|
||||||
def run_ct_jobaps_scrape() -> List[Dict[Any, Any]]:
|
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",
|
"location": "Hartford & CT Statewide",
|
||||||
"is_remote": False,
|
"is_remote": False,
|
||||||
"department": "Government & Public Services",
|
"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}",
|
"description": f"Official State of Connecticut position. Application details available at {full_url}",
|
||||||
"salary_min": None,
|
"salary_min": None,
|
||||||
"salary_max": None,
|
"salary_max": None,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import os
|
||||||
import time
|
import time
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
from scrapers.ats_ingestion import determine_experience_level, determine_department
|
||||||
|
|
||||||
def get_jobspy_proxies():
|
def get_jobspy_proxies():
|
||||||
proxies = get_privado_proxy_list()
|
proxies = get_privado_proxy_list()
|
||||||
|
|
@ -105,11 +106,14 @@ def run_jobspy_scrapes() -> List[Dict[Any, Any]]:
|
||||||
if loc_val == "nan":
|
if loc_val == "nan":
|
||||||
loc_val = loc
|
loc_val = loc
|
||||||
|
|
||||||
|
title_str = str(row.get("title", "Untitled"))
|
||||||
collected_jobs.append({
|
collected_jobs.append({
|
||||||
"title": str(row.get("title", "Untitled")),
|
"title": title_str,
|
||||||
"company": str(row.get("company", "Unknown")),
|
"company": str(row.get("company", "Unknown")),
|
||||||
"location": loc_val,
|
"location": loc_val,
|
||||||
"is_remote": False,
|
"is_remote": False,
|
||||||
|
"department": determine_department(title_str, ""),
|
||||||
|
"experience_level": determine_experience_level(title_str),
|
||||||
"description": str(row.get("description", "") or "No description provided."),
|
"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_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,
|
"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":
|
if not job_url or job_url == "nan":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
title_str = str(row.get("title", "Untitled"))
|
||||||
collected_jobs.append({
|
collected_jobs.append({
|
||||||
"title": str(row.get("title", "Untitled")),
|
"title": title_str,
|
||||||
"company": str(row.get("company", "Unknown")),
|
"company": str(row.get("company", "Unknown")),
|
||||||
"location": "Remote, USA",
|
"location": "Remote, USA",
|
||||||
"is_remote": True,
|
"is_remote": True,
|
||||||
|
"department": determine_department(title_str, ""),
|
||||||
|
"experience_level": determine_experience_level(title_str),
|
||||||
"description": str(row.get("description", "") or "No description provided."),
|
"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_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,
|
"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 ? (
|
{pendingReviews.length === 0 ? (
|
||||||
<div className="py-6 text-center text-xs text-zinc-400 font-mono">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -809,7 +809,7 @@ export default function AdminPage() {
|
||||||
|
|
||||||
{reportedReviews.length === 0 ? (
|
{reportedReviews.length === 0 ? (
|
||||||
<div className="py-6 text-center text-xs text-zinc-400 font-mono">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -870,7 +870,7 @@ export default function AdminPage() {
|
||||||
|
|
||||||
{claims.length === 0 ? (
|
{claims.length === 0 ? (
|
||||||
<div className="py-8 text-center text-xs text-zinc-400 font-mono">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -964,7 +964,7 @@ export default function AdminPage() {
|
||||||
|
|
||||||
{auditLogs.length === 0 ? (
|
{auditLogs.length === 0 ? (
|
||||||
<div className="py-8 text-center text-xs text-zinc-400 font-mono">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
|
|
||||||
|
|
@ -150,14 +150,26 @@ export async function GET(req: Request) {
|
||||||
{
|
{
|
||||||
OR: [
|
OR: [
|
||||||
{ experienceLevel: "Entry Level" },
|
{ experienceLevel: "Entry Level" },
|
||||||
{ experienceLevel: "Mid-Level" },
|
|
||||||
{ title: { contains: "Junior" } },
|
{ title: { contains: "Junior" } },
|
||||||
{ title: { contains: "Entry" } },
|
{ title: { contains: "Entry" } },
|
||||||
{ title: { contains: "Help Desk" } },
|
{ title: { contains: "Help Desk" } },
|
||||||
{ title: { contains: "Support" } },
|
|
||||||
{ title: { contains: "Technician" } },
|
{ title: { contains: "Technician" } },
|
||||||
{ title: { contains: "Associate" } },
|
|
||||||
{ title: { contains: "Assistant" } },
|
{ 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 && (
|
{selectedAppIds.length > 0 && (
|
||||||
<div className="bg-zinc-900 text-white p-3.5 rounded-md flex items-center justify-between text-xs shadow-md">
|
<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">
|
<span className="font-semibold font-mono">
|
||||||
✓ {selectedAppIds.length} candidate{selectedAppIds.length > 1 ? "s" : ""} selected
|
{selectedAppIds.length} candidate{selectedAppIds.length > 1 ? "s" : ""} selected
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
@ -380,7 +380,7 @@ export default function EmployerATSPage() {
|
||||||
|
|
||||||
{snapshot && (
|
{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">
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -312,7 +312,7 @@ export default function JobDetailPage() {
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
router.push(`/login?callbackUrl=/jobs/${jobId}`);
|
window.location.href = "/login";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setShowApplyModal(true);
|
setShowApplyModal(true);
|
||||||
|
|
@ -324,7 +324,7 @@ export default function JobDetailPage() {
|
||||||
: "bg-stone-900 hover:bg-stone-800 text-stone-50 border border-stone-900"
|
: "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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -422,7 +422,7 @@ export default function JobDetailPage() {
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{compatibility.strongMatches.map((m: string, i: number) => (
|
{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">
|
<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>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -524,7 +524,7 @@ export default function JobDetailPage() {
|
||||||
: "bg-stone-900 hover:bg-stone-800 text-stone-50 border border-stone-900"
|
: "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>
|
||||||
</div>
|
</div>
|
||||||
</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"
|
: "bg-stone-900 text-stone-50 border-stone-900 hover:bg-stone-800"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{currentStatus === "APPLIED" ? "Applied ✓" : "Apply →"}
|
{currentStatus === "APPLIED" ? "Applied" : "Apply →"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -131,10 +131,10 @@ export function NotificationBell() {
|
||||||
{!item.isRead && (
|
{!item.isRead && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMarkAsRead(item.id)}
|
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"
|
title="Mark read"
|
||||||
>
|
>
|
||||||
✓
|
[read]
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IJobSourceAdapter, DiscoveryResult, SourceHealth, RateLimitPolicy, NormalizedJobRecord } from "../types";
|
import { IJobSourceAdapter, DiscoveryResult, SourceHealth, RateLimitPolicy, NormalizedJobRecord } from "../types";
|
||||||
import { generateContentFingerprint, normalizeJobUrl } from "../deduplication";
|
import { generateContentFingerprint, normalizeJobUrl, classifyExperienceLevel } from "../deduplication";
|
||||||
|
|
||||||
export class AshbySourceAdapter implements IJobSourceAdapter {
|
export class AshbySourceAdapter implements IJobSourceAdapter {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
|
@ -89,11 +89,7 @@ export class AshbySourceAdapter implements IJobSourceAdapter {
|
||||||
location,
|
location,
|
||||||
isRemote,
|
isRemote,
|
||||||
department: deptName,
|
department: deptName,
|
||||||
experienceLevel: title.toLowerCase().includes("senior")
|
experienceLevel: classifyExperienceLevel(title),
|
||||||
? "Senior"
|
|
||||||
: title.toLowerCase().includes("lead")
|
|
||||||
? "Lead / Staff"
|
|
||||||
: "Mid-Level",
|
|
||||||
description: cleanDesc.slice(0, 5000) || `Position at ${this.name}. Apply on Ashby.`,
|
description: cleanDesc.slice(0, 5000) || `Position at ${this.name}. Apply on Ashby.`,
|
||||||
jobUrl,
|
jobUrl,
|
||||||
datePosted: item.publishedAt ? new Date(item.publishedAt) : new Date(),
|
datePosted: item.publishedAt ? new Date(item.publishedAt) : new Date(),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IJobSourceAdapter, DiscoveryResult, SourceHealth, RateLimitPolicy, NormalizedJobRecord } from "../types";
|
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 {
|
export class GreenhouseSourceAdapter implements IJobSourceAdapter {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
|
@ -94,11 +94,7 @@ export class GreenhouseSourceAdapter implements IJobSourceAdapter {
|
||||||
location,
|
location,
|
||||||
isRemote,
|
isRemote,
|
||||||
department: deptName,
|
department: deptName,
|
||||||
experienceLevel: title.toLowerCase().includes("senior")
|
experienceLevel: classifyExperienceLevel(title),
|
||||||
? "Senior"
|
|
||||||
: title.toLowerCase().includes("lead")
|
|
||||||
? "Lead / Staff"
|
|
||||||
: "Mid-Level",
|
|
||||||
description: cleanDesc.slice(0, 5000) || `Position at ${this.name}. Apply on company careers portal.`,
|
description: cleanDesc.slice(0, 5000) || `Position at ${this.name}. Apply on company careers portal.`,
|
||||||
jobUrl,
|
jobUrl,
|
||||||
datePosted: item.updated_at ? new Date(item.updated_at) : new Date(),
|
datePosted: item.updated_at ? new Date(item.updated_at) : new Date(),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IJobSourceAdapter, DiscoveryResult, SourceHealth, RateLimitPolicy, NormalizedJobRecord } from "../types";
|
import { IJobSourceAdapter, DiscoveryResult, SourceHealth, RateLimitPolicy, NormalizedJobRecord } from "../types";
|
||||||
import { generateContentFingerprint, normalizeJobUrl } from "../deduplication";
|
import { generateContentFingerprint, normalizeJobUrl, classifyExperienceLevel } from "../deduplication";
|
||||||
|
|
||||||
export class LeverSourceAdapter implements IJobSourceAdapter {
|
export class LeverSourceAdapter implements IJobSourceAdapter {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
|
@ -97,11 +97,7 @@ export class LeverSourceAdapter implements IJobSourceAdapter {
|
||||||
location,
|
location,
|
||||||
isRemote,
|
isRemote,
|
||||||
department: deptName,
|
department: deptName,
|
||||||
experienceLevel: title.toLowerCase().includes("senior")
|
experienceLevel: classifyExperienceLevel(title),
|
||||||
? "Senior"
|
|
||||||
: title.toLowerCase().includes("lead")
|
|
||||||
? "Lead / Staff"
|
|
||||||
: "Mid-Level",
|
|
||||||
description: cleanDesc.slice(0, 5000) || `Position at ${this.name}. Apply on Lever.`,
|
description: cleanDesc.slice(0, 5000) || `Position at ${this.name}. Apply on Lever.`,
|
||||||
jobUrl,
|
jobUrl,
|
||||||
datePosted: item.createdAt ? new Date(item.createdAt) : new Date(),
|
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 >= 75) tier = "GOOD";
|
||||||
else if (score >= 55) tier = "QUESTIONABLE";
|
else if (score >= 55) tier = "QUESTIONABLE";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
score,
|
score,
|
||||||
tier,
|
tier,
|
||||||
breakdown: {
|
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