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:
parent
098115b4cc
commit
3e8b4cd654
5 changed files with 30 additions and 11 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"):
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue