# Fabled Assistant - Project Context > **Purpose:** This file is the canonical reference for re-initializing Claude Code > context on this project. **Update this file after every change session.** ## Session Checklist > **IMPORTANT:** At the end of every change session, you MUST: > 1. **Update this file** (`summary.md`) to reflect all architectural, data model, > API, file structure, and roadmap changes made during the session. > 2. **Commit all changes** with a descriptive commit message summarizing what was > done (e.g., "Merge tasks into notes: single table with task attributes"). > Include file-level details in the commit body when the change is non-trivial. ## Last Updated 2026-02-14 — Phase 6.0: Model selection, dashboard chat input, model warming ## Project Overview Fabled Assistant is a self-hosted note-taking and task-tracking application with integrated LLM capabilities. It is designed to run on container infrastructure (Docker Swarm) and connect to Ollama or any self-hostable LLM-compatible system for AI-assisted features. ## Core Architecture ### Stack | Layer | Technology | Notes | |-------------|-----------|-------| | Frontend | Vue 3 + TypeScript + Vite + Pinia + Vue Router | SPA served from the same container as the API | | Backend/API | Quart (Python 3.12) | Async framework; serves both API and built frontend static files | | LLM | Ollama | Or any OpenAI-compatible self-hosted LLM API | | Database | PostgreSQL 16 | asyncpg driver, SQLAlchemy 2.0 async ORM, Alembic migrations | | Deployment | Docker Compose | Single-container app + separate DB + LLM service | ### Key Design Decisions - **Single container for frontend + API:** Quart serves the Vue.js production build as static files and exposes the REST API under `/api/`. - **Quart chosen for familiarity:** The maintainer (bvandeusen) knows Quart well. - **SPA routing via 404 handler:** `app.py` uses `@app.errorhandler(404)` (not a catch-all route) to serve static files or fall back to `index.html`. API routes (`/api/*`) get JSON 404 responses. This avoids a catch-all `/` route intercepting API GETs. - **LLM integration is a separate service:** The app communicates with Ollama (or compatible) over HTTP. - **Inline tag extraction:** Tags are extracted from note/task body text using `#tag` syntax (Obsidian-style), not manually entered. Backend is source of truth for tag extraction. The `TAG_RE` regex uses `(?/dev/null || true) && alembic upgrade head && hypercorn ... ``` - `alembic stamp --purge base` clears any stale revision stamps (safe no-op if none) - `alembic upgrade head` runs all pending migrations in order - Migrations are idempotent so re-running is safe ### Creating a New Migration When adding a new migration, follow these conventions: 1. **Create the migration file:** ``` alembic/versions/0005_description.py ``` 2. **Use raw SQL for idempotency:** ```python from alembic import op revision = "0005" down_revision = "0004" def upgrade() -> None: # For new enums: op.execute(""" DO $$ BEGIN CREATE TYPE my_enum AS ENUM ('a', 'b'); EXCEPTION WHEN duplicate_object THEN null; END $$ """) # For new tables: op.execute(""" CREATE TABLE IF NOT EXISTS my_table (...) """) # For new indexes: op.execute("CREATE INDEX IF NOT EXISTS ix_name ON table (col)") # For adding columns: op.execute(""" DO $$ BEGIN ALTER TABLE my_table ADD COLUMN new_col TEXT DEFAULT ''; EXCEPTION WHEN duplicate_column THEN null; END $$ """) def downgrade() -> None: op.execute("DROP INDEX IF EXISTS ix_name") op.execute("DROP TABLE IF EXISTS my_table") op.execute("DROP TYPE IF EXISTS my_enum") ``` 3. **Rebuild and test:** ```bash docker compose up --build ``` To test from a completely fresh database: ```bash docker compose down -v && docker compose up --build ``` ### Important Notes - Do NOT use `op.create_table()` or `sa.Enum()` — SQLAlchemy's event system can fire `CREATE TYPE` even with `create_type=False`, causing failures on re-run - Always write raw SQL with PostgreSQL idempotency guards - Set `down_revision` to chain from the previous migration's `revision` - Data migrations (like 0003) should use `op.get_bind()` + `sa.text()` for queries ## Implemented Features ### Notes & Tasks - Full CRUD with markdown bodies, Obsidian-style `#tag` extraction (skips code fences), hierarchical tag filtering - Unified data model: a task is a note with `status IS NOT NULL` — convert freely between note ↔ task - Task attributes: status (todo/in_progress/done), priority (none/low/medium/high), due_date - Obsidian-style `[[wikilinks]]` with auto-create on click, backlinks ("what links here") - Tiptap WYSIWYG editor with markdown round-trip, tag/wikilink autocomplete and decorations, sticky toolbar - AI Assist panel in editor: background LLM generation via SSE with section targeting, accept/reject - Table of contents sidebar on note/task viewers (auto-generated from headings, hidden ≤1200px) - Inline edit buttons on NoteCard/TaskCard (hover on desktop, always visible on touch) - Search (ILIKE), sort, tag filter pills, pagination on list views ### Dashboard - Actionable home page with 5 task sections: Overdue, Due Today, Due This Week, High Priority, In Progress - Priority-aware sorting (priority desc → due date asc) within each section - Cascading deduplication — tasks appear only in their highest-priority section - `due_before`/`due_after` query params on `/api/tasks` for date-range filtering - Recent chats and recently edited notes sections - All task sections hidden when empty; marking done removes from all lists ### LLM Chat - Ollama integration via async HTTP (httpx), auto-pull default model on startup - Background generation with `GenerationBuffer` (in-memory SSE fan-out, `Last-Event-ID` reconnect, 60s cleanup) - Stop generation with partial content preservation - Note-aware context building: current note + keyword search for related notes + URL fetching - Context pills with promote/exclude controls; note picker (paperclip) in chat input - LLM-generated conversation titles (re-generated every 10th message) - Save assistant messages as notes (LLM-titled, chat-tagged); summarize conversations - Dedicated `/chat` page with responsive sidebar + slide-out chat panel from header - Model catalog with installed/available tabs, download progress, select, remove - Per-conversation model selection via ModelSelector dropdown in chat header (persisted via PATCH) - Dashboard inline chat input: model selector + note picker + textarea; creates conversation and navigates - Model warming: default model pre-loaded into Ollama on dashboard mount via fire-and-forget POST - Hot/cold model indicators: `/api/chat/ps` proxies Ollama `/api/ps`; ModelSelector shows filled/empty circles - Ollama configured with `OLLAMA_MAX_LOADED_MODELS=2` and `OLLAMA_KEEP_ALIVE=30m` ### Authentication & User Management - Session cookie auth with bcrypt, first-user-is-admin, orphaned data claiming - Per-user data isolation across all resources - Registration auto-closes after first user; admin toggle; invitation-based registration - Email invitation system: admin sends invite → branded email → `/register-invite` token flow - Password reset: email-based, SHA256-hashed tokens, 1-hour expiry - Session cookie hardening: HttpOnly, SameSite=Lax, optional Secure flag - Admin user management: list, delete, invite, revoke ### Notifications & Email - SMTP email service (aiosmtplib): STARTTLS (587) and implicit TLS (465) - Security alerts: login, failed login, logout, password change (fire-and-forget) - Task due date reminders: hourly background check, grouped per user, dedup via logs - Invitation and password reset emails with branded HTML templates - Per-user notification preferences (task reminders, security alerts) - Admin SMTP configuration via Settings UI with test email ### Logging & Observability - Single `app_logs` table: audit (security events), usage (API requests), error (unhandled exceptions) - Request logging middleware with timing (skips log endpoints to avoid recursion) - Admin log viewer: stats summary, category/search/date filters, paginated table, expandable JSON details - Configurable retention via `LOG_RETENTION_DAYS` (default 90), hourly cleanup ### Settings & Admin - Per-user key-value settings store (assistant name, default model, notification prefs) - Admin: backup/restore (full or per-user JSON), SMTP config, base URL, registration toggle, user management, log viewer - Configurable base URL for email links (admin setting, env var fallback) ### UI & Theming - Dark/light theme with CSS custom properties, design tokens, `prefers-color-scheme` detection - Responsive design: breakpoints (480/768/1024), hamburger menu, mobile touch targets (44px), sidebar overlays - App shell: navbar always visible, 100dvh flex layout, all views fit within viewport - Toast notifications (success/error/warning, 4s auto-dismiss) - DOMPurify sanitization on all rendered markdown ### Infrastructure - Multi-stage Dockerfile: Node build → Python runtime, auto-migration on startup - Docker Compose (dev) + Docker Swarm production stack with secrets, overlay networks, health checks - Quart serves Vue SPA from static + REST API under `/api/`; 404 handler for SPA routing - Alembic migrations: 12 migrations, all raw SQL with idempotency guards (IF NOT EXISTS, DO $$ BEGIN...EXCEPTION) - Config from env vars + Docker secrets file support (`_read_secret`) ## Development Workflow - All development and testing done via Docker: `docker compose up --build` - No local dependency installation — everything containerized - Frontend dev: Vite dev server with proxy to Quart (via Docker) - Production build: Dockerfile multi-stage — Vite builds Vue into static files, Quart serves them - To reset database: `docker compose down -v && docker compose up --build` ## Backlog - Tagging/labeling system with LLM-suggested tags - Calendar/timeline view for tasks - Import/export (Markdown files, JSON) - Application-level rate limiting on auth endpoints - Security headers middleware (CSP, X-Frame-Options, etc.) — currently handled at reverse proxy level - Session invalidation on user deletion