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:
parent
a06b133a6e
commit
82eeb4defc
4 changed files with 22 additions and 5 deletions
11
app/api.py
11
app/api.py
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue