¶ Overview
A cinematic developer portfolio that doubles as a job search workbench. Visitors land on a sticky viewport 3D landing built with React Three Fiber and Lenis with custom scene snapping, interact with a streaming chat assistant grounded in a personal knowledge base, and see project cards that surface only when the question genuinely earns them. Behind a hidden /admin route sits a fully operational back office: CRUD for projects, skills, about, and journey; GitHub repo import with LLM scans cached per commit SHA; a YAML resume editor with one-call tailoring and cover letter generation supporting PDF, YAML, and Markdown export; and an LLM cost dashboard logging every call. Built to be the canonical, AI-discoverable source for who I am and what I ship.
¶ Important info
Two containers total (frontend plus backend), one SQLite file on disk, one local JSON file for RAG. No external datastore. The backend exposes 10 routers and 57 routes and ships with a 116-test pytest suite covering SSE streaming, the regex and classifier prompt injection guard, the project card gate, conversation memory with TTL and LRU, the per-IP burst limiter, and admin CRUD. AI discoverability is treated as a first-class surface: llms.txt and llms-full.txt routes, a PWA manifest, sitemap.ts, and an explicit robots.ts allowlist for 18 named AI crawlers including GPTBot, ClaudeBot, PerplexityBot, and Google Extended, so generative engines cite the profile rather than hallucinate it. The production deploy target is Railway with a mounted volume at /app/data so the SQLite file and knowledge index survive redeploys
¶ Problem faced
Two non-obvious problems sit on top of each other. First, the cinematic scroll: three R3F scenes share one sticky viewport and animate off scrollYProgress, but Lenis's inertial wheel handling allowed a single trackpad gesture to fling past two scenes, and the R3F frameloop kept burning frames while off-screen. Panes also visibly popped into place on scroll-up because Three.js's clock and camera drifted while paused. Second, the chat: a generic RAG pipeline will attach a project card to a vague query because cosine similarity finds something regardless of intent, and any LLM-fronted endpoint without a guard layer is one prompt away from leaking the system message. A naive guard fails open the moment the classifier errors.
¶ How it was solved
Scroll: a custom SceneSnap controller caps Lenis's wheel target at the next scene anchor so one gesture advances exactly one scene with no overshoot, with an idle nearest-anchor fallback. The R3F frameloop is set to never past progress 0.56. A ResumeReset repairs clock.oldTime, snaps the camera back to its target, and forces a synchronous advance render before paint on resume, eliminating the pop. Chat: a two-layer guard combining a regex tripwire and an LLM intent classifier that fails closed on classifier error, plus a query-side filter that word-boundary matches the user message against project titles and tech stack entries. RAG-retrieved projects are suppressed unless the visitor explicitly asked. Memory is a process-local TTL and LRU cache keyed by conversation ID with capped turn replay. The per-IP burst limiter sits in front of a daily quota backed by SQLite. Every LLM call writes feature, tokens, latency, and cost to llm_usage_logs via a non-blocking background task. Key tradeoffs: SQLite and local JSON over Postgres and pgvector for fewer moving parts; query gating over confidence thresholds for deterministic, explainable behavior with no magic numbers to tune.