# 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-13 — Phase 5.9: Invitation system, table of contents, actionable dashboard, settings improvements ## 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 ## Phased Roadmap ### Phase 1 — Skeleton & Dev Environment ✓ - [x] Initialize Python project (pyproject.toml, Quart app scaffold) - [x] Initialize Vue.js project (Vite-based, inside `frontend/`) - [x] Set up Dockerfile (multi-stage: build Vue, serve with Quart) - [x] Docker Compose stack with Ollama service - [x] Quart serves Vue static build + `/api/health` endpoint - [x] Database setup (PostgreSQL 16, asyncpg, SQLAlchemy 2.0, Alembic) ### Phase 2 — Notes CRUD + UX ✓ - [x] Database model for notes (title, body, tags[], parent_id, timestamps) - [x] REST API: create, read, update, delete, list notes with pagination - [x] Vue views: note list, note editor (markdown), note viewer (rendered) - [x] Search (ILIKE on title/body) and tag filtering (hierarchical via unnest) - [x] Inline `#tag` extraction from body text (backend regex, skips code fences) - [x] Hierarchical tag filtering (`#project` matches `project` and `project/*`) - [x] Dark/light theming with CSS custom properties + toggle + localStorage - [x] App header with navigation and theme toggle - [x] Tag pills (clickable + dismissible) on cards, viewer, and list filter bar - [x] Pagination bar with prev/next and page numbers - [x] Sort controls (field + asc/desc) - [x] Toast notifications (success/error, 3s auto-dismiss) - [x] Ctrl+S save shortcut in editor - [x] Unsaved changes guard (route leave + beforeunload) - [x] DOMPurify sanitization on rendered markdown - [x] Inline tag linkification in rendered markdown (clickable `#tag` links) ### Phase 3 — Tasks CRUD + Wikilinks ✓ - [x] Task model with status (todo/in_progress/done) and priority (none/low/medium/high) enums - [x] Alembic migration for tasks table with PG enums and indexes - [x] REST API: full CRUD + PATCH status toggle + filter by status/priority/tags - [x] Vue views: task list (search, status/priority filters, sort, pagination), editor (Ctrl+S, dirty guard), viewer (rendered markdown) - [x] StatusBadge (clickable, cycles status), PriorityBadge, TaskCard components - [x] Obsidian-style wikilinks: `[[Title]]` and `[[Title|Display]]` in rendered markdown - [x] Wikilink click handling resolves notes by title via `/api/notes/by-title` ### Phase 3.5 — Note-Task Integration + Bug Fixes ✓ - [x] **Wikilink auto-create:** Clicking `[[New Page]]` creates the note if it doesn't exist - [x] **Backlinks system:** "What links here" section on note and task viewers - [x] **Rendered markdown previews:** NoteCard/TaskCard show rendered markdown (links/images stripped) - [x] **Tag autocomplete Tab cycling:** Tab cycles through suggestions, single match accepts immediately - [x] **HomeView error isolation:** Notes and tasks load independently (one failure doesn't break both) - [x] **SPA routing fix:** Replaced catch-all route with 404 error handler to prevent API interception - [x] **500 error handler:** JSON error responses for API routes, traceback logging - [x] **Idempotent migrations:** All migrations rewritten to raw SQL with IF NOT EXISTS guards - [x] **Auto-migration on startup:** Dockerfile runs `alembic upgrade head` before starting app ### Phase 3.6 — Merge Tasks into Notes ✓ - [x] **Unified note/task model:** Task is just a note with `status IS NOT NULL` - [x] **Migration 0004:** Added `status`, `priority`, `due_date` columns to notes table, migrated task data from companion notes and orphan tasks, dropped `tasks` table - [x] **Eliminated companion note system:** No more companion note creation, title sync, cascade deletes, or `_skip_cascade` flags - [x] **Standardized on `body`:** Tasks use `body` everywhere (not `description`); API accepts `description` as fallback - [x] **Convert to task:** Simple `update_note(id, status='todo', priority='none')` — same ID preserved - [x] **Convert to note:** New `POST /api/notes/:id/convert-to-note` endpoint clears task attributes - [x] **Tasks service as thin wrappers:** `services/tasks.py` delegates entirely to `services/notes.py` - [x] **Frontend unified types:** `Task` is a re-export alias for `Note`; `note.ts` defines `TaskStatus`, `TaskPriority` - [x] **Simplified views:** Removed companion note UI from TaskEditorView/TaskViewerView/NoteViewerView; added Convert to Note button on TaskViewerView ### Phase 4 — LLM Chat Integration ✓ - [x] **Ollama client:** async HTTP via httpx — `ensure_model()` (auto-pull on startup), `stream_chat()` (streaming NDJSON), `generate_completion()` (non-streaming) - [x] **Context building:** System prompt with note-aware context — includes current note (via `context_note_id`), keyword-extracted related note search (top 3, 5 keywords, 2000-char previews), URL content fetching (regex detection, HTML stripping, truncated to 4000 chars), returns `(messages, context_meta)` tuple with auto-found note IDs/titles, accepts `exclude_note_ids` to skip notes during auto-search - [x] **Conversation persistence:** `conversations` + `messages` tables in PostgreSQL, full CRUD - [x] **SSE streaming endpoint:** `POST /api/chat/conversations/:id/messages` streams LLM response chunks as SSE events, saves complete response to DB on finish - [x] **Save as note:** Save any assistant message as a new note (LLM-generated title, tagged `chat`) - [x] **Summarize conversation:** Send full conversation to LLM with summarize prompt, save result as note - [x] **Model management:** `GET /api/chat/models` lists available Ollama models; auto-pull default model (`llama3.1`) on app startup - [x] **Dedicated chat page:** `/chat` with sidebar (conversation list, create/delete) + message thread + markdown rendering + streaming indicator - [x] **Slide-out chat panel:** Toggle from header, receives `contextNoteId` from current route (`/notes/:id` or `/tasks/:id`), auto-creates conversation on first message - [x] **Frontend SSE client:** `apiStreamPost()` uses fetch + ReadableStream to parse SSE data lines - [x] **Auto-title:** Conversation title generated by LLM (concise 3-8 words) ### Phase 4.5 — Chat UX + Settings + Model Management ✓ - [x] **Settings infrastructure:** Key-value `settings` table, `GET/PUT /api/settings`, Pinia store with defaults - [x] **Configurable assistant name:** Default "Fable", editable in settings, injected into LLM system prompt and chat message labels - [x] **Settings page:** `/settings` route with assistant name form + model catalog - [x] **Model catalog:** 18 models across 3 categories (General Purpose, Coding, Uncensored / Creative Writing) with descriptions, sizes, and best-for labels - [x] **Model management:** Download (pull with SSE progress streaming + live percentage in UI), select (set as default), and remove (delete from Ollama) with confirmation UI - [x] **Status indicator in nav bar:** Moved from per-component (ChatView/ChatPanel) to global AppHeader; green/yellow/red dot with label - [x] **Chat bubble layout:** User messages right-aligned (primary color bg), assistant messages left-aligned (card bg), speech bubble tails - [x] **Floating dark input bar:** `#1c1c1e` background, rounded corners, circular send button with arrow - [x] **Auto-focus input:** Chat input auto-focuses on mount, conversation switch, and after sending - [x] **HTML entity fix:** `linkifyTags` regex now uses `(?=`) params to `list_notes()` and `/api/tasks` endpoint - [x] **Dashboard rewrite:** HomeView now shows Overdue (red left-border accent), Due Today, In Progress, Recent Chats, Recently Edited sections - [x] **Parallel fetching:** 5 API calls via `Promise.allSettled` — notes, overdue tasks, due-today tasks, in-progress tasks, chats - [x] **Client-side filtering:** Filters out `done` tasks from overdue/due-today, deduplicates in-progress against other lists - [x] **Task sections hidden when empty:** Only shown if there are matching tasks - [x] **Status toggle:** Marking a task `done` removes it from all dashboard lists ### Future / Stretch - Tagging/labeling system with LLM-suggested tags - Calendar/timeline view for tasks - Import/export (Markdown files, JSON) - Plugin/extension system ## 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` ## Current Status **Phase:** Phase 5.9 complete. Invitation system, table of contents, actionable dashboard, settings improvements. - Full note-taking and task-tracking CRUD with markdown, wikilinks, backlinks, tags - **Tiptap WYSIWYG editor** with inline formatting preview, markdown round-trip, paste handling - **Tag/wikilink autocomplete** via `@tiptap/suggestion` with heading disambiguation - Unified note/task model (a task is a note with `status IS NOT NULL`) - **Multi-user authentication** with session cookies, bcrypt passwords, admin role - **Per-user data isolation** across notes, conversations, and settings - LLM chat via Ollama with background generation task + SSE streaming - **AI Assist panel** pinned to bottom 1/3 of editor viewport with auto-syncing sections; uses background-task + buffer architecture with keepalive SSE (same pattern as chat) - Note-aware context: auto-includes current note + searches related notes by keyword - Context pills with promote/exclude controls; note picker in chat input - Save assistant messages as notes (LLM-titled, chat-tagged); summarize conversations as notes - Dedicated `/chat` page with responsive sidebar (overlay on mobile) - Settings page: assistant name, model catalog (installed/available tabs), password change, data export/restore (admin) - **Invitation system**: admin sends email invitations, recipients register via token-based `/register-invite` flow - **Registration control**: auto-closes after first user, admin toggle, invitation-based registration - **Admin user management**: `/admin/users` with user list, delete, invite, revoke - **Session cookie hardening**: HttpOnly, SameSite=Lax, optional Secure flag - **Actionable dashboard**: HomeView shows overdue/due-today/in-progress task sections (hidden when empty), recent chats, recently edited notes - **Table of contents**: Sticky sidebar on note/task viewers, auto-generated from markdown headings - **App-wide layout fix**: navbar always visible, all views fit within viewport - **Responsive design**: hamburger menu, mobile touch targets, responsive breakpoints - **Docker Swarm production stack** with secrets, network isolation, health checks, resource limits - **Application logging**: audit (security events), usage (API requests), error (unhandled exceptions) - **Admin log viewer**: `/admin/logs` with stats, filters, pagination, expandable detail rows - **Automatic log retention**: configurable via `LOG_RETENTION_DAYS` (default 90 days) - **SMTP email notifications**: security alerts (login/logout/failed login/password change), task due date reminders, invitation emails - **Admin SMTP configuration**: Settings UI with test email, DB-stored config with env var fallbacks - **Admin base URL setting**: Configurable public URL used in email links - **Per-user notification preferences**: toggle task reminders and security alerts independently - **Password reset flow**: Email-based with SHA256-hashed tokens, 1-hour expiry - Dark/light theme with CSS custom properties and design tokens