feat(peregrine): wire cloud_session into pages for multi-tenant db path routing
resolve_session() is a no-op in local mode — no behavior change for existing users. In cloud mode, injects user-scoped db_path into st.session_state at page load.
This commit is contained in:
parent
96715bdeb6
commit
59a6c1ebaf
5 changed files with 58 additions and 50 deletions
60
app/Home.py
60
app/Home.py
|
|
@ -18,12 +18,14 @@ _USER_YAML = Path(__file__).parent.parent / "config" / "user.yaml"
|
||||||
_profile = UserProfile(_USER_YAML) if UserProfile.exists(_USER_YAML) else None
|
_profile = UserProfile(_USER_YAML) if UserProfile.exists(_USER_YAML) else None
|
||||||
_name = _profile.name if _profile else "Job Seeker"
|
_name = _profile.name if _profile else "Job Seeker"
|
||||||
|
|
||||||
from scripts.db import DEFAULT_DB, init_db, get_job_counts, purge_jobs, purge_email_data, \
|
from scripts.db import init_db, get_job_counts, purge_jobs, purge_email_data, \
|
||||||
purge_non_remote, archive_jobs, kill_stuck_tasks, get_task_for_job, get_active_tasks, \
|
purge_non_remote, archive_jobs, kill_stuck_tasks, get_task_for_job, get_active_tasks, \
|
||||||
insert_job, get_existing_urls
|
insert_job, get_existing_urls
|
||||||
from scripts.task_runner import submit_task
|
from scripts.task_runner import submit_task
|
||||||
|
from app.cloud_session import resolve_session, get_db_path
|
||||||
|
|
||||||
init_db(DEFAULT_DB)
|
resolve_session("peregrine")
|
||||||
|
init_db(get_db_path())
|
||||||
|
|
||||||
def _email_configured() -> bool:
|
def _email_configured() -> bool:
|
||||||
_e = Path(__file__).parent.parent / "config" / "email.yaml"
|
_e = Path(__file__).parent.parent / "config" / "email.yaml"
|
||||||
|
|
@ -136,7 +138,7 @@ st.divider()
|
||||||
|
|
||||||
@st.fragment(run_every=10)
|
@st.fragment(run_every=10)
|
||||||
def _live_counts():
|
def _live_counts():
|
||||||
counts = get_job_counts(DEFAULT_DB)
|
counts = get_job_counts(get_db_path())
|
||||||
col1, col2, col3, col4, col5 = st.columns(5)
|
col1, col2, col3, col4, col5 = st.columns(5)
|
||||||
col1.metric("Pending Review", counts.get("pending", 0))
|
col1.metric("Pending Review", counts.get("pending", 0))
|
||||||
col2.metric("Approved", counts.get("approved", 0))
|
col2.metric("Approved", counts.get("approved", 0))
|
||||||
|
|
@ -155,18 +157,18 @@ with left:
|
||||||
st.subheader("Find New Jobs")
|
st.subheader("Find New Jobs")
|
||||||
st.caption("Scrapes all configured boards and adds new listings to your review queue.")
|
st.caption("Scrapes all configured boards and adds new listings to your review queue.")
|
||||||
|
|
||||||
_disc_task = get_task_for_job(DEFAULT_DB, "discovery", 0)
|
_disc_task = get_task_for_job(get_db_path(), "discovery", 0)
|
||||||
_disc_running = _disc_task and _disc_task["status"] in ("queued", "running")
|
_disc_running = _disc_task and _disc_task["status"] in ("queued", "running")
|
||||||
|
|
||||||
if st.button("🚀 Run Discovery", use_container_width=True, type="primary",
|
if st.button("🚀 Run Discovery", use_container_width=True, type="primary",
|
||||||
disabled=bool(_disc_running)):
|
disabled=bool(_disc_running)):
|
||||||
submit_task(DEFAULT_DB, "discovery", 0)
|
submit_task(get_db_path(), "discovery", 0)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if _disc_running:
|
if _disc_running:
|
||||||
@st.fragment(run_every=4)
|
@st.fragment(run_every=4)
|
||||||
def _disc_status():
|
def _disc_status():
|
||||||
t = get_task_for_job(DEFAULT_DB, "discovery", 0)
|
t = get_task_for_job(get_db_path(), "discovery", 0)
|
||||||
if t and t["status"] in ("queued", "running"):
|
if t and t["status"] in ("queued", "running"):
|
||||||
lbl = "Queued…" if t["status"] == "queued" else "Scraping job boards… this may take a minute"
|
lbl = "Queued…" if t["status"] == "queued" else "Scraping job boards… this may take a minute"
|
||||||
st.info(f"⏳ {lbl}")
|
st.info(f"⏳ {lbl}")
|
||||||
|
|
@ -184,18 +186,18 @@ with enrich_col:
|
||||||
st.subheader("Enrich Descriptions")
|
st.subheader("Enrich Descriptions")
|
||||||
st.caption("Re-fetch missing descriptions for any listing (LinkedIn, Indeed, Glassdoor, Adzuna, The Ladders, generic).")
|
st.caption("Re-fetch missing descriptions for any listing (LinkedIn, Indeed, Glassdoor, Adzuna, The Ladders, generic).")
|
||||||
|
|
||||||
_enrich_task = get_task_for_job(DEFAULT_DB, "enrich_descriptions", 0)
|
_enrich_task = get_task_for_job(get_db_path(), "enrich_descriptions", 0)
|
||||||
_enrich_running = _enrich_task and _enrich_task["status"] in ("queued", "running")
|
_enrich_running = _enrich_task and _enrich_task["status"] in ("queued", "running")
|
||||||
|
|
||||||
if st.button("🔍 Fill Missing Descriptions", use_container_width=True, type="primary",
|
if st.button("🔍 Fill Missing Descriptions", use_container_width=True, type="primary",
|
||||||
disabled=bool(_enrich_running)):
|
disabled=bool(_enrich_running)):
|
||||||
submit_task(DEFAULT_DB, "enrich_descriptions", 0)
|
submit_task(get_db_path(), "enrich_descriptions", 0)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if _enrich_running:
|
if _enrich_running:
|
||||||
@st.fragment(run_every=4)
|
@st.fragment(run_every=4)
|
||||||
def _enrich_status():
|
def _enrich_status():
|
||||||
t = get_task_for_job(DEFAULT_DB, "enrich_descriptions", 0)
|
t = get_task_for_job(get_db_path(), "enrich_descriptions", 0)
|
||||||
if t and t["status"] in ("queued", "running"):
|
if t and t["status"] in ("queued", "running"):
|
||||||
st.info("⏳ Fetching descriptions…")
|
st.info("⏳ Fetching descriptions…")
|
||||||
else:
|
else:
|
||||||
|
|
@ -210,7 +212,7 @@ with enrich_col:
|
||||||
|
|
||||||
with mid:
|
with mid:
|
||||||
unscored = sum(1 for j in __import__("scripts.db", fromlist=["get_jobs_by_status"])
|
unscored = sum(1 for j in __import__("scripts.db", fromlist=["get_jobs_by_status"])
|
||||||
.get_jobs_by_status(DEFAULT_DB, "pending")
|
.get_jobs_by_status(get_db_path(), "pending")
|
||||||
if j.get("match_score") is None and j.get("description"))
|
if j.get("match_score") is None and j.get("description"))
|
||||||
st.subheader("Score Listings")
|
st.subheader("Score Listings")
|
||||||
st.caption(f"Run TF-IDF match scoring against {_name}'s resume. {unscored} pending job{'s' if unscored != 1 else ''} unscored.")
|
st.caption(f"Run TF-IDF match scoring against {_name}'s resume. {unscored} pending job{'s' if unscored != 1 else ''} unscored.")
|
||||||
|
|
@ -231,7 +233,7 @@ with mid:
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
with right:
|
with right:
|
||||||
approved_count = get_job_counts(DEFAULT_DB).get("approved", 0)
|
approved_count = get_job_counts(get_db_path()).get("approved", 0)
|
||||||
st.subheader("Send to Notion")
|
st.subheader("Send to Notion")
|
||||||
st.caption("Push all approved jobs to your Notion tracking database.")
|
st.caption("Push all approved jobs to your Notion tracking database.")
|
||||||
if approved_count == 0:
|
if approved_count == 0:
|
||||||
|
|
@ -243,7 +245,7 @@ with right:
|
||||||
):
|
):
|
||||||
with st.spinner("Syncing to Notion…"):
|
with st.spinner("Syncing to Notion…"):
|
||||||
from scripts.sync import sync_to_notion
|
from scripts.sync import sync_to_notion
|
||||||
count = sync_to_notion(DEFAULT_DB)
|
count = sync_to_notion(get_db_path())
|
||||||
st.success(f"Synced {count} job{'s' if count != 1 else ''} to Notion!")
|
st.success(f"Synced {count} job{'s' if count != 1 else ''} to Notion!")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
|
|
@ -258,18 +260,18 @@ with email_left:
|
||||||
"New recruiter outreach is added to your Job Review queue.")
|
"New recruiter outreach is added to your Job Review queue.")
|
||||||
|
|
||||||
with email_right:
|
with email_right:
|
||||||
_email_task = get_task_for_job(DEFAULT_DB, "email_sync", 0)
|
_email_task = get_task_for_job(get_db_path(), "email_sync", 0)
|
||||||
_email_running = _email_task and _email_task["status"] in ("queued", "running")
|
_email_running = _email_task and _email_task["status"] in ("queued", "running")
|
||||||
|
|
||||||
if st.button("📧 Sync Emails", use_container_width=True, type="primary",
|
if st.button("📧 Sync Emails", use_container_width=True, type="primary",
|
||||||
disabled=bool(_email_running)):
|
disabled=bool(_email_running)):
|
||||||
submit_task(DEFAULT_DB, "email_sync", 0)
|
submit_task(get_db_path(), "email_sync", 0)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if _email_running:
|
if _email_running:
|
||||||
@st.fragment(run_every=4)
|
@st.fragment(run_every=4)
|
||||||
def _email_status():
|
def _email_status():
|
||||||
t = get_task_for_job(DEFAULT_DB, "email_sync", 0)
|
t = get_task_for_job(get_db_path(), "email_sync", 0)
|
||||||
if t and t["status"] in ("queued", "running"):
|
if t and t["status"] in ("queued", "running"):
|
||||||
st.info("⏳ Syncing emails…")
|
st.info("⏳ Syncing emails…")
|
||||||
else:
|
else:
|
||||||
|
|
@ -304,7 +306,7 @@ with url_tab:
|
||||||
disabled=not (url_text or "").strip()):
|
disabled=not (url_text or "").strip()):
|
||||||
_urls = [u.strip() for u in url_text.strip().splitlines() if u.strip().startswith("http")]
|
_urls = [u.strip() for u in url_text.strip().splitlines() if u.strip().startswith("http")]
|
||||||
if _urls:
|
if _urls:
|
||||||
_n = _queue_url_imports(DEFAULT_DB, _urls)
|
_n = _queue_url_imports(get_db_path(), _urls)
|
||||||
if _n:
|
if _n:
|
||||||
st.success(f"Queued {_n} job{'s' if _n != 1 else ''} for import. Check Job Review shortly.")
|
st.success(f"Queued {_n} job{'s' if _n != 1 else ''} for import. Check Job Review shortly.")
|
||||||
else:
|
else:
|
||||||
|
|
@ -327,7 +329,7 @@ with csv_tab:
|
||||||
if _csv_urls:
|
if _csv_urls:
|
||||||
st.caption(f"Found {len(_csv_urls)} URL(s) in CSV.")
|
st.caption(f"Found {len(_csv_urls)} URL(s) in CSV.")
|
||||||
if st.button("📥 Import CSV Jobs", key="add_csv_btn", use_container_width=True):
|
if st.button("📥 Import CSV Jobs", key="add_csv_btn", use_container_width=True):
|
||||||
_n = _queue_url_imports(DEFAULT_DB, _csv_urls)
|
_n = _queue_url_imports(get_db_path(),_csv_urls)
|
||||||
st.success(f"Queued {_n} job{'s' if _n != 1 else ''} for import.")
|
st.success(f"Queued {_n} job{'s' if _n != 1 else ''} for import.")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
else:
|
else:
|
||||||
|
|
@ -337,7 +339,7 @@ with csv_tab:
|
||||||
@st.fragment(run_every=3)
|
@st.fragment(run_every=3)
|
||||||
def _scrape_status():
|
def _scrape_status():
|
||||||
import sqlite3 as _sq
|
import sqlite3 as _sq
|
||||||
conn = _sq.connect(DEFAULT_DB)
|
conn = _sq.connect(get_db_path())
|
||||||
conn.row_factory = _sq.Row
|
conn.row_factory = _sq.Row
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""SELECT bt.status, bt.error, j.title, j.company, j.url
|
"""SELECT bt.status, bt.error, j.title, j.company, j.url
|
||||||
|
|
@ -384,7 +386,7 @@ with st.expander("⚠️ Danger Zone", expanded=False):
|
||||||
st.warning("Are you sure? This cannot be undone.")
|
st.warning("Are you sure? This cannot be undone.")
|
||||||
c1, c2 = st.columns(2)
|
c1, c2 = st.columns(2)
|
||||||
if c1.button("Yes, purge", type="primary", use_container_width=True):
|
if c1.button("Yes, purge", type="primary", use_container_width=True):
|
||||||
deleted = purge_jobs(DEFAULT_DB, statuses=["pending", "rejected"])
|
deleted = purge_jobs(get_db_path(), statuses=["pending", "rejected"])
|
||||||
st.success(f"Purged {deleted} jobs.")
|
st.success(f"Purged {deleted} jobs.")
|
||||||
st.session_state.pop("confirm_purge", None)
|
st.session_state.pop("confirm_purge", None)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
@ -402,7 +404,7 @@ with st.expander("⚠️ Danger Zone", expanded=False):
|
||||||
st.warning("This deletes all email contacts and email-sourced jobs. Cannot be undone.")
|
st.warning("This deletes all email contacts and email-sourced jobs. Cannot be undone.")
|
||||||
c1, c2 = st.columns(2)
|
c1, c2 = st.columns(2)
|
||||||
if c1.button("Yes, purge emails", type="primary", use_container_width=True):
|
if c1.button("Yes, purge emails", type="primary", use_container_width=True):
|
||||||
contacts, jobs = purge_email_data(DEFAULT_DB)
|
contacts, jobs = purge_email_data(get_db_path())
|
||||||
st.success(f"Purged {contacts} email contacts, {jobs} email jobs.")
|
st.success(f"Purged {contacts} email contacts, {jobs} email jobs.")
|
||||||
st.session_state.pop("confirm_purge", None)
|
st.session_state.pop("confirm_purge", None)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
@ -411,11 +413,11 @@ with st.expander("⚠️ Danger Zone", expanded=False):
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
with tasks_col:
|
with tasks_col:
|
||||||
_active = get_active_tasks(DEFAULT_DB)
|
_active = get_active_tasks(get_db_path())
|
||||||
st.markdown("**Kill stuck tasks**")
|
st.markdown("**Kill stuck tasks**")
|
||||||
st.caption(f"Force-fail all queued/running background tasks. Currently **{len(_active)}** active.")
|
st.caption(f"Force-fail all queued/running background tasks. Currently **{len(_active)}** active.")
|
||||||
if st.button("⏹ Kill All Tasks", use_container_width=True, disabled=len(_active) == 0):
|
if st.button("⏹ Kill All Tasks", use_container_width=True, disabled=len(_active) == 0):
|
||||||
killed = kill_stuck_tasks(DEFAULT_DB)
|
killed = kill_stuck_tasks(get_db_path())
|
||||||
st.success(f"Killed {killed} task(s).")
|
st.success(f"Killed {killed} task(s).")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
|
|
@ -429,8 +431,8 @@ with st.expander("⚠️ Danger Zone", expanded=False):
|
||||||
st.warning("This will delete ALL pending, approved, and rejected jobs, then re-scrape. Applied and synced records are kept.")
|
st.warning("This will delete ALL pending, approved, and rejected jobs, then re-scrape. Applied and synced records are kept.")
|
||||||
c1, c2 = st.columns(2)
|
c1, c2 = st.columns(2)
|
||||||
if c1.button("Yes, wipe + scrape", type="primary", use_container_width=True):
|
if c1.button("Yes, wipe + scrape", type="primary", use_container_width=True):
|
||||||
purge_jobs(DEFAULT_DB, statuses=["pending", "approved", "rejected"])
|
purge_jobs(get_db_path(), statuses=["pending", "approved", "rejected"])
|
||||||
submit_task(DEFAULT_DB, "discovery", 0)
|
submit_task(get_db_path(), "discovery", 0)
|
||||||
st.session_state.pop("confirm_purge", None)
|
st.session_state.pop("confirm_purge", None)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
if c2.button("Cancel ", use_container_width=True):
|
if c2.button("Cancel ", use_container_width=True):
|
||||||
|
|
@ -451,7 +453,7 @@ with st.expander("⚠️ Danger Zone", expanded=False):
|
||||||
st.warning("Deletes all pending jobs. Rejected jobs are kept. Cannot be undone.")
|
st.warning("Deletes all pending jobs. Rejected jobs are kept. Cannot be undone.")
|
||||||
c1, c2 = st.columns(2)
|
c1, c2 = st.columns(2)
|
||||||
if c1.button("Yes, purge pending", type="primary", use_container_width=True):
|
if c1.button("Yes, purge pending", type="primary", use_container_width=True):
|
||||||
deleted = purge_jobs(DEFAULT_DB, statuses=["pending"])
|
deleted = purge_jobs(get_db_path(), statuses=["pending"])
|
||||||
st.success(f"Purged {deleted} pending jobs.")
|
st.success(f"Purged {deleted} pending jobs.")
|
||||||
st.session_state.pop("confirm_purge", None)
|
st.session_state.pop("confirm_purge", None)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
@ -469,7 +471,7 @@ with st.expander("⚠️ Danger Zone", expanded=False):
|
||||||
st.warning("Deletes all non-remote jobs not yet applied to. Cannot be undone.")
|
st.warning("Deletes all non-remote jobs not yet applied to. Cannot be undone.")
|
||||||
c1, c2 = st.columns(2)
|
c1, c2 = st.columns(2)
|
||||||
if c1.button("Yes, purge on-site", type="primary", use_container_width=True):
|
if c1.button("Yes, purge on-site", type="primary", use_container_width=True):
|
||||||
deleted = purge_non_remote(DEFAULT_DB)
|
deleted = purge_non_remote(get_db_path())
|
||||||
st.success(f"Purged {deleted} non-remote jobs.")
|
st.success(f"Purged {deleted} non-remote jobs.")
|
||||||
st.session_state.pop("confirm_purge", None)
|
st.session_state.pop("confirm_purge", None)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
@ -487,7 +489,7 @@ with st.expander("⚠️ Danger Zone", expanded=False):
|
||||||
st.warning("Deletes all approved-but-not-applied jobs. Cannot be undone.")
|
st.warning("Deletes all approved-but-not-applied jobs. Cannot be undone.")
|
||||||
c1, c2 = st.columns(2)
|
c1, c2 = st.columns(2)
|
||||||
if c1.button("Yes, purge approved", type="primary", use_container_width=True):
|
if c1.button("Yes, purge approved", type="primary", use_container_width=True):
|
||||||
deleted = purge_jobs(DEFAULT_DB, statuses=["approved"])
|
deleted = purge_jobs(get_db_path(), statuses=["approved"])
|
||||||
st.success(f"Purged {deleted} approved jobs.")
|
st.success(f"Purged {deleted} approved jobs.")
|
||||||
st.session_state.pop("confirm_purge", None)
|
st.session_state.pop("confirm_purge", None)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
@ -512,7 +514,7 @@ with st.expander("⚠️ Danger Zone", expanded=False):
|
||||||
st.info("Jobs will be archived (not deleted) — URLs are kept for dedup.")
|
st.info("Jobs will be archived (not deleted) — URLs are kept for dedup.")
|
||||||
c1, c2 = st.columns(2)
|
c1, c2 = st.columns(2)
|
||||||
if c1.button("Yes, archive", type="primary", use_container_width=True):
|
if c1.button("Yes, archive", type="primary", use_container_width=True):
|
||||||
archived = archive_jobs(DEFAULT_DB, statuses=["pending", "rejected"])
|
archived = archive_jobs(get_db_path(), statuses=["pending", "rejected"])
|
||||||
st.success(f"Archived {archived} jobs.")
|
st.success(f"Archived {archived} jobs.")
|
||||||
st.session_state.pop("confirm_purge", None)
|
st.session_state.pop("confirm_purge", None)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
@ -530,7 +532,7 @@ with st.expander("⚠️ Danger Zone", expanded=False):
|
||||||
st.info("Approved jobs will be archived (not deleted).")
|
st.info("Approved jobs will be archived (not deleted).")
|
||||||
c1, c2 = st.columns(2)
|
c1, c2 = st.columns(2)
|
||||||
if c1.button("Yes, archive approved", type="primary", use_container_width=True):
|
if c1.button("Yes, archive approved", type="primary", use_container_width=True):
|
||||||
archived = archive_jobs(DEFAULT_DB, statuses=["approved"])
|
archived = archive_jobs(get_db_path(), statuses=["approved"])
|
||||||
st.success(f"Archived {archived} approved jobs.")
|
st.success(f"Archived {archived} approved jobs.")
|
||||||
st.session_state.pop("confirm_purge", None)
|
st.session_state.pop("confirm_purge", None)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
|
||||||
10
app/app.py
10
app/app.py
|
|
@ -22,6 +22,7 @@ IS_DEMO = os.environ.get("DEMO_MODE", "").lower() in ("1", "true", "yes")
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
from scripts.db import DEFAULT_DB, init_db, get_active_tasks
|
from scripts.db import DEFAULT_DB, init_db, get_active_tasks
|
||||||
from app.feedback import inject_feedback_button
|
from app.feedback import inject_feedback_button
|
||||||
|
from app.cloud_session import resolve_session, get_db_path
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
|
|
@ -30,7 +31,8 @@ st.set_page_config(
|
||||||
layout="wide",
|
layout="wide",
|
||||||
)
|
)
|
||||||
|
|
||||||
init_db(DEFAULT_DB)
|
resolve_session("peregrine")
|
||||||
|
init_db(get_db_path())
|
||||||
|
|
||||||
# ── Startup cleanup — runs once per server process via cache_resource ──────────
|
# ── Startup cleanup — runs once per server process via cache_resource ──────────
|
||||||
@st.cache_resource
|
@st.cache_resource
|
||||||
|
|
@ -40,7 +42,7 @@ def _startup() -> None:
|
||||||
2. Auto-queues re-runs for any research generated without SearXNG data,
|
2. Auto-queues re-runs for any research generated without SearXNG data,
|
||||||
if SearXNG is now reachable.
|
if SearXNG is now reachable.
|
||||||
"""
|
"""
|
||||||
conn = sqlite3.connect(DEFAULT_DB)
|
conn = sqlite3.connect(get_db_path())
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE background_tasks SET status='failed', error='Interrupted by server restart',"
|
"UPDATE background_tasks SET status='failed', error='Interrupted by server restart',"
|
||||||
" finished_at=datetime('now') WHERE status IN ('queued','running')"
|
" finished_at=datetime('now') WHERE status IN ('queued','running')"
|
||||||
|
|
@ -61,7 +63,7 @@ def _startup() -> None:
|
||||||
_ACTIVE_STAGES,
|
_ACTIVE_STAGES,
|
||||||
).fetchall()
|
).fetchall()
|
||||||
for (job_id,) in rows:
|
for (job_id,) in rows:
|
||||||
submit_task(str(DEFAULT_DB), "company_research", job_id)
|
submit_task(str(get_db_path()), "company_research", job_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # never block startup
|
pass # never block startup
|
||||||
|
|
||||||
|
|
@ -113,7 +115,7 @@ pg = st.navigation(pages)
|
||||||
# The sidebar context WRAPS the fragment call — do not write to st.sidebar inside it.
|
# The sidebar context WRAPS the fragment call — do not write to st.sidebar inside it.
|
||||||
@st.fragment(run_every=3)
|
@st.fragment(run_every=3)
|
||||||
def _task_indicator():
|
def _task_indicator():
|
||||||
tasks = get_active_tasks(DEFAULT_DB)
|
tasks = get_active_tasks(get_db_path())
|
||||||
if not tasks:
|
if not tasks:
|
||||||
return
|
return
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from app.cloud_session import resolve_session, get_db_path
|
||||||
|
resolve_session("peregrine")
|
||||||
|
|
||||||
_ROOT = Path(__file__).parent.parent.parent
|
_ROOT = Path(__file__).parent.parent.parent
|
||||||
CONFIG_DIR = _ROOT / "config"
|
CONFIG_DIR = _ROOT / "config"
|
||||||
USER_YAML = CONFIG_DIR / "user.yaml"
|
USER_YAML = CONFIG_DIR / "user.yaml"
|
||||||
|
|
@ -74,18 +77,16 @@ def _suggest_profile(gpus: list[str]) -> str:
|
||||||
|
|
||||||
def _submit_wizard_task(section: str, input_data: dict) -> int:
|
def _submit_wizard_task(section: str, input_data: dict) -> int:
|
||||||
"""Submit a wizard_generate background task. Returns task_id."""
|
"""Submit a wizard_generate background task. Returns task_id."""
|
||||||
from scripts.db import DEFAULT_DB
|
|
||||||
from scripts.task_runner import submit_task
|
from scripts.task_runner import submit_task
|
||||||
params = json.dumps({"section": section, "input": input_data})
|
params = json.dumps({"section": section, "input": input_data})
|
||||||
task_id, _ = submit_task(DEFAULT_DB, "wizard_generate", 0, params=params)
|
task_id, _ = submit_task(get_db_path(), "wizard_generate", 0, params=params)
|
||||||
return task_id
|
return task_id
|
||||||
|
|
||||||
|
|
||||||
def _poll_wizard_task(section: str) -> dict | None:
|
def _poll_wizard_task(section: str) -> dict | None:
|
||||||
"""Return the most recent wizard_generate task row for a given section, or None."""
|
"""Return the most recent wizard_generate task row for a given section, or None."""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from scripts.db import DEFAULT_DB
|
conn = sqlite3.connect(get_db_path())
|
||||||
conn = sqlite3.connect(DEFAULT_DB)
|
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT * FROM background_tasks "
|
"SELECT * FROM background_tasks "
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,13 @@ import yaml
|
||||||
import os as _os
|
import os as _os
|
||||||
|
|
||||||
from scripts.user_profile import UserProfile
|
from scripts.user_profile import UserProfile
|
||||||
|
from app.cloud_session import resolve_session, get_db_path
|
||||||
|
|
||||||
_USER_YAML = Path(__file__).parent.parent.parent / "config" / "user.yaml"
|
_USER_YAML = Path(__file__).parent.parent.parent / "config" / "user.yaml"
|
||||||
_profile = UserProfile(_USER_YAML) if UserProfile.exists(_USER_YAML) else None
|
_profile = UserProfile(_USER_YAML) if UserProfile.exists(_USER_YAML) else None
|
||||||
_name = _profile.name if _profile else "Job Seeker"
|
_name = _profile.name if _profile else "Job Seeker"
|
||||||
|
|
||||||
|
resolve_session("peregrine")
|
||||||
st.title("⚙️ Settings")
|
st.title("⚙️ Settings")
|
||||||
|
|
||||||
CONFIG_DIR = Path(__file__).parent.parent.parent / "config"
|
CONFIG_DIR = Path(__file__).parent.parent.parent / "config"
|
||||||
|
|
@ -1371,12 +1373,11 @@ with tab_finetune:
|
||||||
st.markdown("**Step 2: Extract Training Pairs**")
|
st.markdown("**Step 2: Extract Training Pairs**")
|
||||||
import json as _json
|
import json as _json
|
||||||
import sqlite3 as _sqlite3
|
import sqlite3 as _sqlite3
|
||||||
from scripts.db import DEFAULT_DB as _FT_DB
|
|
||||||
|
|
||||||
jsonl_path = _profile.docs_dir / "training_data" / "cover_letters.jsonl"
|
jsonl_path = _profile.docs_dir / "training_data" / "cover_letters.jsonl"
|
||||||
|
|
||||||
# Show task status
|
# Show task status
|
||||||
_ft_conn = _sqlite3.connect(_FT_DB)
|
_ft_conn = _sqlite3.connect(get_db_path())
|
||||||
_ft_conn.row_factory = _sqlite3.Row
|
_ft_conn.row_factory = _sqlite3.Row
|
||||||
_ft_task = _ft_conn.execute(
|
_ft_task = _ft_conn.execute(
|
||||||
"SELECT * FROM background_tasks WHERE task_type='prepare_training' ORDER BY id DESC LIMIT 1"
|
"SELECT * FROM background_tasks WHERE task_type='prepare_training' ORDER BY id DESC LIMIT 1"
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,15 @@ from scripts.db import (
|
||||||
get_task_for_job,
|
get_task_for_job,
|
||||||
)
|
)
|
||||||
from scripts.task_runner import submit_task
|
from scripts.task_runner import submit_task
|
||||||
|
from app.cloud_session import resolve_session, get_db_path
|
||||||
|
|
||||||
DOCS_DIR = _profile.docs_dir if _profile else Path.home() / "Documents" / "JobSearch"
|
DOCS_DIR = _profile.docs_dir if _profile else Path.home() / "Documents" / "JobSearch"
|
||||||
RESUME_YAML = Path(__file__).parent.parent.parent / "config" / "plain_text_resume.yaml"
|
RESUME_YAML = Path(__file__).parent.parent.parent / "config" / "plain_text_resume.yaml"
|
||||||
|
|
||||||
st.title("🚀 Apply Workspace")
|
st.title("🚀 Apply Workspace")
|
||||||
|
|
||||||
init_db(DEFAULT_DB)
|
resolve_session("peregrine")
|
||||||
|
init_db(get_db_path())
|
||||||
|
|
||||||
# ── PDF generation ─────────────────────────────────────────────────────────────
|
# ── PDF generation ─────────────────────────────────────────────────────────────
|
||||||
def _make_cover_letter_pdf(job: dict, cover_letter: str, output_dir: Path) -> Path:
|
def _make_cover_letter_pdf(job: dict, cover_letter: str, output_dir: Path) -> Path:
|
||||||
|
|
@ -156,7 +158,7 @@ def _copy_btn(text: str, label: str = "📋 Copy", done: str = "✅ Copied!", he
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Job selection ──────────────────────────────────────────────────────────────
|
# ── Job selection ──────────────────────────────────────────────────────────────
|
||||||
approved = get_jobs_by_status(DEFAULT_DB, "approved")
|
approved = get_jobs_by_status(get_db_path(), "approved")
|
||||||
if not approved:
|
if not approved:
|
||||||
st.info("No approved jobs — head to Job Review to approve some listings first.")
|
st.info("No approved jobs — head to Job Review to approve some listings first.")
|
||||||
st.stop()
|
st.stop()
|
||||||
|
|
@ -219,17 +221,17 @@ with col_tools:
|
||||||
if _cl_key not in st.session_state:
|
if _cl_key not in st.session_state:
|
||||||
st.session_state[_cl_key] = job.get("cover_letter") or ""
|
st.session_state[_cl_key] = job.get("cover_letter") or ""
|
||||||
|
|
||||||
_cl_task = get_task_for_job(DEFAULT_DB, "cover_letter", selected_id)
|
_cl_task = get_task_for_job(get_db_path(), "cover_letter", selected_id)
|
||||||
_cl_running = _cl_task and _cl_task["status"] in ("queued", "running")
|
_cl_running = _cl_task and _cl_task["status"] in ("queued", "running")
|
||||||
|
|
||||||
if st.button("✨ Generate / Regenerate", use_container_width=True, disabled=bool(_cl_running)):
|
if st.button("✨ Generate / Regenerate", use_container_width=True, disabled=bool(_cl_running)):
|
||||||
submit_task(DEFAULT_DB, "cover_letter", selected_id)
|
submit_task(get_db_path(), "cover_letter", selected_id)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if _cl_running:
|
if _cl_running:
|
||||||
@st.fragment(run_every=3)
|
@st.fragment(run_every=3)
|
||||||
def _cl_status_fragment():
|
def _cl_status_fragment():
|
||||||
t = get_task_for_job(DEFAULT_DB, "cover_letter", selected_id)
|
t = get_task_for_job(get_db_path(), "cover_letter", selected_id)
|
||||||
if t and t["status"] in ("queued", "running"):
|
if t and t["status"] in ("queued", "running"):
|
||||||
lbl = "Queued…" if t["status"] == "queued" else "Generating via LLM…"
|
lbl = "Queued…" if t["status"] == "queued" else "Generating via LLM…"
|
||||||
st.info(f"⏳ {lbl}")
|
st.info(f"⏳ {lbl}")
|
||||||
|
|
@ -272,7 +274,7 @@ with col_tools:
|
||||||
key=f"cl_refine_{selected_id}"):
|
key=f"cl_refine_{selected_id}"):
|
||||||
import json as _json
|
import json as _json
|
||||||
submit_task(
|
submit_task(
|
||||||
DEFAULT_DB, "cover_letter", selected_id,
|
get_db_path(), "cover_letter", selected_id,
|
||||||
params=_json.dumps({
|
params=_json.dumps({
|
||||||
"previous_result": cl_text,
|
"previous_result": cl_text,
|
||||||
"feedback": feedback_text.strip(),
|
"feedback": feedback_text.strip(),
|
||||||
|
|
@ -288,7 +290,7 @@ with col_tools:
|
||||||
_copy_btn(cl_text, label="📋 Copy Letter")
|
_copy_btn(cl_text, label="📋 Copy Letter")
|
||||||
with c2:
|
with c2:
|
||||||
if st.button("💾 Save draft", use_container_width=True):
|
if st.button("💾 Save draft", use_container_width=True):
|
||||||
update_cover_letter(DEFAULT_DB, selected_id, cl_text)
|
update_cover_letter(get_db_path(), selected_id, cl_text)
|
||||||
st.success("Saved!")
|
st.success("Saved!")
|
||||||
|
|
||||||
# PDF generation
|
# PDF generation
|
||||||
|
|
@ -297,7 +299,7 @@ with col_tools:
|
||||||
with st.spinner("Generating PDF…"):
|
with st.spinner("Generating PDF…"):
|
||||||
try:
|
try:
|
||||||
pdf_path = _make_cover_letter_pdf(job, cl_text, DOCS_DIR)
|
pdf_path = _make_cover_letter_pdf(job, cl_text, DOCS_DIR)
|
||||||
update_cover_letter(DEFAULT_DB, selected_id, cl_text)
|
update_cover_letter(get_db_path(), selected_id, cl_text)
|
||||||
st.success(f"Saved: `{pdf_path.name}`")
|
st.success(f"Saved: `{pdf_path.name}`")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"PDF error: {e}")
|
st.error(f"PDF error: {e}")
|
||||||
|
|
@ -312,13 +314,13 @@ with col_tools:
|
||||||
with c4:
|
with c4:
|
||||||
if st.button("✅ Mark as Applied", use_container_width=True, type="primary"):
|
if st.button("✅ Mark as Applied", use_container_width=True, type="primary"):
|
||||||
if cl_text:
|
if cl_text:
|
||||||
update_cover_letter(DEFAULT_DB, selected_id, cl_text)
|
update_cover_letter(get_db_path(), selected_id, cl_text)
|
||||||
mark_applied(DEFAULT_DB, [selected_id])
|
mark_applied(get_db_path(), [selected_id])
|
||||||
st.success("Marked as applied!")
|
st.success("Marked as applied!")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if st.button("🚫 Reject listing", use_container_width=True):
|
if st.button("🚫 Reject listing", use_container_width=True):
|
||||||
update_job_status(DEFAULT_DB, [selected_id], "rejected")
|
update_job_status(get_db_path(), [selected_id], "rejected")
|
||||||
# Advance selectbox to next job so list doesn't snap to first item
|
# Advance selectbox to next job so list doesn't snap to first item
|
||||||
current_idx = ids.index(selected_id) if selected_id in ids else 0
|
current_idx = ids.index(selected_id) if selected_id in ids else 0
|
||||||
if current_idx + 1 < len(ids):
|
if current_idx + 1 < len(ids):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue