The same empty-string model guard added to chat.py was missing from the
notes assist route. If default_model was stored as "" in the DB, the
assist route would pass "" to Ollama which responds with 400, surfaced
to the user as a "400" error in the assist panel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tags are now a first-class field rather than being auto-extracted from
the note body. A new TagInput.vue chip component handles tag entry in
both editor views with autocomplete, Enter/comma/backspace UX, and
space-to-hyphen sanitization.
Backend:
- routes/notes.py: create reads tags from JSON; update accepts explicit
tags (omit = keep existing); append_tag writes to tags array with
dedup; suggest-tags accepts current_tags filter; remove extract_tags
- routes/tasks.py: same — explicit tags on create/update; remove extract_tags
- services/tag_suggestions.py: current_tags param replaces body extraction
- services/tools.py: create_note tool schema adds tags param; executor passes it
- services/llm.py: system prompt tells LLM to use tags param, not embed #tag in body
Frontend:
- components/TagInput.vue: new chip-based tag input (autocomplete, keyboard UX)
- NoteEditorView.vue / TaskEditorView.vue: tags ref loaded from note.tags;
TagInput placed between title and body; save/autosave include tags; suggest
now adds chips; fetchTagSuggestions passes current_tags; dirty tracks tags
- TiptapEditor.vue: remove fetchTags prop and TagSuggestion extension;
keep TagDecoration for legacy inline #tag highlighting
No DB migration needed — tags column already correct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tags with spaces (e.g. #science fiction) were breaking extraction because
TAG_RE only matched word characters — it would stop at the space and extract
#science instead of #science-fiction.
- TAG_RE (backend + frontend): add hyphens to character class so #science-fiction
is recognized as a single tag: [\w][\w-]* per segment
- System prompt: instruct LLM to use hyphens in multi-word tags, never spaces
- tag_suggestions.py: update prompt example + sanitize output by replacing
spaces with hyphens as a safety net regardless of LLM output
- append-tag route: sanitize incoming tag (spaces → hyphens) before appending
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend security & correctness:
- Add rate_limit.py: sliding-window rate limiter (asyncio) applied to login,
register, forgot/reset password endpoints (10/60s or 5/300s per IP)
- app.py: add security headers in after_request (X-Frame-Options, CSP,
X-Content-Type-Options, Referrer-Policy) using setdefault to preserve SSE headers
- auth.py: refactor duplicate login_required/admin_required into shared _check_auth()
- config.py: add TRUST_PROXY_HEADERS for proxy-aware client IP resolution
- routes/auth.py: rate limiting, _client_ip() helper, cleaned-up reset_password route
- routes/chat.py, notes.py, tasks.py: int() DoS fix on last_event_id; limit capped
at 500; date.fromisoformat() wrapped in try/except → 400 on invalid dates
- services/auth.py: fix Setting.user_id update bug (filter on NULL not user.id);
reset_password_with_token returns int|None (user_id) instead of bool
- services/backup.py: add _security_notice to full backup JSON export
- services/assist.py: system prompt explicitly preserves markdown list structure
and nested indented sub-items
Infrastructure:
- docker-compose.yml: add healthcheck on app service (/api/health, 10s interval)
- .dockerignore: prevent secrets/node_modules/__pycache__/.env.* leaking into build
Frontend bug fixes:
- TaskCard.vue, TaskViewerView.vue: fix isOverdue() timezone bug (ISO string compare)
- useAssist.ts: accept() now resets state to idle when document changed since proposal
- stores/chat.ts: fix memory leak in _pollUntilLoaded() (try/catch around fetchStatus)
- TiptapEditor.vue: selection offset uses closest-match strategy (not first-match)
- utils/markdown.ts: explicit DOMPurify config with FORCE_BODY; remove as const
(DOMPurify expects mutable string[])
New features:
- Auto-save (5-minute interval) in NoteEditorView and TaskEditorView — only when
editing an existing dirty record; silent on error, shows "Auto-saved" toast
- sectionParser.ts: top-level bullet/numbered list items are now individual sections
in the AI Assist panel (previously treated as one undifferentiated block)
- editor-shared.css: extracted ~500 lines of CSS duplicated between both editors;
includes .inline-assist-btn at global scope (required for teleported elements)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New NoteEmbedding model + migration 0014 stores float embeddings (JSONB)
- services/embeddings.py: get_embedding, upsert_note_embedding,
semantic_search_notes (cosine similarity), backfill_note_embeddings
- build_context() now tries semantic search first, falls back to keyword search;
accepts cached_note_ids to reuse last-turn notes and stabilise the system
prompt prefix for Ollama's KV cache
- generation_buffer.py: per-conversation note ID cache (get/set/clear)
- generation_task.py: passes cached IDs into build_context, updates cache
after each turn, and invalidates it after create_note/update_note/create_task
- app.py: pulls nomic-embed-text at startup and launches a background backfill
to embed all existing notes (30 s delay so Ollama has time to load the model)
- routes/notes.py + services/tools.py: fire-and-forget embedding update on
every note create or update via the API or LLM tool calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CalDAV integration: per-user calendar config, create/list/search events
via caldav library, LLM tools for calendar operations from chat
- LLM-suggested tags: new tag_suggestions service prompts LLM with existing
tags and note content to suggest 3-5 relevant tags; exposed via API
endpoints (suggest-tags, append-tag); integrated into editor views
(suggest button + clickable pills) and chat tool calls (pills in
ToolCallCard with one-click apply)
- Settings/model UI refinements, generation task improvements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The assist flow previously tied the entire LLM generation to a single
POST request with no keepalives, causing NS_ERROR_NET_PARTIAL_TRANSFER
in Firefox when Hypercorn closed the connection during gaps between
chunks. This refactor decouples generation into a background task with
a buffer and a separate SSE stream — the same pattern used by chat.
- generation_buffer.py: Widen _buffers to support string keys, add
create/get/remove_assist_buffer() using "assist:{user_id}" keys,
fix cleanup log format for string keys
- generation_task.py: Add run_assist_generation() — lightweight
background task with no DB persistence or title generation
- notes.py: Replace single POST SSE route with POST /api/notes/assist
(returns 202) + GET /api/notes/assist/stream (SSE with 15s keepalives
and Last-Event-ID reconnection); 409 if already running
- useAssist.ts: Switch from apiStreamPost to apiPost + apiSSEStream
two-step pattern with named event mapping and stream handle cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace plain textarea with Tiptap (ProseMirror) WYSIWYG editor for notes
and tasks. Headings, bold, lists, and code blocks now render inline while
editing. Tags and wikilinks get visual highlighting via decoration plugins,
and autocomplete uses @tiptap/suggestion with heading disambiguation.
Layout: pin navbar with app-shell flex column (100dvh), sticky editor
toolbar above scrolling content, AI Assist panel fixed at bottom 1/3 with
full-height section selector and prompt. Increase max-width to 960px
across all views. Hide assist controls during streaming/review.
Other fixes: markdown paste handling, auto-syncing assist sections via
body watcher, trailing newline on AI accept, ChatView height fix.
Add README.md describing the app, usage guide, and technical overview.
Update summary.md with Phase 5.2 roadmap and current status.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>