Files
impactflow_discovery/app/main.py
T
Joel Salmon 50453901b3 Phase 3: coaching preferences + weekly check-in engine
Add coaching preferences (auto-derived from the profile, user-overridable) and
a periodic check-in engine that quotes the person's own words and asks whether
their direction still feels valid — mirror, not compass.

- Preferences are deterministic: a documented triad mapping (gut → direct/
  higher-friction, heart → warm/drift-sensitive, head → reflective/question-led)
  produces defaults for the six fields (coaching_frequency, coaching_style,
  misalignment_threshold, friction_tolerance, prefer_questions_over_directives,
  time_of_day_preference). PUT overrides; regenerate re-derives.
- CheckinCoach (app/services/coaching.py): Anthropic-backed; writes a check-in
  that quotes the person's goals back and asks if the direction still holds.
- Endpoints (app/routers/coaching.py): GET/PUT/regenerate preferences;
  GET/POST checkins; respond (records still_valid); admin POST /run is the
  weekly batch (due = cadence elapsed + locked profile), intended for a cron.
- Models + migration 005: coaching_preferences (per user) and coaching_checkin.
- Frontend: coaching.html (preferences form + check-in feed); linked from
  profile.html.

Tests: 68 passing (added deterministic-preference unit tests and coaching
endpoint/batch tests; run in-container). README updated for Phase 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:10:54 -05:00

84 lines
2.6 KiB
Python

"""FastAPI entrypoint for the ImpactFlow self-discovery module."""
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from app import models # noqa: F401 - register ORM models with Base.metadata
from app.auth import ensure_api_key_admin
from app.database import AsyncSessionLocal, Base, engine
from app.routers import (
activity,
auth as auth_router,
coaching,
discovery,
)
from app.routers.activity import prune_old_activity
from app.tracking import ActivityTrackingMiddleware
# Path to the static directory, resolved relative to this file so it works
# regardless of the current working directory.
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
@asynccontextmanager
async def lifespan(app: FastAPI):
# Alembic owns schema migrations in Docker; this create_all is an
# idempotent safety net so the app also runs cleanly in local/dev where
# migrations may not have been applied.
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with AsyncSessionLocal() as db:
await ensure_api_key_admin(db)
await prune_old_activity(db)
yield
app = FastAPI(title="ImpactFlow Self-Discovery", lifespan=lifespan)
# add_middleware wraps the previous app, so the LAST call becomes the
# OUTERMOST middleware. Register inner-to-outer:
# tracking (innermost — sees request.state.user set by deps) ->
# session (needs to wrap routes for the OAuth state cookie) ->
# CORS (outermost — preflights must short-circuit before anything else).
app.add_middleware(ActivityTrackingMiddleware)
_session_secret = os.getenv("JWT_SECRET") or "dev-session-secret-change-me"
app.add_middleware(SessionMiddleware, secret_key=_session_secret)
_cors_origins = [
o.strip()
for o in os.getenv(
"CORS_ALLOWED_ORIGINS", "http://localhost:8011"
).split(",")
if o.strip()
]
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth_router.router)
app.include_router(activity.router)
app.include_router(discovery.router)
app.include_router(coaching.router)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/")
async def root():
return RedirectResponse(url="/static/discovery.html")