From 82eeb4defc9a51c5466fff8cb3b4fc219224bf9b Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 19:26:34 -0800 Subject: [PATCH] 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. --- app/api.py | 11 +++++++++++ scripts/classifier_adapters.py | 6 ++++-- tests/test_classifier_adapters.py | 8 +++++--- web/src/views/LabelView.vue | 2 ++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/api.py b/app/api.py index ef30eda..30ddc56 100644 --- a/app/api.py +++ b/app/api.py @@ -209,5 +209,16 @@ def get_labels(): # Static SPA — MUST be last (catches all unmatched paths) _DIST = _ROOT / "web" / "dist" if _DIST.exists(): + from fastapi.responses import FileResponse 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") diff --git a/scripts/classifier_adapters.py b/scripts/classifier_adapters.py index 2817078..5704de1 100644 --- a/scripts/classifier_adapters.py +++ b/scripts/classifier_adapters.py @@ -27,8 +27,9 @@ LABELS: list[str] = [ "survey_received", "neutral", "event_rescheduled", - "unrelated", "digest", + "new_lead", + "hired", ] # 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", "neutral": "automated ATS confirmation such as application received", "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", + "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. diff --git a/tests/test_classifier_adapters.py b/tests/test_classifier_adapters.py index f50ef3b..85f9dc1 100644 --- a/tests/test_classifier_adapters.py +++ b/tests/test_classifier_adapters.py @@ -2,14 +2,16 @@ import pytest -def test_labels_constant_has_nine_items(): +def test_labels_constant_has_ten_items(): from scripts.classifier_adapters import LABELS - assert len(LABELS) == 9 + assert len(LABELS) == 10 assert "interview_scheduled" in LABELS assert "neutral" in LABELS assert "event_rescheduled" in LABELS - assert "unrelated" 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(): diff --git a/web/src/views/LabelView.vue b/web/src/views/LabelView.vue index 246bcf7..1925bfc 100644 --- a/web/src/views/LabelView.vue +++ b/web/src/views/LabelView.vue @@ -211,6 +211,7 @@ async function handleSkip() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: item.id }), }) + if (store.queue.length < 3) await fetchBatch() } async function handleDiscard() { @@ -228,6 +229,7 @@ async function handleDiscard() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: item.id }), }) + if (store.queue.length < 3) await fetchBatch() } async function handleUndo() {