fix: prevent blank page on rebuild and queue drain on skip/discard

Two bugs fixed:

1. Blank white page after vue SPA rebuild: browsers cached old index.html
   referencing old asset hashes. Assets are deleted on rebuild, causing
   404s for JS/CSS -> blank page. Fix: serve index.html with
   Cache-Control: no-cache so browsers always fetch fresh HTML.
   Hashed assets (/assets/chunk-abc123.js) remain cacheable forever.

2. Queue draining to empty on skip/discard: handleSkip and handleDiscard
   never refilled the local queue buffer. After enough skips, store.current
   went null and the empty state showed (blank-looking). Fix: both handlers
   now call fetchBatch() when queue drops below 3, matching handleLabel.

Also: sync classifier_adapters LABELS to match current 10-label schema
(new_lead + hired, remove unrelated).

48 Python tests pass, 48 frontend tests pass.
This commit is contained in:
pyr0ball 2026-03-03 19:26:34 -08:00
parent a06b133a6e
commit 82eeb4defc
4 changed files with 22 additions and 5 deletions

View file

@ -209,5 +209,16 @@ def get_labels():
# Static SPA — MUST be last (catches all unmatched paths) # Static SPA — MUST be last (catches all unmatched paths)
_DIST = _ROOT / "web" / "dist" _DIST = _ROOT / "web" / "dist"
if _DIST.exists(): if _DIST.exists():
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
# Serve index.html with no-cache so browsers always fetch fresh HTML after rebuilds.
# Hashed assets (/assets/index-abc123.js) can be cached forever — they change names
# when content changes (standard Vite cache-busting strategy).
_NO_CACHE = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache"}
@app.get("/")
def get_spa_root():
return FileResponse(str(_DIST / "index.html"), headers=_NO_CACHE)
app.mount("/", StaticFiles(directory=str(_DIST), html=True), name="spa") app.mount("/", StaticFiles(directory=str(_DIST), html=True), name="spa")

View file

@ -27,8 +27,9 @@ LABELS: list[str] = [
"survey_received", "survey_received",
"neutral", "neutral",
"event_rescheduled", "event_rescheduled",
"unrelated",
"digest", "digest",
"new_lead",
"hired",
] ]
# Natural-language descriptions used by the RerankerAdapter. # Natural-language descriptions used by the RerankerAdapter.
@ -40,8 +41,9 @@ LABEL_DESCRIPTIONS: dict[str, str] = {
"survey_received": "invitation to complete a culture-fit survey or assessment", "survey_received": "invitation to complete a culture-fit survey or assessment",
"neutral": "automated ATS confirmation such as application received", "neutral": "automated ATS confirmation such as application received",
"event_rescheduled": "an interview or scheduled event moved to a new time", "event_rescheduled": "an interview or scheduled event moved to a new time",
"unrelated": "non-job-search email unrelated to any application or recruiter",
"digest": "job digest or multi-listing email with multiple job postings", "digest": "job digest or multi-listing email with multiple job postings",
"new_lead": "unsolicited recruiter outreach or cold contact about a new opportunity",
"hired": "job offer accepted, onboarding logistics, welcome email, or start date confirmation",
} }
# Lazy import shims — allow tests to patch without requiring the libs installed. # Lazy import shims — allow tests to patch without requiring the libs installed.

View file

@ -2,14 +2,16 @@
import pytest import pytest
def test_labels_constant_has_nine_items(): def test_labels_constant_has_ten_items():
from scripts.classifier_adapters import LABELS from scripts.classifier_adapters import LABELS
assert len(LABELS) == 9 assert len(LABELS) == 10
assert "interview_scheduled" in LABELS assert "interview_scheduled" in LABELS
assert "neutral" in LABELS assert "neutral" in LABELS
assert "event_rescheduled" in LABELS assert "event_rescheduled" in LABELS
assert "unrelated" in LABELS
assert "digest" in LABELS assert "digest" in LABELS
assert "new_lead" in LABELS
assert "hired" in LABELS
assert "unrelated" not in LABELS
def test_compute_metrics_perfect_predictions(): def test_compute_metrics_perfect_predictions():

View file

@ -211,6 +211,7 @@ async function handleSkip() {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: item.id }), body: JSON.stringify({ id: item.id }),
}) })
if (store.queue.length < 3) await fetchBatch()
} }
async function handleDiscard() { async function handleDiscard() {
@ -228,6 +229,7 @@ async function handleDiscard() {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: item.id }), body: JSON.stringify({ id: item.id }),
}) })
if (store.queue.length < 3) await fetchBatch()
} }
async function handleUndo() { async function handleUndo() {