diff --git a/backend/app/routers/quizzes.py b/backend/app/routers/quizzes.py index 8212a09..c745239 100644 --- a/backend/app/routers/quizzes.py +++ b/backend/app/routers/quizzes.py @@ -122,6 +122,32 @@ def list_user_jobs(current_user: User = Depends(get_current_user)): return jobs +@router.delete("/jobs/{job_id}", status_code=204) +def forget_job(job_id: str, current_user: User = Depends(get_current_user)): + """Take one job off this person's list. + + A job is a record of work, not the work itself: removing it stops nothing + that is running and deletes nothing it produced. It exists because the list + is a list of what happened, and one somebody has read and dealt with is + clutter in front of the ones they have not. + + Only from your own list — the id alone is not authority to touch anybody + else's, and the keys behind it are shared. + """ + import redis as redis_lib + from app.config import settings + + r = redis_lib.from_url(settings.REDIS_URL, decode_responses=True) + key = f"extraction:user_jobs:{current_user.id}" + if not r.lrem(key, 0, job_id): + raise HTTPException(404, "That job is not in your list") + # The steps and status go with it. They are keyed by job id and nothing + # else points at them once the list entry is gone, so leaving them would + # simply be rubbish with an hour to live. + for suffix in ("status", "steps", "error", "job_title", "batch_id", "quiz_id", "article"): + r.delete(f"extraction:{suffix}:{job_id}") + + @router.get("/job/{job_id}") def get_extraction_job(job_id: str, current_user: User = Depends(require_moderator)): """Poll extraction job progress. Returns the steps, the status, and — when it diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 5a6a36e..023a08c 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -93,76 +93,6 @@ function FeedbackBadge() { } -function JobsBadge({ jobs }) { - const [open, setOpen] = useState(false) - const wrap = useRef(null) - const allJobs = jobs - const activeJobs = jobs.filter(j => j.status === 'running' || j.status === 'pending') - - // Anywhere outside closes it, and so does Escape. It only closed when a job - // in it was clicked, so a panel opened to check on something then sat over - // the page until you found the badge again. - useEffect(() => { - if (!open) return undefined - const away = event => { if (!wrap.current?.contains(event.target)) setOpen(false) } - const onKey = event => { if (event.key === 'Escape') setOpen(false) } - document.addEventListener('mousedown', away) - document.addEventListener('keydown', onKey) - return () => { - document.removeEventListener('mousedown', away) - document.removeEventListener('keydown', onKey) - } - }, [open]) - - if (allJobs.length === 0) return null - - const running = activeJobs.length - return ( -
- History of quiz extractions — including skipped questions and chunk details. + Everything the machine has been asked to do for you — extractions, AI + drafts, card generation. Open the log to see what it did, step by + step, and why it stopped if it stopped. Jobs age out of this list on + their own after a day.
A document goes in, a model proposes questions, you read them, and the ones worth keeping move into the bank. Nothing is in the bank until