Files
FabledScribe/summary.md
T
bvandeusen d2b8ab8fe8 Add LLM chat integration with streaming responses via Ollama
Phase 4: Full chat system with SSE streaming, note-aware context, and
conversation persistence.

Backend:
- Migration 0005: conversations + messages tables with FKs and indexes
- Conversation/Message SQLAlchemy models with relationships
- LLM service: ensure_model (auto-pull on startup), stream_chat (NDJSON),
  generate_completion, fetch_url_content (HTML stripping), build_context
  (keyword extraction, related note search, URL content injection)
- Chat service: conversation CRUD, save_response_as_note,
  summarize_conversation_as_note
- Chat routes blueprint: 9 endpoints including SSE streaming for messages,
  save/summarize as note, Ollama model listing
- Auto-pull llama3.1 model on app startup (non-blocking)

Frontend:
- apiStreamPost: SSE client using fetch + ReadableStream
- Chat Pinia store with streaming state management
- ChatView: dedicated /chat page with conversation sidebar + message thread
- ChatPanel: slide-out panel with contextNoteId from current route
- ChatMessage: markdown-rendered message bubble with "Save as Note" action
- Updated AppHeader with Chat nav link + panel toggle button
- Updated App.vue to mount ChatPanel with route-derived context
- Added /chat and /chat/:id routes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 18:45:22 -05:00

28 KiB

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-10 — Phase 4 complete (LLM Chat Integration — streaming chat with Ollama, note-aware context, save/summarize as note)

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 /<path:path> 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.
  • 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.
  • Unified note/task model: A task is just a note with task attributes enabled. A note has status IS NOT NULL ⟹ it's a task. "Convert to task" sets status='todo'; "convert to note" clears status, priority, due_date. No companion notes, no cascade deletes, no bidirectional sync needed.
  • Wikilinks: Obsidian-style [[Title]] and [[Title|Display Text]] in markdown bodies. Clicking a wikilink uses POST /api/notes/resolve-title to auto-create missing notes.
  • SSE over WebSockets for LLM streaming: Uses POST with text/event-stream response (not EventSource). Frontend uses fetch() + ReadableStream since we need to POST the message body. Simpler than WebSockets; Quart supports async generators natively for SSE.
  • Context building server-side: Backend fetches URL content and searches notes — frontend just sends the message text + optional note ID. Keyword extraction uses simple word splitting with stopword filtering (no embeddings).
  • Auto-pull model on startup: Non-blocking, logs warning on failure so the app still starts even if Ollama isn't ready.
  • Backlinks: GET /api/notes/:id/backlinks searches all note bodies for [[Title]] patterns referencing the given note. Results include type: "note" or type: "task" based on whether the linking note has status IS NOT NULL.
  • Idempotent raw SQL migrations: All Alembic migrations use raw SQL with CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS, and DO $$ BEGIN CREATE TYPE ... EXCEPTION WHEN duplicate_object to allow safe re-runs and fresh database creation.

High-Level Component Diagram

┌─────────────────────────────────────────────┐
│              Docker Compose                  │
│                                              │
│  ┌──────────────────────┐   ┌────────────┐  │
│  │  fabledassistant     │   │  ollama     │  │
│  │  ┌────────────────┐  │   │            │  │
│  │  │  Quart Server   │  │   │  LLM API   │  │
│  │  │  ┌──────────┐  │  │   │            │  │
│  │  │  │ Vue SPA  │  │  │   └────────────┘  │
│  │  │  │ (static) │  │  │         ▲         │
│  │  │  └──────────┘  │  │         │         │
│  │  │  ┌──────────┐  │  │    HTTP/REST      │
│  │  │  │ /api/*   │──┼──┼─────────┘         │
│  │  │  └──────────┘  │  │                   │
│  │  │       │        │  │  ┌────────────┐   │
│  │  │       ▼        │  │  │ PostgreSQL │   │
│  │  │  ┌──────────┐  │  │  │    16      │   │
│  │  │  │ asyncpg  │──┼──┼──▶            │   │
│  │  │  └──────────┘  │  │  └────────────┘   │
│  │  └────────────────┘  │                   │
│  └──────────────────────┘                   │
└─────────────────────────────────────────────┘

Data Model

Notes (unified — includes tasks)

  • id (int PK), title (str), body (markdown str), tags (ARRAY[str]), parent_id (nullable FK to self), status (nullable str — todo/in_progress/done), priority (nullable str — none/low/medium/high), due_date (nullable date), created_at, updated_at
  • A note is a task when status IS NOT NULL — the is_task property checks this
  • 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
  • Auto-create via get_or_create_note_by_title() for wikilink clicks
  • to_dict() returns: id, title, body, tags, parent_id, status, priority, due_date, is_task, created_at, updated_at
  • Indexes: GIN on tags, B-tree on status

Task ≡ Note with task attributes

  • No separate tasks table. The services/tasks.py module is a thin wrapper around services/notes.py that passes is_task=True for listing and defaults status='todo', priority='none' for creation.
  • "Convert to task" = update_note(id, status='todo', priority='none')
  • "Convert to note" = update_note(id, status=None, priority=None, due_date=None)
  • Task body field is body (not description) — standardized with notes

Conversations

  • id (int PK), title (str), model (str), created_at, updated_at
  • Has many messages (cascade delete)
  • Title auto-generated from first user message if not set
  • to_dict() returns: id, title, model, message_count, created_at, updated_at

Messages

  • id (int PK), conversation_id (FK to conversations, CASCADE), role (str: system/user/assistant), content (str), context_note_id (nullable FK to notes, SET NULL), created_at
  • Index on conversation_id
  • context_note_id tracks which note was attached as context when message was sent
  • to_dict() returns: id, conversation_id, role, content, context_note_id, created_at

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.ini                  # Alembic config (prepend_sys_path = src)
├── alembic/
│   ├── env.py                   # Async migration runner
│   └── versions/
│       ├── 0001_create_notes_table.py  # Notes table (raw SQL, idempotent)
│       ├── 0002_create_tasks_table.py  # Tasks table + enums (raw SQL, idempotent)
│       ├── 0003_task_note_companion.py # Data migration: create companion notes for existing tasks
│       ├── 0004_merge_tasks_into_notes.py # Add task columns to notes, migrate data, drop tasks table
│       └── 0005_add_chat_tables.py    # Conversations + messages tables with FKs and indexes
├── src/
│   └── fabledassistant/
│       ├── __init__.py
│       ├── app.py               # Quart app factory: SPA via 404 handler, JSON 404/500 for API
│       ├── config.py            # Config from env vars
│       ├── models/
│       │   ├── __init__.py      # async_session factory, Base, imports Note + TaskStatus + TaskPriority + Conversation + Message
│       │   ├── note.py          # Note model (unified: id, title, body, tags[], parent_id, status, priority, due_date, timestamps) + TaskStatus/TaskPriority enums + is_task property
│       │   └── conversation.py  # Conversation + Message models with relationships and to_dict()
│       ├── routes/
│       │   ├── __init__.py
│       │   ├── api.py           # /api blueprint with /health endpoint
│       │   ├── chat.py          # /api/chat blueprint: conversations CRUD, SSE message streaming, save/summarize as note, models list
│       │   ├── notes.py         # /api/notes CRUD + /by-title + /resolve-title + /convert-to-task + /convert-to-note + /backlinks
│       │   └── tasks.py         # /api/tasks CRUD + PATCH status (thin wrappers, accepts body not description)
│       ├── services/
│       │   ├── notes.py         # CRUD, is_task filter, status/priority filters, convert_note_to_task, convert_task_to_note, get_backlinks
│       │   ├── tasks.py         # Thin wrappers around notes.py (create_task, list_tasks with is_task=True, etc.)
│       │   ├── llm.py           # Ollama interaction: ensure_model, stream_chat, generate_completion, fetch_url_content, build_context (keyword extraction, note search, URL fetching)
│       │   └── chat.py          # Conversation CRUD, add_message, save_response_as_note, summarize_conversation_as_note
│       ├── 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 + ChatPanel (slide-out) + 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 + apiStreamPost (SSE via fetch + ReadableStream)
    │   ├── composables/
    │   │   ├── useTheme.ts      # Theme toggle, localStorage, prefers-color-scheme
    │   │   └── useAutocomplete.ts # #tag + [[wikilink]] autocomplete: Tab cycling, debounced search
    │   ├── stores/
    │   │   ├── notes.ts         # CRUD + tag filter, resolveTitle, convertToTask, convertToNote, fetchBacklinks, fetchAllTags
    │   │   ├── tasks.ts         # CRUD + status/priority filter, patchStatus (uses body not description)
    │   │   ├── chat.ts          # Conversation CRUD, sendMessage (SSE streaming), saveMessageAsNote, summarizeAsNote, fetchModels
    │   │   └── toast.ts         # Toast notification state, 3s auto-dismiss
    │   ├── types/
    │   │   ├── note.ts          # Note interface (with status, priority, due_date, is_task) + TaskStatus, TaskPriority types + NoteListResponse
    │   │   ├── chat.ts          # Message, Conversation, ConversationDetail, OllamaModel interfaces
    │   │   └── task.ts          # Task = re-export of Note; TaskListResponse
    │   ├── utils/
    │   │   ├── tags.ts          # extractTags(), linkifyTags(), linkifyWikilinks()
    │   │   └── markdown.ts      # renderMarkdown() (full), renderPreview() (strips links/images for cards)
    │   ├── views/
    │   │   ├── ChatView.vue     # Dedicated /chat page: sidebar (conversation list) + message thread + input + summarize
    │   │   ├── HomeView.vue     # Landing page: recent notes + tasks (independent error handling)
    │   │   ├── NotesListView.vue # Note list: search, sort, tag filter pills, pagination
    │   │   ├── NoteEditorView.vue # Create/edit: Ctrl+S, unsaved guard, toasts, autocomplete
    │   │   ├── NoteViewerView.vue # Markdown render, wikilink auto-create, convert-to-task (only when !is_task), backlinks
    │   │   ├── TasksListView.vue # Task list: search, status/priority filters, sort, pagination, status toggle
    │   │   ├── TaskEditorView.vue # Create/edit task: fields (body not description), Ctrl+S, dirty guard, autocomplete
    │   │   └── TaskViewerView.vue # Task detail: rendered markdown, badges, convert-to-note button, backlinks
    │   ├── components/
    │   │   ├── AppHeader.vue    # Nav bar: brand, Notes + Tasks + Chat links, chat panel toggle, theme toggle
    │   │   ├── ChatPanel.vue    # Slide-out chat panel (right side overlay, receives contextNoteId prop)
    │   │   ├── ChatMessage.vue  # Message bubble: markdown rendering, role label, "Save as Note" action on assistant messages
    │   │   ├── NoteCard.vue     # Card with rendered markdown preview (v-html), TagPill, tag-click emit
    │   │   ├── TaskCard.vue     # Card with rendered preview (body not description), StatusBadge (clickable), PriorityBadge, due date, tags
    │   │   ├── StatusBadge.vue  # Color-coded status badge, optional clickable cycling
    │   │   ├── PriorityBadge.vue # Color-coded priority indicator (hidden for "none")
    │   │   ├── MarkdownToolbar.vue # Bold/italic/link/list/heading toolbar for editor
    │   │   ├── 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/*, /chat, /chat/:id
    └── public/

API Endpoints (Current)

Method Path Description
GET /api/health Health check
GET /api/notes List notes (params: q, tag, sort, order, limit, offset; defaults to is_task=false — plain notes only; ?is_task=true for tasks, ?all=true for everything)
POST /api/notes Create note (body: {title, body, status?, priority?, due_date?} — tags auto-extracted)
GET /api/notes/tags List all tags from notes table (param: q for filter)
GET /api/notes/by-title?title=... Resolve note by exact title (case-insensitive)
POST /api/notes/resolve-title Get-or-create note by title (for wikilink clicks)
GET /api/notes/:id Get single note (response includes is_task, status, priority, due_date)
PUT /api/notes/:id Update note (body: {title?, body?, status?, priority?, due_date?} — tags re-extracted if body changes)
DELETE /api/notes/:id Delete note (simple delete, no cascade)
POST /api/notes/:id/convert-to-task Set status='todo', priority='none' on note (returns 200)
POST /api/notes/:id/convert-to-note Clear status, priority, due_date from note (returns 200)
GET /api/notes/:id/backlinks List notes/tasks that reference this note via wikilinks
GET /api/tasks List tasks (params: q, tag, status, priority, sort, order, limit, offset) — queries notes where status IS NOT NULL
POST /api/tasks Create task (body: {title, body, status?, priority?, due_date?} — accepts description as fallback for body)
GET /api/tasks/:id Get single task
PUT /api/tasks/:id Update task (accepts body or description, prefers body)
PATCH /api/tasks/:id/status Quick status toggle (body: {status})
DELETE /api/tasks/:id Delete task (simple delete)
GET /api/chat/conversations List conversations (params: limit, offset)
POST /api/chat/conversations Create conversation (body: {title?, model?})
GET /api/chat/conversations/:id Get conversation with all messages
DELETE /api/chat/conversations/:id Delete conversation (cascades to messages)
PATCH /api/chat/conversations/:id Update conversation title (body: {title})
POST /api/chat/conversations/:id/messages Send message + stream LLM response via SSE (body: {content, context_note_id?})
POST /api/chat/messages/:id/save-as-note Save assistant message as a new note
POST /api/chat/conversations/:id/summarize Summarize conversation via LLM, save as note
GET /api/chat/models List available Ollama models

Alembic Migrations

Overview

Migrations use raw SQL for full idempotency — safe to run on a fresh database or re-run on an existing one. The Dockerfile runs migrations automatically on container startup.

Migration Chain

0001_create_notes_table.py  →  0002_create_tasks_table.py  →  0003_task_note_companion.py  →  0004_merge_tasks_into_notes.py  →  0005_add_chat_tables.py

How Migrations Run

The Dockerfile CMD runs:

(alembic stamp --purge base 2>/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:

    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:

    docker compose up --build
    

    To test from a completely fresh database:

    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 ✓

  • 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/tags
  • Vue views: task list (search, status/priority filters, sort, pagination), editor (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

Phase 3.5 — Note-Task Integration + Bug Fixes ✓

  • Wikilink auto-create: Clicking [[New Page]] creates the note if it doesn't exist
  • Backlinks system: "What links here" section on note and task viewers
  • Rendered markdown previews: NoteCard/TaskCard show rendered markdown (links/images stripped)
  • Tag autocomplete Tab cycling: Tab cycles through suggestions, single match accepts immediately
  • HomeView error isolation: Notes and tasks load independently (one failure doesn't break both)
  • SPA routing fix: Replaced catch-all route with 404 error handler to prevent API interception
  • 500 error handler: JSON error responses for API routes, traceback logging
  • Idempotent migrations: All migrations rewritten to raw SQL with IF NOT EXISTS guards
  • Auto-migration on startup: Dockerfile runs alembic upgrade head before starting app

Phase 3.6 — Merge Tasks into Notes ✓

  • Unified note/task model: Task is just a note with status IS NOT NULL
  • Migration 0004: Added status, priority, due_date columns to notes table, migrated task data from companion notes and orphan tasks, dropped tasks table
  • Eliminated companion note system: No more companion note creation, title sync, cascade deletes, or _skip_cascade flags
  • Standardized on body: Tasks use body everywhere (not description); API accepts description as fallback
  • Convert to task: Simple update_note(id, status='todo', priority='none') — same ID preserved
  • Convert to note: New POST /api/notes/:id/convert-to-note endpoint clears task attributes
  • Tasks service as thin wrappers: services/tasks.py delegates entirely to services/notes.py
  • Frontend unified types: Task is a re-export alias for Note; note.ts defines TaskStatus, TaskPriority
  • Simplified views: Removed companion note UI from TaskEditorView/TaskViewerView/NoteViewerView; added Convert to Note button on TaskViewerView

Phase 4 — LLM Chat Integration ✓

  • Ollama client: async HTTP via httpx — ensure_model() (auto-pull on startup), stream_chat() (streaming NDJSON), generate_completion() (non-streaming)
  • Context building: System prompt with note-aware context — includes current note (via context_note_id), keyword-extracted related note search (top 3), URL content fetching (regex detection, HTML stripping, truncated to 4000 chars)
  • Conversation persistence: conversations + messages tables in PostgreSQL, full CRUD
  • SSE streaming endpoint: POST /api/chat/conversations/:id/messages streams LLM response chunks as SSE events, saves complete response to DB on finish
  • Save as note: Save any assistant message as a new note (first line as title)
  • Summarize conversation: Send full conversation to LLM with summarize prompt, save result as note
  • Model management: GET /api/chat/models lists available Ollama models; auto-pull default model (llama3.1) on app startup
  • Dedicated chat page: /chat with sidebar (conversation list, create/delete) + message thread + markdown rendering + streaming indicator
  • Slide-out chat panel: Toggle from header, receives contextNoteId from current route (/notes/:id or /tasks/:id), auto-creates conversation on first message
  • Frontend SSE client: apiStreamPost() uses fetch + ReadableStream to parse SSE data lines
  • Auto-title: Conversation title auto-set from first user message content

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

  • 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

Open Questions

  • Authentication model: single-user (password-only) vs multi-user?

Current Status

Phase: Phase 4 complete. LLM chat integration with streaming responses.

  • Full note-taking and task-tracking CRUD with markdown, wikilinks, backlinks, tags
  • Unified note/task model (a task is a note with status IS NOT NULL)
  • LLM chat via Ollama with SSE streaming responses
  • Note-aware context: auto-includes current note + searches related notes by keyword
  • URL content fetching: detects URLs in messages, fetches and includes content
  • Save assistant messages as notes; summarize entire conversations as notes
  • Dedicated /chat page with conversation sidebar + message thread
  • Slide-out chat panel accessible from any page, auto-includes note/task context
  • Auto-pull default model (llama3.1) on startup
  • Dark/light theme with CSS custom properties
  • Ready for Phase 5: Polish & Production Hardening