From 3b836103145ae3b2b35ded8ab3dc596686cae605 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 25 Feb 2026 07:27:14 -0800 Subject: [PATCH] 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 --- config/user.yaml.example | 6 +++++ scripts/db.py | 49 +++++++++++++++++++++++++------------- scripts/task_runner.py | 10 ++++---- scripts/user_profile.py | 15 ++++++++++++ tests/test_db.py | 18 ++++++++++++++ tests/test_user_profile.py | 22 +++++++++++++++++ 6 files changed, 100 insertions(+), 20 deletions(-) diff --git a/config/user.yaml.example b/config/user.yaml.example index c015a98..d088a27 100644 --- a/config/user.yaml.example +++ b/config/user.yaml.example @@ -30,6 +30,12 @@ candidate_accessibility_focus: false # Adds an LGBTQIA+ inclusion section (ERGs, non-discrimination policies, culture signals). 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" ollama_models_dir: "~/models/ollama" vllm_models_dir: "~/models/vllm" diff --git a/scripts/db.py b/scripts/db.py index b2443a1..6cf888f 100644 --- a/scripts/db.py +++ b/scripts/db.py @@ -84,7 +84,8 @@ CREATE_BACKGROUND_TASKS = """ CREATE TABLE IF NOT EXISTS background_tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_type TEXT NOT NULL, - job_id INTEGER NOT NULL, + job_id INTEGER DEFAULT 0, + params TEXT, status TEXT NOT NULL DEFAULT 'queued', error TEXT, 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") except sqlite3.OperationalError: pass + try: + conn.execute("ALTER TABLE background_tasks ADD COLUMN params TEXT") + except sqlite3.OperationalError: + pass # column already exists conn.commit() conn.close() @@ -641,28 +646,40 @@ def get_survey_responses(db_path: Path = DEFAULT_DB, job_id: int = None) -> list # ── Background task helpers ─────────────────────────────────────────────────── 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. Returns (task_id, True) if inserted, or (existing_id, False) if a 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) - 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: + try: + if params is not None: + existing = conn.execute( + "SELECT id FROM background_tasks WHERE task_type=? AND job_id=? " + "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() - 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, diff --git a/scripts/task_runner.py b/scripts/task_runner.py index 9e6cafd..956c1bf 100644 --- a/scripts/task_runner.py +++ b/scripts/task_runner.py @@ -24,24 +24,26 @@ from scripts.db import ( 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. 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. """ - 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: t = threading.Thread( 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, ) t.start() 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.""" # job_id == 0 means a global task (e.g. discovery) with no associated job row. job: dict = {} diff --git a/scripts/user_profile.py b/scripts/user_profile.py index a7b340f..1e4981b 100644 --- a/scripts/user_profile.py +++ b/scripts/user_profile.py @@ -23,6 +23,11 @@ _DEFAULTS = { "mission_preferences": {}, "candidate_accessibility_focus": False, "candidate_lgbtq_focus": False, + "tier": "free", + "dev_tier_override": None, + "wizard_complete": False, + "wizard_step": 0, + "dismissed_banners": [], "services": { "streamlit_port": 8501, "ollama_host": "localhost", @@ -64,6 +69,11 @@ class UserProfile: self.mission_preferences: dict[str, str] = data.get("mission_preferences", {}) 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.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"] # ── Service URLs ────────────────────────────────────────────────────────── @@ -90,6 +100,11 @@ class UserProfile: """Return ssl_verify flag for a named service (ollama/vllm/searxng).""" 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 ─────────────────────────────────────────────────────────── def is_nda(self, company: str) -> bool: return company.lower() in self.nda_companies diff --git a/tests/test_db.py b/tests/test_db.py index 9d83b8d..62ea2db 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -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()) conn.close() 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 diff --git a/tests/test_user_profile.py b/tests/test_user_profile.py index 6950dd5..88c4c88 100644 --- a/tests/test_user_profile.py +++ b/tests/test_user_profile.py @@ -84,3 +84,25 @@ def test_docs_dir_expanded(profile_yaml): p = UserProfile(profile_yaml) assert not str(p.docs_dir).startswith("~") 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"