Merge tasks into notes: a task is just a note with task attributes
A task is now a note with status/priority/due_date columns set (status IS NOT NULL). This eliminates the separate tasks table, companion note system, cascade deletes, bidirectional title sync, and _skip_cascade flags. Migration (0004): - Add status, priority, due_date columns to notes table - Migrate task data from companion notes and orphan tasks - Drop tasks table and old enum types Backend: - models/note.py: Add TaskStatus/TaskPriority enums, task columns, is_task property - models/task.py: Deleted (merged into note.py) - models/__init__.py: Re-export enums from note.py, remove Task import - services/notes.py: Remove companion/cascade logic, add is_task filter, convert_note_to_task, convert_task_to_note, simplified backlinks - services/tasks.py: Rewritten as thin wrappers around notes service - routes/notes.py: Add is_task filter (default false), task fields in CRUD, convert-to-note endpoint - routes/tasks.py: description→body (with fallback), remove note_id filter Frontend: - types/note.ts: Add TaskStatus, TaskPriority, task fields to Note interface - types/task.ts: Task is now a re-export alias for Note - stores/notes.ts: Simplify convertToTask, add convertToNote - stores/tasks.ts: description→body in createTask/updateTask - TaskEditorView: description→body, remove companion note UI - TaskViewerView: description→body, remove companion note link, add Convert to Note - NoteViewerView: Remove companion task UI, simplify convert-to-task - TaskCard: description→body, non-null assertions for status/priority Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+86
-70
@@ -3,8 +3,16 @@
|
||||
> **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-09 — Phase 3.5 complete (Task-Note companions, wikilink auto-create, backlinks, bug fixes, Alembic migration infrastructure)
|
||||
2026-02-10 — Phase 3.6 complete (Merged tasks into notes — a task is just a note with task attributes)
|
||||
|
||||
## Project Overview
|
||||
Fabled Assistant is a self-hosted note-taking and task-tracking application with
|
||||
@@ -41,14 +49,16 @@ for AI-assisted features.
|
||||
`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 companion link:** Every task automatically gets a companion note
|
||||
(created in `create_task()`). Title changes sync bidirectionally. Deleting either
|
||||
cascades to the other. `_skip_cascade` flag prevents infinite loops.
|
||||
- **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.
|
||||
- **Backlinks:** `GET /api/notes/:id/backlinks` searches all note bodies and task
|
||||
descriptions for `[[Title]]` patterns referencing the given note.
|
||||
- **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
|
||||
@@ -82,24 +92,27 @@ for AI-assisted features.
|
||||
|
||||
## Data Model
|
||||
|
||||
### Notes (implemented)
|
||||
### Notes (unified — includes tasks)
|
||||
- `id` (int PK), `title` (str), `body` (markdown str), `tags` (ARRAY[str]),
|
||||
`parent_id` (nullable FK to self), `created_at`, `updated_at`
|
||||
`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
|
||||
|
||||
### 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, auto-created companion),
|
||||
`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
|
||||
- Companion note auto-created on task creation, title synced bidirectionally
|
||||
- Deleting a task deletes its companion note (and vice versa)
|
||||
- Indexes: GIN on tags, B-tree on note_id, 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
|
||||
|
||||
### LLM Interactions (Phase 4)
|
||||
- Summarize notes, generate task breakdowns, search/query across notes,
|
||||
@@ -118,24 +131,24 @@ fabledassistant/
|
||||
│ └── 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
|
||||
│ ├── 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
|
||||
├── 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 + 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)
|
||||
│ │ ├── __init__.py # async_session factory, Base, imports Note + TaskStatus + TaskPriority
|
||||
│ │ └── note.py # Note model (unified: id, title, body, tags[], parent_id, status, priority, due_date, timestamps) + TaskStatus/TaskPriority enums + is_task property
|
||||
│ ├── routes/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── api.py # /api blueprint with /health endpoint
|
||||
│ │ ├── notes.py # /api/notes CRUD + /by-title + /resolve-title + /convert-to-task + /backlinks
|
||||
│ │ └── tasks.py # /api/tasks CRUD + PATCH status
|
||||
│ │ ├── 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, tag filter, get_or_create_by_title, convert_note_to_task, get_backlinks, cascade delete/sync
|
||||
│ │ └── tasks.py # CRUD with auto companion note, cascade delete/sync, _skip_cascade flag
|
||||
│ │ ├── 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.)
|
||||
│ ├── utils/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── tags.py # extract_tags() — regex #tag extraction, skips code fences
|
||||
@@ -155,12 +168,12 @@ fabledassistant/
|
||||
│ │ ├── 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, fetchBacklinks, fetchAllTags
|
||||
│ │ ├── tasks.ts # CRUD + status/priority filter, patchStatus
|
||||
│ │ ├── notes.ts # CRUD + tag filter, resolveTitle, convertToTask, convertToNote, fetchBacklinks, fetchAllTags
|
||||
│ │ ├── tasks.ts # CRUD + status/priority filter, patchStatus (uses body not description)
|
||||
│ │ └── toast.ts # Toast notification state, 3s auto-dismiss
|
||||
│ ├── types/
|
||||
│ │ ├── note.ts # Note, NoteListResponse interfaces
|
||||
│ │ └── task.ts # Task, TaskStatus, TaskPriority, TaskListResponse
|
||||
│ │ ├── note.ts # Note interface (with status, priority, due_date, is_task) + TaskStatus, TaskPriority types + NoteListResponse
|
||||
│ │ └── task.ts # Task = re-export of Note; TaskListResponse
|
||||
│ ├── utils/
|
||||
│ │ ├── tags.ts # extractTags(), linkifyTags(), linkifyWikilinks()
|
||||
│ │ └── markdown.ts # renderMarkdown() (full), renderPreview() (strips links/images for cards)
|
||||
@@ -168,14 +181,14 @@ fabledassistant/
|
||||
│ │ ├── 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, companion task link, convert-to-task, backlinks
|
||||
│ │ ├── 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, companion note link, Ctrl+S, dirty guard, autocomplete
|
||||
│ │ └── TaskViewerView.vue # Task detail: rendered markdown, badges, companion note link, backlinks
|
||||
│ │ ├── 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 links, theme toggle
|
||||
│ │ ├── NoteCard.vue # Card with rendered markdown preview (v-html), TagPill, tag-click emit
|
||||
│ │ ├── TaskCard.vue # Card with rendered preview, StatusBadge (clickable), PriorityBadge, due date, tags
|
||||
│ │ ├── 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
|
||||
@@ -193,22 +206,23 @@ fabledassistant/
|
||||
| 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/tags` | List all tags from notes + tasks (param: `q` for filter) |
|
||||
| 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 |
|
||||
| PUT | `/api/notes/:id` | Update note (body: `{title?, body?}` — tags re-extracted if body changes) |
|
||||
| DELETE | `/api/notes/:id` | Delete note (cascades to companion task) |
|
||||
| POST | `/api/notes/:id/convert-to-task` | Convert note into a task (deletes original note) |
|
||||
| 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`, `note_id`, `sort`, `order`, `limit`, `offset`) |
|
||||
| POST | `/api/tasks` | Create task (body: `{title, description, status?, priority?, due_date?}` — companion note auto-created) |
|
||||
| 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 (title syncs to companion note) |
|
||||
| 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 (cascades to companion note) |
|
||||
| DELETE | `/api/tasks/:id` | Delete task (simple delete) |
|
||||
|
||||
## Alembic Migrations
|
||||
|
||||
@@ -219,7 +233,7 @@ container startup.
|
||||
|
||||
### Migration Chain
|
||||
```
|
||||
0001_create_notes_table.py → 0002_create_tasks_table.py → 0003_task_note_companion.py
|
||||
0001_create_notes_table.py → 0002_create_tasks_table.py → 0003_task_note_companion.py → 0004_merge_tasks_into_notes.py
|
||||
```
|
||||
|
||||
### How Migrations Run
|
||||
@@ -236,15 +250,15 @@ When adding a new migration, follow these conventions:
|
||||
|
||||
1. **Create the migration file:**
|
||||
```
|
||||
alembic/versions/0004_description.py
|
||||
alembic/versions/0005_description.py
|
||||
```
|
||||
|
||||
2. **Use raw SQL for idempotency:**
|
||||
```python
|
||||
from alembic import op
|
||||
|
||||
revision = "0004"
|
||||
down_revision = "0003"
|
||||
revision = "0005"
|
||||
down_revision = "0004"
|
||||
|
||||
def upgrade() -> None:
|
||||
# For new enums:
|
||||
@@ -321,21 +335,14 @@ When adding a new migration, follow these conventions:
|
||||
### 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] REST API: full CRUD + PATCH status toggle + filter by status/priority/tags
|
||||
- [x] Vue views: task list (search, status/priority filters, sort, pagination), editor (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 3.5 — Note-Task Integration + Bug Fixes ✓
|
||||
- [x] **Task-Note companion link:** Every task auto-creates a companion note on creation
|
||||
- [x] **Bidirectional title sync:** Renaming task syncs to companion note and vice versa
|
||||
- [x] **Cascade delete:** Deleting a task deletes its companion note (and vice versa)
|
||||
- [x] **Wikilink auto-create:** Clicking `[[New Page]]` creates the note if it doesn't exist
|
||||
- [x] **Convert note to task:** Button on note viewer, creates task with companion note, deletes original
|
||||
- [x] **Backlinks system:** "What links here" section on note and task viewers
|
||||
- [x] **Rendered markdown previews:** NoteCard/TaskCard show rendered markdown (links/images stripped)
|
||||
- [x] **Tag autocomplete Tab cycling:** Tab cycles through suggestions, single match accepts immediately
|
||||
@@ -344,7 +351,17 @@ When adding a new migration, follow these conventions:
|
||||
- [x] **500 error handler:** JSON error responses for API routes, traceback logging
|
||||
- [x] **Idempotent migrations:** All migrations rewritten to raw SQL with IF NOT EXISTS guards
|
||||
- [x] **Auto-migration on startup:** Dockerfile runs `alembic upgrade head` before starting app
|
||||
- [x] **Data migration 0003:** Creates companion notes for pre-existing tasks
|
||||
|
||||
### Phase 3.6 — Merge Tasks into Notes ✓
|
||||
- [x] **Unified note/task model:** Task is just a note with `status IS NOT NULL`
|
||||
- [x] **Migration 0004:** Added `status`, `priority`, `due_date` columns to notes table, migrated task data from companion notes and orphan tasks, dropped `tasks` table
|
||||
- [x] **Eliminated companion note system:** No more companion note creation, title sync, cascade deletes, or `_skip_cascade` flags
|
||||
- [x] **Standardized on `body`:** Tasks use `body` everywhere (not `description`); API accepts `description` as fallback
|
||||
- [x] **Convert to task:** Simple `update_note(id, status='todo', priority='none')` — same ID preserved
|
||||
- [x] **Convert to note:** New `POST /api/notes/:id/convert-to-note` endpoint clears task attributes
|
||||
- [x] **Tasks service as thin wrappers:** `services/tasks.py` delegates entirely to `services/notes.py`
|
||||
- [x] **Frontend unified types:** `Task` is a re-export alias for `Note`; `note.ts` defines `TaskStatus`, `TaskPriority`
|
||||
- [x] **Simplified views:** Removed companion note UI from TaskEditorView/TaskViewerView/NoteViewerView; added Convert to Note button on TaskViewerView
|
||||
|
||||
### Phase 4 — LLM Integration (next)
|
||||
- [ ] Ollama client in the backend (async HTTP via httpx/aiohttp)
|
||||
@@ -380,17 +397,16 @@ When adding a new migration, follow these conventions:
|
||||
- Should LLM streaming use WebSockets from Phase 4 or defer to Phase 5?
|
||||
|
||||
## Current Status
|
||||
**Phase:** Phase 3.5 complete. Note-task companion system, wikilink auto-create,
|
||||
backlinks, and bug fixes fully implemented.
|
||||
**Phase:** Phase 3.6 complete. Tasks merged into notes — unified single-table model.
|
||||
- Single `notes` table with optional `status`, `priority`, `due_date` columns
|
||||
- A note **is a task** when `status IS NOT NULL`
|
||||
- Convert between note ↔ task by setting/clearing task attributes (same ID preserved)
|
||||
- No companion notes, no cascade deletes, no bidirectional sync
|
||||
- Task body standardized as `body` (not `description`)
|
||||
- `services/tasks.py` is a thin wrapper around `services/notes.py`
|
||||
- Frontend `Task` type is an alias for `Note`
|
||||
- Full notes CRUD with markdown editing, rendering, and wikilinks
|
||||
- Full tasks CRUD with status/priority enums, filters, companion note linking
|
||||
- Automatic companion note creation for every task
|
||||
- Bidirectional title sync and cascade delete between tasks and notes
|
||||
- Convert note to task functionality
|
||||
- Backlinks system showing "what links here" for notes and tasks
|
||||
- Wikilink clicks auto-create missing notes
|
||||
- Rendered markdown previews on list cards
|
||||
- Tag autocomplete with Tab cycling
|
||||
- Idempotent raw SQL migrations with auto-run on startup
|
||||
- Full tasks CRUD with status/priority enums and filters
|
||||
- Backlinks, wikilink auto-create, tag autocomplete all work across unified model
|
||||
- Dark/light theme with status/priority/wikilink color variables
|
||||
- Ready for Phase 4: LLM Integration
|
||||
|
||||
Reference in New Issue
Block a user