fix(cloud): use per-user config dir for wizard gate; redirect on invalid session

- 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
This commit is contained in:
pyr0ball 2026-03-13 11:24:42 -07:00
parent 098115b4cc
commit 3e8b4cd654
5 changed files with 30 additions and 11 deletions

View file

@ -69,7 +69,7 @@ _SETUP_BANNERS = [
{"key": "upload_corpus", "text": "Upload your cover letter corpus for voice fine-tuning", {"key": "upload_corpus", "text": "Upload your cover letter corpus for voice fine-tuning",
"link_label": "Settings → Fine-Tune"}, "link_label": "Settings → Fine-Tune"},
{"key": "configure_linkedin", "text": "Configure LinkedIn Easy Apply automation", {"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", {"key": "setup_searxng", "text": "Set up company research with SearXNG",
"link_label": "Settings → Services"}, "link_label": "Settings → Services"},
{"key": "target_companies", "text": "Build a target company list for focused outreach", {"key": "target_companies", "text": "Build a target company list for focused outreach",

View file

@ -22,11 +22,11 @@ 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 from app.cloud_session import resolve_session, get_db_path, get_config_dir
import sqlite3 import sqlite3
st.set_page_config( st.set_page_config(
page_title="Job Seeker", page_title="Peregrine",
page_icon="💼", page_icon="💼",
layout="wide", layout="wide",
) )
@ -80,7 +80,7 @@ except Exception:
# ── First-run wizard gate ─────────────────────────────────────────────────────── # ── First-run wizard gate ───────────────────────────────────────────────────────
from scripts.user_profile import UserProfile as _UserProfile 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 ( _show_wizard = not IS_DEMO and (
not _UserProfile.exists(_USER_YAML) not _UserProfile.exists(_USER_YAML)

View file

@ -112,13 +112,19 @@ def resolve_session(app: str = "peregrine") -> None:
cookie_header = st.context.headers.get("x-cf-session", "") cookie_header = st.context.headers.get("x-cf-session", "")
session_jwt = _extract_session_token(cookie_header) session_jwt = _extract_session_token(cookie_header)
if not session_jwt: if not session_jwt:
st.error("Session token missing. Please log in at circuitforge.tech.") st.components.v1.html(
'<script>window.top.location.href = "https://circuitforge.tech/login";</script>',
height=0,
)
st.stop() st.stop()
try: try:
user_id = validate_session_jwt(session_jwt) user_id = validate_session_jwt(session_jwt)
except Exception as exc: except Exception:
st.error(f"Invalid session — please log in again. ({exc})") st.components.v1.html(
'<script>window.top.location.href = "https://circuitforge.tech/login";</script>',
height=0,
)
st.stop() st.stop()
user_path = _user_data_path(user_id, app) 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) 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 (<data_root>/<user_id>/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: def get_cloud_tier() -> str:
""" """
Return the current user's cloud tier. Return the current user's cloud tier.

View file

@ -389,7 +389,7 @@ with col_tools:
st.markdown("---") st.markdown("---")
else: 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 ─────────────────────────────────────────────────────── # ── Application Q&A ───────────────────────────────────────────────────────
with st.expander("💬 Answer Application Questions"): with st.expander("💬 Answer Application Questions"):

View file

@ -83,10 +83,10 @@ def _extract_career_summary(source: Path) -> str:
def _extract_personal_info(source: Path) -> dict: 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" resume = source / "config" / "plain_text_resume.yaml"
if not resume.exists(): 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(): if not resume.exists():
return {} return {}
data = _load_yaml(resume) 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: 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" src = source / "config" / "plain_text_resume.yaml"
if not src.exists(): if not src.exists():
src = source / "aihawk" / "data_folder" / "plain_text_resume.yaml" src = source / "aihawk" / "data_folder" / "plain_text_resume.yaml"