feat: wizard fields in UserProfile + params column in background_tasks

- Add tier, dev_tier_override, wizard_complete, wizard_step, dismissed_banners
  fields to UserProfile with defaults and effective_tier property
- Add params TEXT column to background_tasks table (CREATE + migration)
- Update insert_task() to accept params with params-aware dedup logic
- Update submit_task() and _run_task() to thread params through
- Add test_wizard_defaults, test_effective_tier_override,
  test_effective_tier_no_override, and test_insert_task_with_params
This commit is contained in:
pyr0ball 2026-02-25 07:27:14 -08:00
parent 1c39af564d
commit 450bfe1913
6 changed files with 100 additions and 20 deletions

View file

@ -30,6 +30,12 @@ candidate_accessibility_focus: false
# Adds an LGBTQIA+ inclusion section (ERGs, non-discrimination policies, culture signals). # Adds an LGBTQIA+ inclusion section (ERGs, non-discrimination policies, culture signals).
candidate_lgbtq_focus: false candidate_lgbtq_focus: false
tier: free # free | paid | premium
dev_tier_override: null # overrides tier locally (for testing only)
wizard_complete: false
wizard_step: 0
dismissed_banners: []
docs_dir: "~/Documents/JobSearch" docs_dir: "~/Documents/JobSearch"
ollama_models_dir: "~/models/ollama" ollama_models_dir: "~/models/ollama"
vllm_models_dir: "~/models/vllm" vllm_models_dir: "~/models/vllm"

View file

@ -84,7 +84,8 @@ CREATE_BACKGROUND_TASKS = """
CREATE TABLE IF NOT EXISTS background_tasks ( CREATE TABLE IF NOT EXISTS background_tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
task_type TEXT NOT NULL, task_type TEXT NOT NULL,
job_id INTEGER NOT NULL, job_id INTEGER DEFAULT 0,
params TEXT,
status TEXT NOT NULL DEFAULT 'queued', status TEXT NOT NULL DEFAULT 'queued',
error TEXT, error TEXT,
created_at DATETIME DEFAULT (datetime('now')), created_at DATETIME DEFAULT (datetime('now')),
@ -150,6 +151,10 @@ def _migrate_db(db_path: Path) -> None:
conn.execute("ALTER TABLE background_tasks ADD COLUMN updated_at TEXT") conn.execute("ALTER TABLE background_tasks ADD COLUMN updated_at TEXT")
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
try:
conn.execute("ALTER TABLE background_tasks ADD COLUMN params TEXT")
except sqlite3.OperationalError:
pass # column already exists
conn.commit() conn.commit()
conn.close() conn.close()
@ -641,28 +646,40 @@ def get_survey_responses(db_path: Path = DEFAULT_DB, job_id: int = None) -> list
# ── Background task helpers ─────────────────────────────────────────────────── # ── Background task helpers ───────────────────────────────────────────────────
def insert_task(db_path: Path = DEFAULT_DB, task_type: str = "", def insert_task(db_path: Path = DEFAULT_DB, task_type: str = "",
job_id: int = None) -> tuple[int, bool]: job_id: int = None,
params: Optional[str] = None) -> tuple[int, bool]:
"""Insert a new background task. """Insert a new background task.
Returns (task_id, True) if inserted, or (existing_id, False) if a Returns (task_id, True) if inserted, or (existing_id, False) if a
queued/running task for the same (task_type, job_id) already exists. queued/running task for the same (task_type, job_id) already exists.
Dedup key: (task_type, job_id) when params is None;
(task_type, job_id, params) when params is provided.
""" """
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
existing = conn.execute( try:
"SELECT id FROM background_tasks WHERE task_type=? AND job_id=? AND status IN ('queued','running')", if params is not None:
(task_type, job_id), existing = conn.execute(
).fetchone() "SELECT id FROM background_tasks WHERE task_type=? AND job_id=? "
if existing: "AND params=? AND status IN ('queued','running')",
(task_type, job_id, params),
).fetchone()
else:
existing = conn.execute(
"SELECT id FROM background_tasks WHERE task_type=? AND job_id=? "
"AND status IN ('queued','running')",
(task_type, job_id),
).fetchone()
if existing:
return existing[0], False
cur = conn.execute(
"INSERT INTO background_tasks (task_type, job_id, params) VALUES (?,?,?)",
(task_type, job_id, params),
)
conn.commit()
return cur.lastrowid, True
finally:
conn.close() conn.close()
return existing[0], False
cur = conn.execute(
"INSERT INTO background_tasks (task_type, job_id, status) VALUES (?, ?, 'queued')",
(task_type, job_id),
)
task_id = cur.lastrowid
conn.commit()
conn.close()
return task_id, True
def update_task_status(db_path: Path = DEFAULT_DB, task_id: int = None, def update_task_status(db_path: Path = DEFAULT_DB, task_id: int = None,

View file

@ -24,24 +24,26 @@ from scripts.db import (
def submit_task(db_path: Path = DEFAULT_DB, task_type: str = "", def submit_task(db_path: Path = DEFAULT_DB, task_type: str = "",
job_id: int = None) -> tuple[int, bool]: job_id: int = None,
params: str | None = None) -> tuple[int, bool]:
"""Submit a background LLM task. """Submit a background LLM task.
Returns (task_id, True) if a new task was queued and a thread spawned. Returns (task_id, True) if a new task was queued and a thread spawned.
Returns (existing_id, False) if an identical task is already in-flight. Returns (existing_id, False) if an identical task is already in-flight.
""" """
task_id, is_new = insert_task(db_path, task_type, job_id) task_id, is_new = insert_task(db_path, task_type, job_id or 0, params=params)
if is_new: if is_new:
t = threading.Thread( t = threading.Thread(
target=_run_task, target=_run_task,
args=(db_path, task_id, task_type, job_id), args=(db_path, task_id, task_type, job_id or 0, params),
daemon=True, daemon=True,
) )
t.start() t.start()
return task_id, is_new return task_id, is_new
def _run_task(db_path: Path, task_id: int, task_type: str, job_id: int) -> None: def _run_task(db_path: Path, task_id: int, task_type: str, job_id: int,
params: str | None = None) -> None:
"""Thread body: run the generator and persist the result.""" """Thread body: run the generator and persist the result."""
# job_id == 0 means a global task (e.g. discovery) with no associated job row. # job_id == 0 means a global task (e.g. discovery) with no associated job row.
job: dict = {} job: dict = {}

View file

@ -23,6 +23,11 @@ _DEFAULTS = {
"mission_preferences": {}, "mission_preferences": {},
"candidate_accessibility_focus": False, "candidate_accessibility_focus": False,
"candidate_lgbtq_focus": False, "candidate_lgbtq_focus": False,
"tier": "free",
"dev_tier_override": None,
"wizard_complete": False,
"wizard_step": 0,
"dismissed_banners": [],
"services": { "services": {
"streamlit_port": 8501, "streamlit_port": 8501,
"ollama_host": "localhost", "ollama_host": "localhost",
@ -64,6 +69,11 @@ class UserProfile:
self.mission_preferences: dict[str, str] = data.get("mission_preferences", {}) self.mission_preferences: dict[str, str] = data.get("mission_preferences", {})
self.candidate_accessibility_focus: bool = bool(data.get("candidate_accessibility_focus", False)) self.candidate_accessibility_focus: bool = bool(data.get("candidate_accessibility_focus", False))
self.candidate_lgbtq_focus: bool = bool(data.get("candidate_lgbtq_focus", False)) self.candidate_lgbtq_focus: bool = bool(data.get("candidate_lgbtq_focus", False))
self.tier: str = data.get("tier", "free")
self.dev_tier_override: str | None = data.get("dev_tier_override") or None
self.wizard_complete: bool = bool(data.get("wizard_complete", False))
self.wizard_step: int = int(data.get("wizard_step", 0))
self.dismissed_banners: list[str] = list(data.get("dismissed_banners", []))
self._svc = data["services"] self._svc = data["services"]
# ── Service URLs ────────────────────────────────────────────────────────── # ── Service URLs ──────────────────────────────────────────────────────────
@ -90,6 +100,11 @@ class UserProfile:
"""Return ssl_verify flag for a named service (ollama/vllm/searxng).""" """Return ssl_verify flag for a named service (ollama/vllm/searxng)."""
return bool(self._svc.get(f"{service}_ssl_verify", True)) return bool(self._svc.get(f"{service}_ssl_verify", True))
@property
def effective_tier(self) -> str:
"""Returns dev_tier_override if set, otherwise tier."""
return self.dev_tier_override or self.tier
# ── NDA helpers ─────────────────────────────────────────────────────────── # ── NDA helpers ───────────────────────────────────────────────────────────
def is_nda(self, company: str) -> bool: def is_nda(self, company: str) -> bool:
return company.lower() in self.nda_companies return company.lower() in self.nda_companies

View file

@ -558,3 +558,21 @@ def test_update_job_fields_ignores_unknown_columns(tmp_path):
row = dict(conn.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone()) row = dict(conn.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone())
conn.close() conn.close()
assert row["title"] == "Real Title" assert row["title"] == "Real Title"
def test_insert_task_with_params(tmp_path):
from scripts.db import init_db, insert_task
db = tmp_path / "t.db"
init_db(db)
import json
params = json.dumps({"section": "career_summary"})
task_id, is_new = insert_task(db, "wizard_generate", 0, params=params)
assert is_new is True
# Second call with same params = dedup
task_id2, is_new2 = insert_task(db, "wizard_generate", 0, params=params)
assert is_new2 is False
assert task_id == task_id2
# Different section = new task
params2 = json.dumps({"section": "job_titles"})
task_id3, is_new3 = insert_task(db, "wizard_generate", 0, params=params2)
assert is_new3 is True

View file

@ -84,3 +84,25 @@ def test_docs_dir_expanded(profile_yaml):
p = UserProfile(profile_yaml) p = UserProfile(profile_yaml)
assert not str(p.docs_dir).startswith("~") assert not str(p.docs_dir).startswith("~")
assert p.docs_dir.is_absolute() assert p.docs_dir.is_absolute()
def test_wizard_defaults(tmp_path):
p = tmp_path / "user.yaml"
p.write_text("name: Test\nemail: t@t.com\ncareer_summary: x\n")
u = UserProfile(p)
assert u.wizard_complete is False
assert u.wizard_step == 0
assert u.tier == "free"
assert u.dev_tier_override is None
assert u.dismissed_banners == []
def test_effective_tier_override(tmp_path):
p = tmp_path / "user.yaml"
p.write_text("name: T\nemail: t@t.com\ncareer_summary: x\ntier: free\ndev_tier_override: premium\n")
u = UserProfile(p)
assert u.effective_tier == "premium"
def test_effective_tier_no_override(tmp_path):
p = tmp_path / "user.yaml"
p.write_text("name: T\nemail: t@t.com\ncareer_summary: x\ntier: paid\n")
u = UserProfile(p)
assert u.effective_tier == "paid"