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>
271 lines
16 KiB
Markdown
271 lines
16 KiB
Markdown
# 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
|
|
`#tag` syntax (Obsidian-style), not manually entered. Backend is source of truth
|
|
for tag extraction.
|
|
- **Hierarchical tags:** `#project/webapp` stored as `"project/webapp"`. Filtering
|
|
by `project` matches both `project` and `project/*` children via SQL `unnest` +
|
|
`LIKE` prefix.
|
|
- **Dark-first theming:** CSS custom properties on `:root` (light) and
|
|
`[data-theme="dark"]`, with `prefers-color-scheme` detection defaulting to dark.
|
|
- **Task-Note linking:** Tasks have an optional `note_id` FK (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 `#tag` regex
|
|
- 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 ✓
|
|
- [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/note_id/tags
|
|
- [x] Task-Note linking via optional `note_id` FK
|
|
- [x] Vue views: task list (search, status/priority filters, sort, pagination), editor (note-link autocomplete, 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`
|
|
- [x] "Linked Tasks" section on NoteViewerView with inline status toggling
|
|
- [x] 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 `#tag` extraction 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
|