peregrine/app/pages/1_Job_Review.py
pyr0ball 9702646738 fix(cloud): replace DEFAULT_DB with get_db_path() across all Streamlit pages
Pages were hardcoding DEFAULT_DB at import time, meaning cloud-mode
per-user DB routing was silently ignored. Pages affected:
1_Job_Review, 5_Interviews, 6_Interview_Prep, 7_Survey.

Adds resolve_session("peregrine") + get_db_path() pattern to each,
matching the pattern already used in 4_Apply.py.

Fixes #24.
2026-04-01 07:09:35 -07:00

206 lines
9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# app/pages/1_Job_Review.py
"""
Job Review — browse listings, approve/reject inline, generate cover letters,
and mark approved jobs as applied.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
import streamlit as st
from scripts.db import (
DEFAULT_DB, init_db, get_jobs_by_status, update_job_status,
update_cover_letter, mark_applied, get_email_leads,
)
from app.cloud_session import resolve_session, get_db_path
resolve_session("peregrine")
st.title("📋 Job Review")
init_db(get_db_path())
_email_leads = get_email_leads(get_db_path())
# ── Sidebar filters ────────────────────────────────────────────────────────────
with st.sidebar:
st.header("Filters")
show_status = st.selectbox(
"Show",
["pending", "approved", "applied", "rejected", "synced"],
index=0,
)
remote_only = st.checkbox("Remote only", value=False)
min_score = st.slider("Min match score", 0, 100, 0)
st.header("Sort")
sort_by = st.selectbox(
"Sort by",
["Date Found (newest)", "Date Found (oldest)", "Match Score (high→low)", "Match Score (low→high)", "Company AZ", "Title AZ"],
index=0,
)
jobs = get_jobs_by_status(get_db_path(), show_status)
if remote_only:
jobs = [j for j in jobs if j.get("is_remote")]
if min_score > 0:
jobs = [j for j in jobs if (j.get("match_score") or 0) >= min_score]
# Apply sort
if sort_by == "Date Found (newest)":
jobs = sorted(jobs, key=lambda j: j.get("date_found") or "", reverse=True)
elif sort_by == "Date Found (oldest)":
jobs = sorted(jobs, key=lambda j: j.get("date_found") or "")
elif sort_by == "Match Score (high→low)":
jobs = sorted(jobs, key=lambda j: j.get("match_score") or 0, reverse=True)
elif sort_by == "Match Score (low→high)":
jobs = sorted(jobs, key=lambda j: j.get("match_score") or 0)
elif sort_by == "Company AZ":
jobs = sorted(jobs, key=lambda j: (j.get("company") or "").lower())
elif sort_by == "Title AZ":
jobs = sorted(jobs, key=lambda j: (j.get("title") or "").lower())
if not jobs:
st.info(f"No {show_status} jobs matching your filters.")
st.stop()
st.caption(f"Showing {len(jobs)} {show_status} job{'s' if len(jobs) != 1 else ''}")
st.divider()
if show_status == "pending" and _email_leads:
st.subheader(f"📧 Email Leads ({len(_email_leads)})")
st.caption(
"Inbound recruiter emails not yet matched to a scraped listing. "
"Approve to add to Job Review; Reject to dismiss."
)
for lead in _email_leads:
lead_id = lead["id"]
with st.container(border=True):
left_l, right_l = st.columns([7, 3])
with left_l:
st.markdown(f"**{lead['title']}** — {lead['company']}")
badge_cols = st.columns(4)
badge_cols[0].caption("📧 Email Lead")
badge_cols[1].caption(f"📅 {lead.get('date_found', '')}")
if lead.get("description"):
with st.expander("📄 Email excerpt", expanded=False):
st.text(lead["description"][:500])
with right_l:
if st.button("✅ Approve", key=f"el_approve_{lead_id}",
type="primary", use_container_width=True):
update_job_status(get_db_path(), [lead_id], "approved")
st.rerun()
if st.button("❌ Reject", key=f"el_reject_{lead_id}",
use_container_width=True):
update_job_status(get_db_path(), [lead_id], "rejected")
st.rerun()
st.divider()
# Filter email leads out of the main pending list (already shown above)
if show_status == "pending":
jobs = [j for j in jobs if j.get("source") != "email"]
# ── Job cards ──────────────────────────────────────────────────────────────────
for job in jobs:
job_id = job["id"]
score = job.get("match_score")
if score is None:
score_badge = "⬜ No score"
elif score >= 70:
score_badge = f"🟢 {score:.0f}%"
elif score >= 40:
score_badge = f"🟡 {score:.0f}%"
else:
score_badge = f"🔴 {score:.0f}%"
remote_badge = "🌐 Remote" if job.get("is_remote") else "🏢 On-site"
src = (job.get("source") or "").lower()
source_badge = f"🤖 {src.title()}" if src == "linkedin" else f"👤 {src.title() or 'Manual'}"
with st.container(border=True):
left, right = st.columns([7, 3])
# ── Left: job info ─────────────────────────────────────────────────────
with left:
st.markdown(f"**{job['title']}** — {job['company']}")
badge_cols = st.columns(4)
badge_cols[0].caption(remote_badge)
badge_cols[1].caption(source_badge)
badge_cols[2].caption(score_badge)
badge_cols[3].caption(f"📅 {job.get('date_found', '')}")
if job.get("keyword_gaps"):
st.caption(f"**Keyword gaps:** {job['keyword_gaps']}")
# Cover letter expander (approved view)
if show_status == "approved":
_cl_key = f"cl_{job_id}"
if _cl_key not in st.session_state:
st.session_state[_cl_key] = job.get("cover_letter") or ""
cl_exists = bool(st.session_state[_cl_key])
with st.expander("📝 Cover Letter", expanded=cl_exists):
gen_label = "Regenerate" if cl_exists else "Generate Cover Letter"
if st.button(gen_label, key=f"gen_{job_id}"):
with st.spinner("Generating via LLM…"):
try:
from scripts.generate_cover_letter import generate as _gen
st.session_state[_cl_key] = _gen(
job.get("title", ""),
job.get("company", ""),
job.get("description", ""),
)
st.rerun()
except Exception as e:
st.error(f"Generation failed: {e}")
st.text_area(
"cover_letter_edit",
key=_cl_key,
height=300,
label_visibility="collapsed",
)
save_col, _ = st.columns([2, 5])
if save_col.button("💾 Save draft", key=f"save_cl_{job_id}"):
update_cover_letter(get_db_path(), job_id, st.session_state[_cl_key])
st.success("Saved!")
# Applied date + cover letter preview (applied/synced)
if show_status in ("applied", "synced") and job.get("applied_at"):
st.caption(f"✅ Applied: {job['applied_at']}")
if show_status in ("applied", "synced") and job.get("cover_letter"):
with st.expander("📝 Cover Letter (sent)"):
st.text(job["cover_letter"])
# ── Right: actions ─────────────────────────────────────────────────────
with right:
if job.get("url"):
st.link_button("View listing →", job["url"], use_container_width=True)
if job.get("salary"):
st.caption(f"💰 {job['salary']}")
if show_status == "pending":
if st.button("✅ Approve", key=f"approve_{job_id}",
type="primary", use_container_width=True):
update_job_status(get_db_path(), [job_id], "approved")
st.rerun()
if st.button("❌ Reject", key=f"reject_{job_id}",
use_container_width=True):
update_job_status(get_db_path(), [job_id], "rejected")
st.rerun()
elif show_status == "approved":
if st.button("🚀 Apply →", key=f"apply_page_{job_id}",
type="primary", use_container_width=True):
st.session_state["apply_job_id"] = job_id
st.switch_page("pages/4_Apply.py")
if st.button("✅ Mark Applied", key=f"applied_{job_id}",
use_container_width=True):
cl_text = st.session_state.get(f"cl_{job_id}", "")
if cl_text:
update_cover_letter(get_db_path(), job_id, cl_text)
mark_applied(get_db_path(), [job_id])
st.rerun()