From 3e8b4cd654a8620d1cbf1e5e672cab1fff9cdbe8 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Fri, 13 Mar 2026 11:24:42 -0700 Subject: [PATCH] fix(cloud): use per-user config dir for wizard gate; redirect on invalid session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app.py: wizard gate now reads get_config_dir()/user.yaml instead of hardcoded repo-level config/ — fixes perpetual onboarding loop in cloud mode where per-user wizard_complete was never seen - app.py: page title corrected to "Peregrine" - cloud_session.py: add get_config_dir() returning per-user config path in cloud mode, repo config/ locally - cloud_session.py: replace st.error() with JS redirect on missing/invalid session token so users land on login page instead of error screen - Home.py, 4_Apply.py, migrate.py: remove remaining AIHawk UI references --- app/Home.py | 2 +- app/app.py | 6 +++--- app/cloud_session.py | 25 ++++++++++++++++++++++--- app/pages/4_Apply.py | 2 +- scripts/migrate.py | 6 +++--- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/Home.py b/app/Home.py index d06c405..7b23d94 100644 --- a/app/Home.py +++ b/app/Home.py @@ -69,7 +69,7 @@ _SETUP_BANNERS = [ {"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", - "link_label": "Settings → AIHawk"}, + "link_label": "Settings → Integrations"}, {"key": "setup_searxng", "text": "Set up company research with SearXNG", "link_label": "Settings → Services"}, {"key": "target_companies", "text": "Build a target company list for focused outreach", diff --git a/app/app.py b/app/app.py index d6f17a3..b1bf71a 100644 --- a/app/app.py +++ b/app/app.py @@ -22,11 +22,11 @@ IS_DEMO = os.environ.get("DEMO_MODE", "").lower() in ("1", "true", "yes") import streamlit as st from scripts.db import DEFAULT_DB, init_db, get_active_tasks from app.feedback import inject_feedback_button -from app.cloud_session import resolve_session, get_db_path +from app.cloud_session import resolve_session, get_db_path, get_config_dir import sqlite3 st.set_page_config( - page_title="Job Seeker", + page_title="Peregrine", page_icon="💼", layout="wide", ) @@ -80,7 +80,7 @@ except Exception: # ── First-run wizard gate ─────────────────────────────────────────────────────── from scripts.user_profile import UserProfile as _UserProfile -_USER_YAML = Path(__file__).parent.parent / "config" / "user.yaml" +_USER_YAML = get_config_dir() / "user.yaml" _show_wizard = not IS_DEMO and ( not _UserProfile.exists(_USER_YAML) diff --git a/app/cloud_session.py b/app/cloud_session.py index a88631e..9db96cd 100644 --- a/app/cloud_session.py +++ b/app/cloud_session.py @@ -112,13 +112,19 @@ def resolve_session(app: str = "peregrine") -> None: cookie_header = st.context.headers.get("x-cf-session", "") session_jwt = _extract_session_token(cookie_header) if not session_jwt: - st.error("Session token missing. Please log in at circuitforge.tech.") + st.components.v1.html( + '', + height=0, + ) st.stop() try: user_id = validate_session_jwt(session_jwt) - except Exception as exc: - st.error(f"Invalid session — please log in again. ({exc})") + except Exception: + st.components.v1.html( + '', + height=0, + ) st.stop() user_path = _user_data_path(user_id, app) @@ -141,6 +147,19 @@ def get_db_path() -> Path: return st.session_state.get("db_path", DEFAULT_DB) +def get_config_dir() -> Path: + """ + Return the config directory for this session. + Cloud: per-user path (//peregrine/config/) so each + user's YAML files (user.yaml, plain_text_resume.yaml, etc.) are + isolated and never shared across tenants. + Local: repo-level config/ directory. + """ + if CLOUD_MODE and st.session_state.get("db_path"): + return Path(st.session_state["db_path"]).parent / "config" + return Path(__file__).parent.parent.parent / "config" + + def get_cloud_tier() -> str: """ Return the current user's cloud tier. diff --git a/app/pages/4_Apply.py b/app/pages/4_Apply.py index dd3c5b5..1e9a3d1 100644 --- a/app/pages/4_Apply.py +++ b/app/pages/4_Apply.py @@ -389,7 +389,7 @@ with col_tools: st.markdown("---") else: - st.warning("Resume YAML not found — check that AIHawk is cloned.") + st.warning("Resume profile not found — complete setup or upload a resume in Settings → Resume Profile.") # ── Application Q&A ─────────────────────────────────────────────────────── with st.expander("💬 Answer Application Questions"): diff --git a/scripts/migrate.py b/scripts/migrate.py index 67cfad8..edf97cf 100644 --- a/scripts/migrate.py +++ b/scripts/migrate.py @@ -83,10 +83,10 @@ def _extract_career_summary(source: Path) -> str: def _extract_personal_info(source: Path) -> dict: - """Extract personal info from aihawk resume yaml.""" + """Extract personal info from resume yaml.""" resume = source / "config" / "plain_text_resume.yaml" if not resume.exists(): - resume = source / "aihawk" / "data_folder" / "plain_text_resume.yaml" + resume = source / "aihawk" / "data_folder" / "plain_text_resume.yaml" # legacy path if not resume.exists(): return {} data = _load_yaml(resume) @@ -196,7 +196,7 @@ def _copy_configs(source: Path, dest: Path, apply: bool) -> None: def _copy_aihawk_resume(source: Path, dest: Path, apply: bool) -> None: - print("\n── Copying AIHawk resume profile") + print("\n── Copying resume profile") src = source / "config" / "plain_text_resume.yaml" if not src.exists(): src = source / "aihawk" / "data_folder" / "plain_text_resume.yaml"