Files
FabledScribe/summary.md
T
bvandeusen 22a3a3c1d1 Initial commit: note-taking/task-tracking app with LLM integration scaffold
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>
2026-02-09 23:35:44 -05:00

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 #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 ✓

  • 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/health endpoint
  • 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 #tag extraction from body text (backend regex, skips code fences)
  • Hierarchical tag filtering (#project matches project and project/*)
  • 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 #tag links)
  • 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_id FK
  • 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 #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