Fix project/milestone association and redesign AppHeader navigation
Backend: - routes/tasks.py: POST + PUT were silently dropping project_id, milestone_id, parent_id from request body — root cause of association not saving from the task editor - routes/tasks.py: GET /api/tasks/:id now includes parent_title in response (secondary lookup when parent_id is set) - routes/notes.py: add PATCH /api/notes/:id for partial updates (used by sub-task status toggle; PUT already existed but PATCH was missing) - routes/projects.py: GET /api/projects/:id/notes now fetches milestone IDs and passes them via milestone_ids so tasks assigned to a milestone (but lacking project_id) are included in the project view - services/notes.py: create_note() auto-sets project_id from milestone when milestone_id is provided and project_id is omitted; list_notes() gains milestone_ids param — when combined with project_id uses OR condition (project_id=X OR milestone_id IN (...)) Frontend: - NoteEditorView: add MilestoneSelector; milestone resets when project changes; all save paths (save/create/auto-save) include milestone_id - stores/notes.ts: add milestone_id to createNote + updateNote types - TaskEditorView: sub-tasks now inherit milestone_id from parent task - AppHeader: three-zone layout — brand left, Notes/Projects/Tasks/Chat centered (absolute positioning), right rail with status/theme/? and gear dropdown containing Settings/Users/Logs; mobile dropdown unchanged Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+20
-10
@@ -12,7 +12,7 @@
|
||||
> Include file-level details in the commit body when the change is non-trivial.
|
||||
|
||||
## Last Updated
|
||||
2026-03-02 — Projects + Milestones hierarchy; RAG auto-injection; summarization improvements; browser push notifications; PWA manifest; Radicale CalDAV reverted to external; Flutter Android companion app updated
|
||||
2026-03-03 — Project/milestone association bug fixes (tasks route was silently dropping project_id/milestone_id); PATCH /api/notes endpoint; parent_title in task API response; milestone_id support in NoteEditorView + notes store; sub-tasks inherit milestone; list_notes OR query for project view; AppHeader redesigned (centered nav, gear dropdown for settings)
|
||||
|
||||
## Project Overview
|
||||
Fabled Assistant is a self-hosted note-taking and task-tracking application with
|
||||
@@ -354,14 +354,14 @@ fabledassistant/
|
||||
│ │ ├── HomeView.vue # Chat-first dashboard: quick actions + chat widget (top, full-width), inline response panel, two-column grid (3fr tasks / 2fr notes); task sections: Overdue, Due Today, Due This Week, High Priority, In Progress, Other (capped 10, due-dated first); 8 recent notes; model warming on mount
|
||||
│ │ ├── SettingsView.vue # Settings page: assistant name, chat/intent model dropdowns (populated from installed models), email change (with password confirmation for local-auth users), change password, notifications, CalDAV, SMTP (admin), base URL (admin), data export/restore (admin)
|
||||
│ │ ├── NotesListView.vue # Note list: search, sort, tag filter pills, pagination
|
||||
│ │ ├── NoteEditorView.vue # Create/edit: Tiptap editor, sticky toolbar, TagInput chip field (between title and body), AI Assist panel (right-side 320px, collapsible), line-level diff view, Proofread action, floating inline ✨ pill on text selection, LLM tag suggestions (adds chips), Ctrl+S, auto-save (5min), unsaved guard; styles from editor-shared.css
|
||||
│ │ ├── NoteEditorView.vue # Create/edit: Tiptap editor, sticky toolbar, TagInput chip field (between title and body), ProjectSelector + MilestoneSelector (milestone resets when project changes), AI Assist panel (right-side 320px, collapsible), line-level diff view, Proofread action, floating inline ✨ pill on text selection, LLM tag suggestions (adds chips), Ctrl+S, auto-save (5min), unsaved guard; styles from editor-shared.css
|
||||
│ │ ├── NoteViewerView.vue # Markdown render, wikilink auto-create, convert-to-task, backlinks, table of contents sidebar
|
||||
│ │ ├── TasksListView.vue # Task list: search, status/priority filters, sort, pagination
|
||||
│ │ ├── TaskEditorView.vue # Create/edit task: Tiptap editor, sticky toolbar, TagInput chip field (between metadata fields and body), AI Assist panel (right-side 320px, collapsible), line-level diff view, Proofread action, floating inline ✨ pill on text selection, LLM tag suggestions (adds chips), Ctrl+S, auto-save (5min), dirty guard; styles from editor-shared.css + task-specific scoped styles
|
||||
│ │ └── TaskViewerView.vue # Task detail: rendered markdown, badges (isOverdue uses ISO string comparison), convert-to-note, backlinks, table of contents sidebar
|
||||
│ ├── components/
|
||||
│ │ ├── LogsView.vue # Admin log viewer: stats summary, category/search/date filters, paginated table with IP column + expandable detail rows (expands on ip_address or details)
|
||||
│ │ ├── AppHeader.vue # Nav bar: brand, nav links (incl. admin Logs), status indicator, theme toggle, user info + logout, hamburger menu (mobile)
|
||||
│ │ ├── AppHeader.vue # Nav bar: three-zone layout — brand (left), Notes/Projects/Tasks/Chat centered (absolute positioning), right rail: status + ? + theme + gear dropdown (Settings/Users/Logs) + username/signout; hamburger + mobile dropdown on ≤768px
|
||||
│ │ ├── ChatPanel.vue # Slide-out chat panel (right side overlay, receives contextNoteId prop), bubble-style messages, floating dark input, note picker, context pills with promote (+) only (no exclude)
|
||||
│ │ ├── ModelSelector.vue # Model dropdown (v-model pattern): fetches installed + running models, hot/cold indicators
|
||||
│ │ ├── DashboardChatInput.vue # Inline chat bar: ModelSelector + note picker + textarea + send button; emits submit event
|
||||
@@ -407,7 +407,7 @@ fabledassistant/
|
||||
| PUT | `/api/admin/registration` | Toggle registration open/closed (admin only, body: `{open: bool}`) |
|
||||
| GET | `/api/admin/logs` | List log entries (admin only, params: `category`, `user_id`, `search`, `date_from`, `date_to`, `limit`, `offset`) |
|
||||
| GET | `/api/admin/logs/stats` | Get log category counts (admin only) |
|
||||
| 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) |
|
||||
| GET | `/api/notes` | List notes (params: `q`, `tag`, `sort`, `order`, `limit`, `offset`, `project_id`, `milestone_id`, `parent_id`, `type`; `type=note` default — plain notes only; `type=task` for tasks, `type=all` for everything) |
|
||||
| POST | `/api/notes` | Create note (body: `{title, body, tags?: string[], status?, priority?, due_date?}` — tags explicit array) |
|
||||
| GET | `/api/notes/tags` | List all tags from notes table (param: `q` for filter) |
|
||||
| POST | `/api/notes/suggest-tags` | LLM-suggest tags (body: `{title, body, current_tags?: string[]}`, returns `{suggested_tags: [...]}`) |
|
||||
@@ -415,7 +415,8 @@ fabledassistant/
|
||||
| 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?, tags?: string[], status?, priority?, due_date?}` — omitting tags keeps existing) |
|
||||
| PUT | `/api/notes/:id` | Update note (body: `{title?, body?, tags?: string[], status?, priority?, due_date?, project_id?, milestone_id?, parent_id?}`) |
|
||||
| PATCH | `/api/notes/:id` | Partial update note — same accepted fields as PUT; used for sub-task status toggle |
|
||||
| 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) |
|
||||
@@ -569,7 +570,7 @@ When adding a new migration, follow these conventions:
|
||||
- **Keyboard shortcuts overlay:** Press `?` or click `?` in nav bar. State shared via `useShortcuts` composable.
|
||||
|
||||
### LLM Chat
|
||||
- Ollama integration via async HTTP (httpx), default model `qwen3:latest` (better tool support than mistral), auto-pull on startup
|
||||
- Ollama integration via async HTTP (httpx), default model `qwen3:latest` (`config.py` fallback); docker-compose default is `llama3.1` (override via `OLLAMA_MODEL` env var or user `default_model` setting); auto-pull on startup
|
||||
- Background generation with `GenerationBuffer` (in-memory SSE fan-out, `Last-Event-ID` reconnect, 60s cleanup)
|
||||
- Stop generation with partial content preservation
|
||||
- Note-aware context building: current note + keyword search for related notes + URL fetching
|
||||
@@ -586,8 +587,9 @@ When adding a new migration, follow these conventions:
|
||||
- Save assistant messages as notes (LLM-titled, chat-tagged); summarize conversations
|
||||
- Dedicated `/chat` page with responsive sidebar + slide-out chat panel from header
|
||||
- Model catalog with installed/available tabs, download progress, select, remove
|
||||
- Per-conversation model selection via ModelSelector dropdown in chat header (persisted via PATCH)
|
||||
- Per-conversation model selection via ModelSelector dropdown in chat header (persisted via PATCH); user's `default_model` setting is always the source of truth for generation and summarization — `conv.model` (stored at creation time) no longer short-circuits the setting lookup
|
||||
- Dashboard inline chat input: model selector + note picker + textarea; creates conversation and navigates
|
||||
- **Startup warm-up (fixed):** No longer warms `Config.OLLAMA_MODEL`. At startup, queries all distinct `default_model` values from user settings, cross-references with Ollama's installed models, and warms only the intersection. Models selected by users but not yet installed are skipped with an info log.
|
||||
- Model warming: default model pre-loaded into Ollama on dashboard mount via fire-and-forget POST
|
||||
- **Model load state indicator:** `GET /api/chat/status` now queries `/api/tags` and `/api/ps` in
|
||||
parallel. Model status has three values: `"not_found"` (not installed), `"cold"` (installed but
|
||||
@@ -765,9 +767,17 @@ When adding a new migration, follow these conventions:
|
||||
- **Services**: `services/projects.py` (CRUD + `get_or_create_project` + `get_project_summary`); `services/milestones.py` (CRUD + `get_milestone_progress` + `get_project_milestone_summary`).
|
||||
- **Routes**: `routes/projects.py` under `/api/projects`; `routes/milestones.py` under `/api/projects/<pid>/milestones`.
|
||||
- **LLM tools**: `create_project`, `list_projects`, `get_project`, `update_project`; `create_milestone`, `list_milestones`; `create_note`/`create_task`/`update_note` accept optional `project` + `milestone` + `parent_task` params.
|
||||
- **Frontend**: `ProjectListView.vue` (card grid, stacked milestone progress bars per card), `ProjectView.vue` (milestone-grouped kanban + inline milestone management), `ProjectSelector.vue` (dropdown used in NoteEditorView + TaskEditorView), `MilestoneSelector.vue` (used in TaskEditorView).
|
||||
- **Frontend**: `ProjectListView.vue` (card grid, stacked milestone progress bars per card), `ProjectView.vue` (milestone-grouped kanban + inline milestone management + `+` button in Todo column header → `/tasks/new?projectId=X&milestoneId=Y`), `ProjectSelector.vue` (dropdown used in NoteEditorView + TaskEditorView), `MilestoneSelector.vue` (used in NoteEditorView + TaskEditorView).
|
||||
- **Sub-tasks in TaskEditorView**: Sub-tasks section fetches child tasks (`parent_id=<id>`), shows toggle + inline create form. URL query params `?projectId=X&milestoneId=Y&parentId=Z` pre-fill editor fields on new task.
|
||||
- **Types**: `frontend/src/types/note.ts` Note interface now includes `project_id: number | null` and `milestone_id: number | null`.
|
||||
- `app.py` registers `projects_bp` and `milestones_bp`; router adds `/projects` and `/projects/:id`.
|
||||
- **`services/notes.py`**: `list_notes()` accepts `parent_id` filter and new `milestone_ids: list[int]` param — when provided with `project_id`, uses `OR (project_id=X OR milestone_id IN (...))` so tasks assigned to a milestone-but-not-project are still returned. `create_note()` auto-sets `project_id` from the milestone's project when `milestone_id` is provided and `project_id` is omitted.
|
||||
- **`services/milestones.py`**: `find_milestone_by_title()` for cross-project milestone lookup.
|
||||
- **`GET /api/notes`** now accepts additional query params: `project_id`, `milestone_id`, `parent_id`, `type` (`note`/`task`/`all`).
|
||||
- **`routes/projects.py` `GET /api/projects/:id/notes`**: fetches project's milestone IDs and passes them via `milestone_ids` to `list_notes`, so tasks assigned via milestone (but lacking `project_id`) are included.
|
||||
- **`routes/tasks.py` bug fix**: `POST /api/tasks` and `PUT /api/tasks/:id` were silently dropping `project_id`, `milestone_id`, and `parent_id` from the request body. Both routes now read and pass all three fields. `GET /api/tasks/:id` now includes `parent_title` in the response (secondary lookup when `parent_id` is set).
|
||||
- **`routes/notes.py`**: Added `PATCH /api/notes/:id` for partial updates (same accepted fields as PUT). Used by sub-task status toggle in the frontend.
|
||||
- **`services/tools.py`**: `list_notes` tool supports project filter + `updated_at`; `list_tasks` tool supports milestone without requiring project; `tag_mode` defaults to `add`; fail-fast project resolution raises tool error immediately.
|
||||
|
||||
### RAG Auto-injection (Phase B)
|
||||
- Notes scoring ≥0.60 cosine similarity are injected automatically into every chat system prompt (max 3, up to 800 chars each) under `--- Relevant Notes ---` heading.
|
||||
@@ -785,9 +795,9 @@ When adding a new migration, follow these conventions:
|
||||
### Browser Push Notifications (Phase E)
|
||||
- **`models/push_subscription.py`**: PushSubscription — endpoint, p256dh, auth, user_agent; UNIQUE(user_id, endpoint).
|
||||
- **Migration**: `0018_add_push_subscriptions.py` (down_revision="0017").
|
||||
- **`services/push.py`**: `vapid_enabled()`, `save_subscription()`, `delete_subscription()`, `send_push_notification()` (lazy-imports pywebpush, fire-and-forget, removes 410 Gone subs).
|
||||
- **`services/push.py`**: `vapid_enabled()`, `save_subscription()`, `delete_subscription()`, `send_push_notification()` (lazy-imports pywebpush, fire-and-forget, removes 410 Gone subs). `ensure_vapid_keys()` auto-generates an EC P-256 key pair on first boot, saves to `/data/vapid_keys.json` (inside `app_data` volume) so it survives container restarts.
|
||||
- **`routes/push.py`**: `GET /api/push/vapid-public-key`, `POST/DELETE /api/push/subscribe`.
|
||||
- Config: `VAPID_PRIVATE_KEY`, `VAPID_PUBLIC_KEY`, `VAPID_CLAIMS_SUB` env vars.
|
||||
- Config: `VAPID_PRIVATE_KEY`, `VAPID_PUBLIC_KEY`, `VAPID_CLAIMS_SUB` env vars — all **optional** (keys are auto-generated at startup if absent). Env vars still take precedence for deployments that manage keys externally.
|
||||
- `generation_task.py`: fires push after `buf.state = COMPLETED` (guarded by `vapid_enabled()`).
|
||||
- `pywebpush>=2.0` added to `pyproject.toml`.
|
||||
- **Frontend**: `frontend/public/sw.js` (push + notificationclick handlers); `frontend/src/stores/push.ts` (isSupported, permission, isSubscribed, subscribe/unsubscribe); SettingsView "Push Notifications" toggle.
|
||||
|
||||
Reference in New Issue
Block a user