Skip to content

Commit c57cd79

Browse files
authored
fix: improve HN gig lead classification and contacts (#383)
* fix: improve HN job lead classification * fix: clarify HN lead actions in dashboard * fix: address HN lead review feedback * fix: prioritize job URLs over generic paths
1 parent 406f177 commit c57cd79

13 files changed

Lines changed: 1014 additions & 49 deletions

File tree

apps/admin_dashboard/src/dashboard-utils.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
formatDate,
77
githubUrl,
88
isTerminalJobStatus,
9+
jobLeadClassificationMethodLabel,
910
labelForOnboardingState,
1011
linkedinUrl,
1112
onboardingStateValue,
@@ -61,4 +62,11 @@ describe("dashboard utility helpers", () => {
6162
expect(isTerminalJobStatus("running")).toBe(false)
6263
expect(isTerminalJobStatus(undefined)).toBe(false)
6364
})
65+
66+
it("labels job lead classification provenance without guessing", () => {
67+
expect(jobLeadClassificationMethodLabel("llm")).toBe("LLM")
68+
expect(jobLeadClassificationMethodLabel("heuristic")).toBe("Keyword fallback")
69+
expect(jobLeadClassificationMethodLabel("unknown")).toBe("Unknown")
70+
expect(jobLeadClassificationMethodLabel()).toBe("Unknown")
71+
})
6472
})

apps/admin_dashboard/src/dashboard-utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ export function isTerminalJobStatus(value?: string | null) {
6060
)
6161
}
6262

63+
export function jobLeadClassificationMethodLabel(value?: string | null) {
64+
if (value === "llm") return "LLM"
65+
if (value === "heuristic") return "Keyword fallback"
66+
return "Unknown"
67+
}
68+
6369
export function onboardingStateValue(person: OnboardingStateCarrier) {
6470
return person.onboarding_state || person.onboardingState || person.cOnboardingState || ""
6571
}

apps/admin_dashboard/src/main.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
formatDate,
4545
githubUrl,
4646
isTerminalJobStatus,
47+
jobLeadClassificationMethodLabel,
4748
jsonPreview,
4849
labelForOnboardingState,
4950
linkedinUrl,
@@ -255,6 +256,7 @@ type JobLead = {
255256
confidence_label?: string
256257
rationale?: string
257258
method?: string
259+
contact_email?: string | null
258260
}
259261
review_summary?: string
260262
reviewed_by_discord_user_id?: string
@@ -5595,12 +5597,19 @@ function defaultJobPostTagNames(lead: JobLead, channel?: JobPostChannel) {
55955597

55965598
function jobLeadClassificationLabel(lead: JobLead) {
55975599
const method = lead.contractor_classification?.method
5598-
const confidenceLabel = lead.contractor_classification?.confidence_label
5599-
if (method && confidenceLabel) {
5600-
const methodLabel = method === "llm" ? "LLM" : "Keyword fallback"
5601-
return `${methodLabel}: ${confidenceLabel} fit`
5602-
}
5603-
return lead.review_summary || "Classification unavailable"
5600+
const postingType = lead.contractor_classification?.posting_type || lead.posting_type
5601+
const postingTypeLabel =
5602+
{
5603+
part_time: "Part-time / contract",
5604+
full_time: "Full-time",
5605+
part_time_or_full_time: "Full-time or part-time / contract",
5606+
unknown: "Employment type unknown",
5607+
}[String(postingType || "")] || "Employment type unknown"
5608+
if (method) {
5609+
const methodLabel = jobLeadClassificationMethodLabel(method)
5610+
return `${postingTypeLabel} · ${methodLabel}`
5611+
}
5612+
return postingType ? postingTypeLabel : lead.review_summary || "Classification unavailable"
56045613
}
56055614

56065615
function JobLeadListItem({
@@ -5659,6 +5668,7 @@ function JobLeadListItem({
56595668
lead.discord_guild_id,
56605669
)}/${encodeURIComponent(lead.discord_thread_id)}`
56615670
: ""
5671+
const contactEmail = lead.contractor_classification?.contact_email
56625672
return (
56635673
<article className="grid gap-4 rounded-md border bg-background p-4 lg:grid-cols-[minmax(0,1fr)_220px_190px] lg:items-start">
56645674
<div className="min-w-0">
@@ -5670,9 +5680,6 @@ function JobLeadListItem({
56705680
</div>
56715681
<div className="mt-2 flex flex-wrap gap-1.5">
56725682
{lead.organization ? <Badge variant="neutral">{lead.organization}</Badge> : null}
5673-
{lead.posting_type ? (
5674-
<Badge variant="neutral">{titleCase(lead.posting_type)}</Badge>
5675-
) : null}
56765683
{lead.location ? <Badge variant="neutral">{lead.location}</Badge> : null}
56775684
{(lead.tags || []).slice(0, 6).map((tag) => (
56785685
<Badge key={tag} variant="queued">
@@ -5707,10 +5714,18 @@ function JobLeadListItem({
57075714
target="_blank"
57085715
rel="noreferrer"
57095716
>
5710-
Apply
5717+
Apply website
57115718
<ExternalLink className="size-3.5" />
57125719
</a>
57135720
) : null}
5721+
{contactEmail ? (
5722+
<a
5723+
className="inline-flex items-center gap-1 font-extrabold text-primary"
5724+
href={`mailto:${contactEmail}`}
5725+
>
5726+
Email {contactEmail}
5727+
</a>
5728+
) : null}
57145729
{discordUrl ? (
57155730
<a
57165731
className="inline-flex items-center gap-1 font-extrabold text-primary"

apps/api/src/five08/backend/static/dashboard/.vite/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"index.html": {
3-
"file": "assets/index-DCDV0WK0.js",
3+
"file": "assets/index-LL0oAOC_.js",
44
"name": "index",
55
"src": "index.html",
66
"isEntry": true,

apps/api/src/five08/backend/static/dashboard/assets/index-DCDV0WK0.js renamed to apps/api/src/five08/backend/static/dashboard/assets/index-LL0oAOC_.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/api/src/five08/backend/static/dashboard/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>508 Operations Dashboard</title>
7-
<script type="module" crossorigin src="/dashboard/assets/index-DCDV0WK0.js"></script>
7+
<script type="module" crossorigin src="/dashboard/assets/index-LL0oAOC_.js"></script>
88
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-2UypYUrJ.css">
99
</head>
1010
<body>

docs/discord-gig-dashboard.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,21 @@ log in and see only gigs they originally posted, based on
5454
Dashboard gig mutations also require `lifecycle_stage = 'pending_gig'`, so the
5555
gig controls cannot mutate future non-gig project engagement rows by id.
5656

57+
## Hacker News Job Lead Review
58+
59+
The Gigs page can scrape employer posts from Hacker News “Who is hiring?”
60+
threads into a review queue. Each lead shows an explicit employment type such
61+
as `Full-time`, `Part-time / contract`, or `Full-time or part-time / contract`,
62+
plus whether the result came from the LLM classifier or keyword fallback.
63+
64+
When a post contains them, the review card exposes the role-specific
65+
application page and a direct contact email as separate actions. Model-proposed
66+
links and email addresses are accepted only when they appear verbatim in the
67+
source post; deterministic scoring prefers application pages over company
68+
homepages. Re-scraping can correct pending or rejected leads that were
69+
previously admitted by a keyword false positive, while approved or posted leads
70+
remain unchanged.
71+
5772
## Candidate Sources
5873

5974
`engagement_applications.source` records how a candidate entered the gig flow:

0 commit comments

Comments
 (0)