fix: Settings widget crash, stale setup banners, Docker service controls
- Settings → Search: add-title (+) and Import buttons crashed with StreamlitAPIException when writing to _sp_titles_multi after it was already instantiated. Fix: pending-key pattern (_sp_titles_pending / _sp_locs_pending) applied before widget renders on next pass. - Home setup banners: fired for email/notion/keywords even when those features were already configured. Add 'done' condition callables (_email_configured, _notion_configured, _keywords_configured) to suppress banners automatically when config files are present. - Services tab start/stop buttons: docker CLI was unavailable inside the container so _docker_available was False and buttons never showed. Bind-mount host /usr/bin/docker (ro) + /var/run/docker.sock into the app container so it can control sibling containers via DooD pattern.
This commit is contained in:
parent
c8132bcb5c
commit
9229f9ce69
3 changed files with 48 additions and 7 deletions
41
app/Home.py
41
app/Home.py
|
|
@ -25,17 +25,45 @@ from scripts.task_runner import submit_task
|
|||
|
||||
init_db(DEFAULT_DB)
|
||||
|
||||
def _email_configured() -> bool:
|
||||
_e = Path(__file__).parent.parent / "config" / "email.yaml"
|
||||
if not _e.exists():
|
||||
return False
|
||||
import yaml as _yaml
|
||||
_cfg = _yaml.safe_load(_e.read_text()) or {}
|
||||
return bool(_cfg.get("username") or _cfg.get("user") or _cfg.get("imap_host"))
|
||||
|
||||
def _notion_configured() -> bool:
|
||||
_n = Path(__file__).parent.parent / "config" / "notion.yaml"
|
||||
if not _n.exists():
|
||||
return False
|
||||
import yaml as _yaml
|
||||
_cfg = _yaml.safe_load(_n.read_text()) or {}
|
||||
return bool(_cfg.get("token"))
|
||||
|
||||
def _keywords_configured() -> bool:
|
||||
_k = Path(__file__).parent.parent / "config" / "resume_keywords.yaml"
|
||||
if not _k.exists():
|
||||
return False
|
||||
import yaml as _yaml
|
||||
_cfg = _yaml.safe_load(_k.read_text()) or {}
|
||||
return bool(_cfg.get("keywords") or _cfg.get("required") or _cfg.get("preferred"))
|
||||
|
||||
_SETUP_BANNERS = [
|
||||
{"key": "connect_cloud", "text": "Connect a cloud service for resume/cover letter storage",
|
||||
"link_label": "Settings → Integrations"},
|
||||
"link_label": "Settings → Integrations",
|
||||
"done": _notion_configured},
|
||||
{"key": "setup_email", "text": "Set up email sync to catch recruiter outreach",
|
||||
"link_label": "Settings → Email"},
|
||||
"link_label": "Settings → Email",
|
||||
"done": _email_configured},
|
||||
{"key": "setup_email_labels", "text": "Set up email label filters for auto-classification",
|
||||
"link_label": "Settings → Email (label guide)"},
|
||||
"link_label": "Settings → Email (label guide)",
|
||||
"done": _email_configured},
|
||||
{"key": "tune_mission", "text": "Tune your mission preferences for better cover letters",
|
||||
"link_label": "Settings → My Profile"},
|
||||
{"key": "configure_keywords", "text": "Configure keywords and blocklist for smarter search",
|
||||
"link_label": "Settings → Search"},
|
||||
"link_label": "Settings → Search",
|
||||
"done": _keywords_configured},
|
||||
{"key": "upload_corpus", "text": "Upload your cover letter corpus for voice fine-tuning",
|
||||
"link_label": "Settings → Fine-Tune"},
|
||||
{"key": "configure_linkedin", "text": "Configure LinkedIn Easy Apply automation",
|
||||
|
|
@ -513,7 +541,10 @@ with st.expander("⚠️ Danger Zone", expanded=False):
|
|||
# ── Setup banners ─────────────────────────────────────────────────────────────
|
||||
if _profile and _profile.wizard_complete:
|
||||
_dismissed = set(_profile.dismissed_banners)
|
||||
_pending_banners = [b for b in _SETUP_BANNERS if b["key"] not in _dismissed]
|
||||
_pending_banners = [
|
||||
b for b in _SETUP_BANNERS
|
||||
if b["key"] not in _dismissed and not b.get("done", lambda: False)()
|
||||
]
|
||||
if _pending_banners:
|
||||
st.divider()
|
||||
st.markdown("#### Finish setting up Peregrine")
|
||||
|
|
|
|||
|
|
@ -324,6 +324,14 @@ with tab_search:
|
|||
st.session_state["_sp_excludes"] = "\n".join(p.get("exclude_keywords", []))
|
||||
st.session_state["_sp_hash"] = _sp_hash
|
||||
|
||||
# Apply any pending programmatic updates BEFORE widgets are instantiated.
|
||||
# Streamlit forbids writing to a widget's key after it renders on the same pass;
|
||||
# button handlers write to *_pending keys instead, consumed here on the next pass.
|
||||
for _pend, _wkey in [("_sp_titles_pending", "_sp_titles_multi"),
|
||||
("_sp_locs_pending", "_sp_locations_multi")]:
|
||||
if _pend in st.session_state:
|
||||
st.session_state[_wkey] = st.session_state.pop(_pend)
|
||||
|
||||
# ── Titles ────────────────────────────────────────────────────────────────
|
||||
_title_row, _suggest_btn_col = st.columns([4, 1])
|
||||
with _title_row:
|
||||
|
|
@ -355,7 +363,7 @@ with tab_search:
|
|||
st.session_state["_sp_title_options"] = _opts
|
||||
if _t not in _sel:
|
||||
_sel.append(_t)
|
||||
st.session_state["_sp_titles_multi"] = _sel
|
||||
st.session_state["_sp_titles_pending"] = _sel
|
||||
st.session_state["_sp_new_title"] = ""
|
||||
st.rerun()
|
||||
with st.expander("📋 Paste a list of titles"):
|
||||
|
|
@ -371,7 +379,7 @@ with tab_search:
|
|||
if _t not in _sel:
|
||||
_sel.append(_t)
|
||||
st.session_state["_sp_title_options"] = _opts
|
||||
st.session_state["_sp_titles_multi"] = _sel
|
||||
st.session_state["_sp_titles_pending"] = _sel
|
||||
st.session_state["_sp_paste_titles"] = ""
|
||||
st.rerun()
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ services:
|
|||
- ./config:/app/config
|
||||
- ./data:/app/data
|
||||
- ${DOCS_DIR:-~/Documents/JobSearch}:/docs
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /usr/bin/docker:/usr/bin/docker:ro
|
||||
environment:
|
||||
- STAGING_DB=/app/data/staging.db
|
||||
- DOCS_DIR=/docs
|
||||
|
|
|
|||
Loading…
Reference in a new issue