"""HTML page routes — server-rendered Jinja2 with HTMX-driven partial refresh.""" from __future__ import annotations from datetime import date, datetime, timezone from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from sqlalchemy import desc, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.auth import CurrentUser, require_auth, require_token from app.config import get_settings, load_groups from app.db import get_session from app.models import Referral, StrategicLog, User from app.services.access import paid_status from app.services.referral_service import assign_code_if_missing from app.templates_env import templates router = APIRouter(dependencies=[Depends(require_token)]) @router.get("/", response_class=HTMLResponse) async def dashboard(request: Request): s = get_settings() groups = load_groups(s.BASELINE_TOML, s.PORTFOLIO_TOML) return templates.TemplateResponse( request, "dashboard.html", {"groups": list(groups.keys()), "anchor": s.CASSANDRA_ANCHOR_DATE}, ) @router.get("/news", response_class=HTMLResponse) async def news_page(request: Request): return templates.TemplateResponse(request, "news.html", {}) @router.get("/upload", response_class=HTMLResponse) async def upload_page(request: Request): """Drag-drop CSV import. Posts to /api/portfolios/upload.""" return templates.TemplateResponse(request, "upload.html", {}) async def _resolve_log_date(session: AsyncSession, day: str | None) -> date: """If `day` is YYYY-MM-DD use it; else fall back to the date of the most recent generated log; else today.""" if day: try: return datetime.strptime(day, "%Y-%m-%d").date() except ValueError: pass latest = (await session.execute( select(StrategicLog.generated_at) .order_by(desc(StrategicLog.generated_at)) .limit(1) )).scalar_one_or_none() if latest is not None: return latest.date() if hasattr(latest, "date") else latest return datetime.now(timezone.utc).date() def _log_page_context(target: date) -> dict: s = get_settings() return { "selected_iso": target.isoformat(), "selected_month": target.strftime("%Y-%m"), "current_tone": s.CASSANDRA_TONE.upper(), "current_analysis": s.CASSANDRA_ANALYSIS.upper(), } @router.get("/log", response_class=HTMLResponse) async def log_page( request: Request, session: AsyncSession = Depends(get_session), ): target = await _resolve_log_date(session, None) return templates.TemplateResponse(request, "log.html", _log_page_context(target)) @router.get("/log/{day}", response_class=HTMLResponse) async def log_page_day( request: Request, day: str, session: AsyncSession = Depends(get_session), ): target = await _resolve_log_date(session, day) return templates.TemplateResponse(request, "log.html", _log_page_context(target)) @router.get("/settings", response_class=HTMLResponse) async def settings_page( request: Request, session: AsyncSession = Depends(get_session), principal: CurrentUser = Depends(require_auth), ): """Per-user settings. Currently shows email, tier, and the referral block (own code + invite link + counts of pending/converted referrals). The Credit / Paddle pieces land in D.3.""" user = principal.user if user is None: # Bearer-token admin path — no per-user settings to show. return templates.TemplateResponse( request, "settings.html", {"user": None, "invite_url": None, "pending_count": 0, "converted_count": 0}, ) # Lazily assign a referral code on first visit. user = await assign_code_if_missing(session, user) # Stats: how many people have signed up with their code so far, and # how many of those converted (paid). D.3 will fill `converted_at`. pending_count = (await session.execute( select(func.count(Referral.id)) .where(Referral.referrer_user_id == user.id) .where(Referral.converted_at.is_(None)) )).scalar() or 0 converted_count = (await session.execute( select(func.count(Referral.id)) .where(Referral.referrer_user_id == user.id) .where(Referral.converted_at.is_not(None)) )).scalar() or 0 invite_url = str(request.url_for("login_page")) + f"?ref={user.referral_code}" return templates.TemplateResponse( request, "settings.html", { "user": user, "invite_url": invite_url, "pending_count": int(pending_count), "converted_count": int(converted_count), "paid": paid_status(user), }, )