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>
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:
- Update this file (
summary.md) to reflect all architectural, data model, API, file structure, and roadmap changes made during the session.- 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.pyuses@app.errorhandler(404)(not a catch-all route) to serve static files or fall back toindex.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
#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. - 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" setsstatus='todo'; "convert to note" clearsstatus,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 usesPOST /api/notes/resolve-titleto auto-create missing notes. - SSE over WebSockets for LLM streaming: Uses
POSTwithtext/event-streamresponse (not EventSource). Frontend usesfetch()+ReadableStreamsince 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/backlinkssearches all note bodies for[[Title]]patterns referencing the given note. Results includetype: "note"ortype: "task"based on whether the linking note hasstatus IS NOT NULL. - Idempotent raw SQL migrations: All Alembic migrations use raw SQL with
CREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS, andDO $$ BEGIN CREATE TYPE ... EXCEPTION WHEN duplicate_objectto 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— theis_taskproperty checks this - 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 - 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.pymodule is a thin wrapper aroundservices/notes.pythat passesis_task=Truefor listing and defaultsstatus='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(notdescription) — 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_idtracks which note was attached as context when message was sentto_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 baseclears any stale revision stamps (safe no-op if none)alembic upgrade headruns 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:
-
Create the migration file:
alembic/versions/0005_description.py -
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") -
Rebuild and test:
docker compose up --buildTo test from a completely fresh database:
docker compose down -v && docker compose up --build
Important Notes
- Do NOT use
op.create_table()orsa.Enum()— SQLAlchemy's event system can fireCREATE TYPEeven withcreate_type=False, causing failures on re-run - Always write raw SQL with PostgreSQL idempotency guards
- Set
down_revisionto chain from the previous migration'srevision - 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/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/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 headbefore 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_datecolumns to notes table, migrated task data from companion notes and orphan tasks, droppedtaskstable - Eliminated companion note system: No more companion note creation, title sync, cascade deletes, or
_skip_cascadeflags - Standardized on
body: Tasks usebodyeverywhere (notdescription); API acceptsdescriptionas 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-noteendpoint clears task attributes - Tasks service as thin wrappers:
services/tasks.pydelegates entirely toservices/notes.py - Frontend unified types:
Taskis a re-export alias forNote;note.tsdefinesTaskStatus,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+messagestables in PostgreSQL, full CRUD - SSE streaming endpoint:
POST /api/chat/conversations/:id/messagesstreams 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/modelslists available Ollama models; auto-pull default model (llama3.1) on app startup - Dedicated chat page:
/chatwith sidebar (conversation list, create/delete) + message thread + markdown rendering + streaming indicator - Slide-out chat panel: Toggle from header, receives
contextNoteIdfrom current route (/notes/:idor/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
/chatpage 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