22a3a3c1d1
Vue 3 + TypeScript frontend with Pinia stores, markdown rendering (marked + DOMPurify), wikilink/tag linkification, and autocomplete. Quart async backend with SQLAlchemy 2.0, PostgreSQL ARRAY columns, task-note companion linking, backlinks, and note-to-task conversion. Docker Compose setup with PostgreSQL 16 and Ollama. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
16 KiB
Fabled Assistant - Project Context
Purpose: This file is the canonical reference for re-initializing Claude Code context on this project. It should be updated after every significant change.
Last Updated
2026-02-08 — Phase 3 complete (Tasks CRUD + Wikilinks)
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.
- 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
#tagsyntax (Obsidian-style), not manually entered. Backend is source of truth for tag extraction. - Hierarchical tags:
#project/webappstored as"project/webapp". Filtering byprojectmatches bothprojectandproject/*children via SQLunnest+LIKEprefix. - Dark-first theming: CSS custom properties on
:root(light) and[data-theme="dark"], withprefers-color-schemedetection defaulting to dark. - Task-Note linking: Tasks have an optional
note_idFK (one task → one note). Notes display their linked tasks in a "Linked Tasks" section. - Wikilinks: Obsidian-style
[[Title]]and[[Title|Display Text]]in markdown bodies resolve to notes by exact title match via/api/notes/by-title.
High-Level Component Diagram
┌─────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌──────────────────────┐ ┌────────────┐ │
│ │ fabledassistant │ │ ollama │ │
│ │ ┌────────────────┐ │ │ │ │
│ │ │ Quart Server │ │ │ LLM API │ │
│ │ │ ┌──────────┐ │ │ │ │ │
│ │ │ │ Vue SPA │ │ │ └────────────┘ │
│ │ │ │ (static) │ │ │ ▲ │
│ │ │ └──────────┘ │ │ │ │
│ │ │ ┌──────────┐ │ │ HTTP/REST │
│ │ │ │ /api/* │──┼──┼─────────┘ │
│ │ │ └──────────┘ │ │ │
│ │ │ │ │ │ ┌────────────┐ │
│ │ │ ▼ │ │ │ PostgreSQL │ │
│ │ │ ┌──────────┐ │ │ │ 16 │ │
│ │ │ │ asyncpg │──┼──┼──▶ │ │
│ │ │ └──────────┘ │ │ └────────────┘ │
│ │ └────────────────┘ │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────┘
Data Model
Notes (implemented)
id(int PK),title(str),body(markdown str),tags(ARRAY[str]),parent_id(nullable FK to self),created_at,updated_at- Tags are auto-extracted from body text on create/update via
#tagregex - Supports hierarchical organization via
parent_id - Lookup by exact title via
get_note_by_title()for wikilink resolution
Tasks (implemented)
id(int PK),title(str),description(markdown str),status(enum: todo/in_progress/done),priority(enum: none/low/medium/high),due_date(date, nullable),note_id(FK to notes, nullable, SET NULL),tags(ARRAY[str]),created_at,updated_at- Tags auto-extracted from description on create/update
- Status enum with quick-toggle cycling: todo → in_progress → done → todo
- Indexes: GIN on tags, B-tree on note_id, B-tree on status
LLM Interactions (Phase 4)
- Summarize notes, generate task breakdowns, search/query across notes, chat-style assistant within the app
Project Structure (Current)
fabledassistant/
├── summary.md # This file — canonical project context
├── pyproject.toml # Python project config
├── Dockerfile # Multi-stage build (Node → Python)
├── docker-compose.yml # Dev compose (app, PostgreSQL, Ollama)
├── alembic/ # DB migrations
│ ├── alembic.ini
│ ├── env.py # Async migration runner
│ └── versions/
│ └── 0002_create_tasks_table.py # Tasks table + enums migration
├── src/
│ └── fabledassistant/
│ ├── __init__.py
│ ├── app.py # Quart app factory, serves SPA + registers blueprints
│ ├── config.py # Config from env vars
│ ├── models/
│ │ ├── __init__.py # async_session factory, Base, imports Note + Task
│ │ ├── note.py # Note model (id, title, body, tags[], parent_id, timestamps)
│ │ └── task.py # Task model (id, title, description, status, priority, due_date, note_id, tags, timestamps)
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── api.py # /api blueprint with /health endpoint
│ │ ├── notes.py # /api/notes CRUD + /by-title endpoint
│ │ └── tasks.py # /api/tasks CRUD + PATCH status
│ ├── services/
│ │ ├── notes.py # Business logic: CRUD, hierarchical tag filter, get_note_by_title
│ │ └── tasks.py # Task CRUD: filter by status/priority/note_id/tags/search
│ ├── utils/
│ │ ├── __init__.py
│ │ └── tags.py # extract_tags() — regex #tag extraction, skips code fences
│ └── static/ # Vue production build (generated by Dockerfile)
└── frontend/
├── package.json # deps: vue, pinia, vue-router, marked, dompurify
├── vite.config.ts
├── tsconfig.json
├── src/
│ ├── App.vue # Shell: AppHeader + router-view + ToastNotification
│ ├── main.ts # App init, imports theme.css
│ ├── assets/
│ │ └── theme.css # CSS custom properties: light/dark themes, body reset
│ ├── api/
│ │ └── client.ts # apiGet/apiPost/apiPut/apiPatch/apiDelete helpers
│ ├── composables/
│ │ └── useTheme.ts # Theme toggle, localStorage, prefers-color-scheme
│ ├── stores/
│ │ ├── notes.ts # Notes state: CRUD + tag filter, pagination, sort, search
│ │ ├── tasks.ts # Tasks state: CRUD + status/priority filter, patchStatus
│ │ └── toast.ts # Toast notification state, 3s auto-dismiss
│ ├── types/
│ │ ├── note.ts # Note, NoteListResponse interfaces
│ │ └── task.ts # Task, TaskStatus, TaskPriority, TaskListResponse
│ ├── utils/
│ │ └── tags.ts # extractTags(), linkifyTags(), linkifyWikilinks()
│ ├── views/
│ │ ├── HomeView.vue # Landing page with health status + links to Notes/Tasks
│ │ ├── NotesListView.vue # Note list: search, sort, tag filter pills, pagination
│ │ ├── NoteEditorView.vue # Create/edit: Ctrl+S, unsaved guard, toasts
│ │ ├── NoteViewerView.vue # Markdown render: DOMPurify, inline tags, wikilinks, linked tasks
│ │ ├── TasksListView.vue # Task list: search, status/priority filters, sort, pagination, status toggle
│ │ ├── TaskEditorView.vue # Create/edit task: all fields, note-link autocomplete, Ctrl+S, dirty guard
│ │ └── TaskViewerView.vue # Task detail: rendered markdown, badges, linked note, wikilinks
│ ├── components/
│ │ ├── AppHeader.vue # Nav bar: brand, Notes + Tasks links, theme toggle
│ │ ├── NoteCard.vue # Card with TagPill, tag-click emit
│ │ ├── TaskCard.vue # Card with StatusBadge (clickable), PriorityBadge, due date, tags
│ │ ├── StatusBadge.vue # Color-coded status badge, optional clickable cycling
│ │ ├── PriorityBadge.vue # Color-coded priority indicator (hidden for "none")
│ │ ├── SearchBar.vue # Debounced search input
│ │ ├── TagPill.vue # Clickable/dismissible tag pill
│ │ ├── PaginationBar.vue # Prev/next + page numbers
│ │ └── ToastNotification.vue # Fixed-position toast container
│ └── router/
│ └── index.ts # Routes: /, /notes/*, /tasks/*
└── public/
API Endpoints (Current)
| Method | Path | Description |
|---|---|---|
| GET | /api/health |
Health check |
| GET | /api/notes |
List notes (params: q, tag, sort, order, limit, offset) |
| POST | /api/notes |
Create note (body: {title, body} — tags auto-extracted) |
| GET | /api/notes/by-title?title=... |
Resolve note by exact title (for wikilinks) |
| GET | /api/notes/:id |
Get single note |
| PUT | /api/notes/:id |
Update note (body: {title?, body?} — tags re-extracted if body changes) |
| DELETE | /api/notes/:id |
Delete note |
| GET | /api/tasks |
List tasks (params: q, tag, status, priority, note_id, sort, order, limit, offset) |
| POST | /api/tasks |
Create task (body: {title, description, status?, priority?, due_date?, note_id?}) |
| GET | /api/tasks/:id |
Get single task |
| PUT | /api/tasks/:id |
Update task |
| PATCH | /api/tasks/:id/status |
Quick status toggle (body: {status}) |
| DELETE | /api/tasks/:id |
Delete task |
Phased Roadmap
Phase 1 — Skeleton & Dev Environment ✓
- Initialize Python project (pyproject.toml, Quart app scaffold)
- Initialize Vue.js project (Vite-based, inside
frontend/) - Set up Dockerfile (multi-stage: build Vue, serve with Quart)
- Docker Compose stack with Ollama service
- Quart serves Vue static build +
/api/healthendpoint - Database setup (PostgreSQL 16, asyncpg, SQLAlchemy 2.0, Alembic)
Phase 2 — Notes CRUD + UX ✓
- Database model for notes (title, body, tags[], parent_id, timestamps)
- REST API: create, read, update, delete, list notes with pagination
- Vue views: note list, note editor (markdown), note viewer (rendered)
- Search (ILIKE on title/body) and tag filtering (hierarchical via unnest)
- Inline
#tagextraction from body text (backend regex, skips code fences) - Hierarchical tag filtering (
#projectmatchesprojectandproject/*) - Dark/light theming with CSS custom properties + toggle + localStorage
- App header with navigation and theme toggle
- Tag pills (clickable + dismissible) on cards, viewer, and list filter bar
- Pagination bar with prev/next and page numbers
- Sort controls (field + asc/desc)
- Toast notifications (success/error, 3s auto-dismiss)
- Ctrl+S save shortcut in editor
- Unsaved changes guard (route leave + beforeunload)
- DOMPurify sanitization on rendered markdown
- Inline tag linkification in rendered markdown (clickable
#taglinks)
Phase 3 — Tasks CRUD + Wikilinks ✓
- Task model with status (todo/in_progress/done) and priority (none/low/medium/high) enums
- Alembic migration for tasks table with PG enums and indexes
- REST API: full CRUD + PATCH status toggle + filter by status/priority/note_id/tags
- Task-Note linking via optional
note_idFK - Vue views: task list (search, status/priority filters, sort, pagination), editor (note-link autocomplete, Ctrl+S, dirty guard), viewer (rendered markdown)
- StatusBadge (clickable, cycles status), PriorityBadge, TaskCard components
- Obsidian-style wikilinks:
[[Title]]and[[Title|Display]]in rendered markdown - Wikilink click handling resolves notes by title via
/api/notes/by-title - "Linked Tasks" section on NoteViewerView with inline status toggling
- Theme variables for status/priority/wikilink/overdue colors (light + dark)
Phase 4 — LLM Integration (next)
- Ollama client in the backend (async HTTP via httpx/aiohttp)
- API endpoints for LLM features (summarize note, generate tasks from note, freeform chat)
- Vue components for LLM interactions (chat panel, summarize button, etc.)
- Configurable LLM endpoint + model selection in app settings
Phase 5 — Polish & Production Hardening
- Authentication (single-user or multi-user, TBD)
- Docker Swarm production stack (secrets, volumes, networking)
- Backup/restore strategy for data
- UI/UX refinements, responsive design
- Error handling, logging, monitoring
Future / Stretch
- WebSocket streaming for LLM responses
- 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
Open Questions
- Authentication model: single-user (password-only) vs multi-user?
- Should LLM streaming use WebSockets from Phase 4 or defer to Phase 5?
Current Status
Phase: Phase 3 complete. Tasks CRUD with wikilinks fully implemented.
- Full notes CRUD with markdown editing, rendering, and wikilinks
- Full tasks CRUD with status/priority enums, filters, note linking
- Inline
#tagextraction for both notes and tasks - Obsidian-style
[[wikilinks]]with title resolution - StatusBadge with clickable cycling, PriorityBadge, overdue date styling
- "Linked Tasks" section on note viewer with inline status toggling
- Dark/light theme with status/priority/wikilink color variables
- Ready for Phase 4: LLM Integration