Commit Graph

40 Commits

Author SHA1 Message Date
bvandeusen ed715dcc23 feat(knowledge): note types, counts, new-note button, audit fixes
- Add note_type (note/person/place/list) selector + entity metadata fields
  (relationship, email, phone / address, hours) to NoteEditorView
- Pre-select type via ?type= query param from KnowledgeView new-note dropdown
- KnowledgeView: add split "New note / ▾" button with type dropdown
- KnowledgeView: show per-type counts on sidebar filter buttons (when > 1)
- Fix: filter-btn now flex layout so count badge aligns to right edge
- Fix: list_notes count_query was missing parent_id filter (inflated totals)
- Fix: PATCH /api/notes/:id now fires upsert_note_embedding (workspace autosave)
- Fix: get_knowledge_counts endpoint for per-type counts
- Fix: get_knowledge_tags was silently discarding note_type filter (double-stmt bug)
- Fix: NoteEditorView onMounted stray brace from edit session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:53:09 -04:00
bvandeusen 71ca0ecb5c fix: voice status global store, per-view mic reactivity, single-voice preview
- Move voice status into settings store (voiceSttReady, voiceTtsReady),
  checked once at login and refreshed after admin model reload
- ChatView, BriefingView, DashboardChatInput now use computed refs from
  the store — mic buttons appear reactively without needing a page reload
- BriefingView: separate STT-only guard for mic PTT vs TTS-only guard for
  listen mode / speak buttons
- Add ▶ Preview button to Voice & Speed section in Settings for single-
  voice testing without enabling blend mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:53:06 -04:00
bvandeusen ac90548823 feat: task management enhancements (cancelled status, recurrence, timestamps)
- Add 'cancelled' status to TaskStatus type, StatusBadge, TaskCard,
  TaskEditorView, TaskViewerView, TasksListView
- Add RecurrenceEditor component (none / interval / calendar rules)
- TaskEditorView: wire RecurrenceEditor, show started_at/completed_at
  timestamps read-only, include recurrence_rule in save payload
- TaskViewerView: show recurrence summary, timestamps in meta row
- tasks.ts: statusFilter/priorityFilter as arrays, add recurrence_rule
  to updateTask and createTask payloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 00:51:36 -04:00
bvandeusen fd05c65018 fix(calendar): correct event timezone handling
- Frontend sends user_timezone (IANA, from Intl.DateTimeFormat) with
  every message POST; threaded through route → generation_task → build_context
- System prompt now tells the LLM the user's timezone so it creates
  events with the correct UTC offset (e.g. 15:00+01:00 not 15:00Z)
- Calendar tool guidance updated to require UTC offset in all event
  datetimes
- EventSlideOver: dateFromIso/timeFromIso now use JS Date to convert
  stored UTC times to local time for display; toIso includes local
  timezone offset when saving so the correct UTC time is stored

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 20:06:09 -04:00
bvandeusen ebc79b34f9 feat(rag): RAG scoping and context isolation controls
- Migration 0030: add conversations.rag_project_id (NULL=orphan-only,
  -1=all notes, positive=project), projects.auto_summary and
  projects.summary_updated_at
- Three-value scope semantics thread from build_context() → semantic
  search and keyword fallback via orphan_only + effective_project_id
- Project summarization background job (generate_project_summary,
  backfill_project_summaries) called via Ollama; triggered on project
  update and note saves (debounced 1h); runs at startup
- New LLM tools: search_projects (SequenceMatcher scoring on
  title+description+auto_summary) and set_rag_scope (persists to DB,
  workspace-guarded, emits new_rag_scope in SSE done event)
- execute_tool() accepts conv_id + workspace_project_id; generation_task
  passes both and captures scope changes for SSE done enrichment
- Frontend: Conversation type gets rag_project_id; chat store adds
  ragProjectId computed + updateRagScope(); SSE done handler syncs scope
- ChatView: replace sidebar ProjectSelector with a scope chip pill above
  the input bar, animated dropdown, pulse on model-driven scope change

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 17:44:39 -04:00
bvandeusen b44d8496bc fix: queue drain survives navigation away from a streaming conversation
Previously the queue drain guard (currentConversation.id === convId) meant
that if the user navigated to ChatView while a workspace stream was running,
the workspace's queued messages were silently abandoned — they sat in
localStorage indefinitely with no code path to resume them.

- Extract _tryDrainQueue(convId) with the same guard logic
- Call it at stream-end (replaces the inline block)
- Call it in fetchConversation after _loadQueue, so returning to a
  conversation with orphaned queue messages drains them automatically
- Order is now preserved: messages drain in the order they were queued,
  even across navigation events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 18:08:45 -04:00
bvandeusen c7ef709633 Multi-user sharing, groups, and in-app notifications
Backend:
- Alembic migration 0025: groups, group_memberships, project_shares,
  note_shares, notifications tables
- models: Group, GroupMembership, ProjectShare, NoteShare, Notification
- services/access.py: permission resolution (viewer/editor/admin/owner)
- services/groups.py + routes/groups.py: full group CRUD + membership
- services/sharing.py + routes/shares.py: project/note sharing API
- services/notifications.py: in-app notification create/list/mark-read
- routes/in_app_notifications.py: GET/POST notification endpoints
- routes/users.py: user search endpoint
- services/projects.py + services/notes.py: *_for_user variants
- routes updated to use *_for_user on get/list; adds permission field
- app.py: register all new blueprints

Frontend:
- api/client.ts: ShareEntry, GroupEntry, NotificationEntry types + helpers
- stores/notifications.ts: Pinia store with polling
- NotificationBell.vue: bell icon + badge + dropdown toggle + 60s poll
- NotificationsPanel.vue: unread notification list with mark-all-read
- ShareDialog.vue: teleport modal for sharing projects/notes with users/groups
- SharedWithMeView.vue: /shared route listing shared projects and notes
- AppHeader: NotificationBell, Shared nav link
- ProjectView, NoteViewerView, TaskViewerView: Share button + ShareDialog
- SettingsView: Groups admin tab with create/delete groups + member mgmt
- router: /shared route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 15:37:00 -04:00
bvandeusen 4dd3c1fe81 Fix queued bubbles, queue persistence, duplicate confirm UX, semantic check
- Queued message bubbles now match user bubble style (primary colour,
  right-aligned, opacity 0.45) in both ChatView and WorkspaceView
- Queue persisted to localStorage (fa_conv_queue_<id>); survives page
  refresh, restored on fetchConversation, cleared on delete
- ToolCallCard confirm/deny: buttons now inline in header row; "Create
  anyway" calls POST /api/notes or /api/tasks directly (no LLM round-trip);
  shows "✓ Created" / "Skipped" state; removed chatStore dependency
- POST /api/notes and POST /api/tasks now accept project (name string) and
  resolve to project_id server-side, matching tools.py behaviour
- Remove semantic similarity duplicate check from create_note and
  create_task — 0.87 threshold was too aggressive for topically-related
  notes; title-based exact/fuzzy checks are sufficient

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 08:11:39 -04:00
bvandeusen d7e1fe6aab UX: queue bubbles, dashboard queuing, duplicate confirm buttons, short-note fix
- Dashboard chat: allow typing/sending during streaming (queued in store)
  placeholder updates to "Type to queue…" during generation
- ChatView/WorkspaceView: replace queued chip with actual pending message
  bubbles — light grey, dashed border, "Queued" badge above content; clear
  button below the stack
- ToolCallCard: when tool returns requires_confirmation=True, show
  "Similar content found" label (not "Error"), link to the similar note,
  and "Create Anyway" / "Don't Create" buttons that auto-send the reply
- tools.py: fuzzy title match now returns requires_confirmation=True with
  similar_note data instead of a hard error, so numbered-series notes
  (Lore: X 0, Lore: X 1) can be created with one button click; semantic
  match responses also include similar_note for the link

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 23:32:21 -04:00
bvandeusen 059a2e06d5 Queue concurrent prompts instead of dropping or conflicting
Chat store: when sendMessage() is called while streaming, push to a
per-conversation queue and return. When the stream ends the next queued
message fires automatically via setTimeout(0) to keep the call stack
clean. clearQueue() and queuedCount exposed for UI consumption.
Queue is cleaned up on conversation delete.

ChatView/WorkspaceView: remove the streaming guard from the local
sendMessage() functions and the :disabled on the textarea so users can
type and submit freely while streaming. Input placeholder changes to
"Type to queue next message…" during streaming. A small " N queued ×"
chip appears below the streaming bubble showing queue depth with a
cancel button.

DashboardChatInput: disable input, attach button, and send button
during streaming. The dashboard creates a fresh conversation per
message so in-conversation queuing doesn't apply — locking the input
is the correct UX here. Placeholder updates to "Generating response…"
during streaming.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:05:49 -04:00
bvandeusen c65aad6639 Upgrade all major frontend dependencies
- TipTap 2 → 3: Extension from @tiptap/core, Placeholder from
  @tiptap/extensions, TaskList/TaskItem from @tiptap/extension-list,
  link: false in StarterKit (now bundles Link), @tiptap/core added
- marked 15 → 17: heading renderer updated to tokens/parseInline API
- Pinia 2 → 3, Vue Router 4 → 5 (no code changes required)
- Vite 6 → 7, @vitejs/plugin-vue 5 → 6, vue-tsc 2 → 3
- TypeScript 5.6 → 5.9: fixed Uint8Array<ArrayBuffer> strictness in
  push.ts, removed unused bodyEl ref in NoteViewerView.vue
- .npmrc: legacy-peer-deps=true for TipTap v3 peer dep resolution

TipTap 3 new capabilities now available: static renderer
(createStaticRenderer), MarkViews, @tiptap/extensions package.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 21:30:55 -04:00
bvandeusen ef141f07f8 UI polish pass: word count, slash commands, task lists, graph peek, bulk delete, export
- WordCount component (toggle words/chars vs read time, persisted mode)
- TipTap: TaskList/TaskItem extensions, slash command menu (H1-H3, lists, code, quote, task)
- Markdown serializer: task list → `- [ ]` / `- [x]` roundtrip
- GraphView: slide-in peek panel for note/task nodes (body, tags, linked nodes); tag nodes still navigate
- ChatView: bulk-select conversations with two-click confirm delete + chat retention policy (default 90d)
- NoteEditorView: pill tab bar with animated edit/preview toggle + WordCount in toolbar
- WorkspaceNoteEditor: inline search with tag matching, inline new-note creation, WordCount
- WorkspaceTaskPanel: task body rendered in slide-over + Edit link
- Settings: data export (Markdown ZIP / JSON) via GET /api/export
- Accessibility: skip-to-content link, aria-labels on all icon-only buttons
- Fix: export route used non-existent fabledassistant.database — corrected to async_session()
- Fix: ToolCallRecord.status type now includes "running" (was causing TS build error)
- Dockerfile: upgrade npm to latest before install to suppress major-version notice
- npm audit fix: patched minimatch (ReDoS) and rollup (path traversal) — 0 vulnerabilities

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 23:15:01 -05:00
bvandeusen 74ebb8a87f Project Workspace view, abort button, session invalidation, workspace fixes
Workspace (/workspace/:projectId):
- Three-panel layout (tasks / chat / notes) with CSS grid collapse toggles
- WorkspaceTaskPanel: tasks grouped by milestone, collapsible groups, task
  detail slide-over with status cycling, TaskLogSection work log, and
  inline-confirm delete
- WorkspaceNoteEditor: list view (sorted by updated_at, inline tag pills,
  inline-confirm delete) with editor view (TipTap, TagInput, Suggest tags,
  60s autosave)
- Persistent workspace conversation stored in localStorage per project;
  reused on return visits
- Thinking enabled (think: true) with Reasoning block in streaming bubble
- workspace_project_id backend pipeline: chat.py → generation_task.py →
  llm.py; system prompt uses project title so agent passes project="Title"
  to tools (fixes create_note failing with numeric project string)
- SSE tool-call watcher bridges agent actions to panel updates
- Height fix: workspace-root uses height 100%; app-content switches to
  overflow hidden via :has() selector
- Entry point: "Open Workspace" button on ProjectView

Abort button:
- Stop button in ChatView header and WorkspaceView input bar
- Calls existing cancelGeneration() / POST .../generation/cancel

Session invalidation:
- POST /api/auth/invalidate-sessions bumps session_version, keeps current
  session alive; useful after SSO/OAuth password rotation
- Button in Settings → Active Sessions section

Other:
- Dashboard recent notes limit increased from 8 to 16
- Workspace chat abort replaces Send button while streaming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:34:06 -05:00
bvandeusen 48f070f773 Project-aware assist, link suggestions, project-scoped RAG, semantic search tool, SSE race fix
- Writing assistant: inject project notes as context (definition-tagged first), wikilink suggestions
- Link suggestions: server-side endpoint finds unlinked term occurrences, NoteEditorView sidebar panel
- Project-scoped RAG: ChatView ProjectSelector filters semantic+keyword search to selected project
- Semantic search tool: LLM search_notes upgraded to hybrid semantic (0.40 threshold) + keyword merge
- SSE race condition fix: drain remaining events after stream loop exits in chat.py and notes.py
- RAG_AUTO_SNIPPET raised 800→4000; sidebar include uses full note body; MAX_BODY_CHARS 8000→24000
- Enter-to-submit on writing assistant instruction textareas (note and task editors)
- DiffView: equal-line collapsing with 3-line context around changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 14:02:54 -05:00
bvandeusen dc39a56293 Per-conv streaming state, immediate message_count commit, auto-new-chat on open
stores/chat.ts:
- Replace 7 global stream refs with convStreams: Record<number, ConvStreamState>
- Each conversation tracks its own stream state; 7 names become computed getters
- Add isStreamingConv(id) public helper
- Increment message_count +2 immediately after POST 202 (not at done) so the
  cleanup watcher never deletes a mid-generation conversation
- SSE reconnect retries now run for background conversations regardless of
  which conv is currently viewed
- error toasts only shown when the erroring conv is currently viewed
- deleteConversation cleans up orphaned stream state

ChatView.vue:
- Remove invalid write to store.lastContextMeta (now a read-only computed)
- Add !store.isStreamingConv(newId) guard to watch(convId) fetch so navigating
  back to a generating conversation shows accumulated content without stale refetch
- Add startNewConversation() helper; opening /chat with no ID auto-creates a
  new conversation and redirects to it (on mount and on navigation)
- Deleting a conversation also auto-creates a new one

Also committing previous-session changes (keyboard nav + task routing):
- App.vue: e shortcut only on note-view (not task-view, which is removed)
- TaskCard.vue: route directly to /tasks/:id (viewer), remove View buttons
- router/index.ts: remove task-view route; /tasks/:id now maps to TaskEditorView
- TasksListView.vue: Enter key pushes to /tasks/:id

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 20:32:56 -05:00
Bryan Van Deusen 87d3f3ea16 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>
2026-03-03 13:34:01 -05:00
bvandeusen 012eb1d46b Add Projects, Milestones, RAG auto-inject, push notifications, PWA, tag normalisation
## Projects & Milestones (Phases A + G)
- New models: Project, Milestone (Project → Milestone → Task hierarchy)
- notes table: project_id + milestone_id FKs; parent_id FK constraint activated
- Migrations: 0017 (projects), 0018 (push_subscriptions), 0019 (events), 0020 (milestones)
- Services: projects.py, milestones.py (CRUD + progress tracking)
- Routes: /api/projects + /api/projects/<id>/milestones
- LLM tools: create/list/get/update project; create/list milestone; project + milestone + parent_task params on note/task tools
- Frontend: ProjectListView (stacked milestone bars), ProjectView (milestone-grouped kanban), ProjectSelector, MilestoneSelector, NoteEditorView + TaskEditorView updated

## RAG Auto-injection (Phase B)
- Notes ≥0.60 cosine similarity auto-injected into system prompt (max 3, 800 chars each)
- excluded_note_ids param; ChatView "Auto-included" sidebar section

## Summarisation improvements (Phase C)
- Threshold 20→30, keep-recent 6→8, max_tokens 200→400
- Two-pass summarisation for histories >50 messages

## Browser push notifications (Phase E)
- PushSubscription model + migration; pywebpush dependency
- /api/push routes; VAPID config; fire-and-forget on generation complete
- Frontend: sw.js, push store, Settings toggle

## PWA manifest (Phase F)
- manifest.json, Apple meta tags, service worker registration in main.ts

## Tag normalisation
- All tags lowercased + deduplicated at backend (create_note/update_note) and frontend (TagInput sanitize)
- Note/Task types gain project_id + milestone_id fields; store signatures updated

## CalDAV
- Radicale embedded server reverted; back to user-configured external CalDAV

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 20:52:21 -05:00
bvandeusen 432e0bd2a0 Show Qwen3 thinking output in chat as collapsible Reasoning block
Ollama streams message.thinking tokens alongside message.content when
think=True — previously silently dropped. Now forwarded end-to-end.

Backend:
- llm.py: ChatChunk type gains "thinking" variant; stream_chat_with_tools
  yields ChatChunk(type="thinking") for msg.thinking chunks before content
- generation_task.py: thinking chunks emit "thinking_chunk" SSE events
  (not added to content_so_far — not persisted to DB)

Frontend:
- types/chat.ts: Message.thinking?: string (session-only, not from DB)
- stores/chat.ts: streamingThinking ref; thinking_chunk handler accumulates
  chunks; on done, thinking carried into committed Message object then cleared
- ChatMessage.vue: collapsible <details class="thinking-block"> shown for
  messages that have .thinking content (collapsed by default)
- ChatView.vue + ChatPanel.vue: live thinking block in streaming bubble —
  open while only thinking is flowing, auto-collapses when content arrives;
  typing indicator hidden while thinking is active

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 23:16:59 -05:00
bvandeusen e119331645 Phase 21: Intent-first pipeline, visible ack, KV-stable system prompt
Pipeline changes (generation_task.py, intent.py):
- Remove optimistic streaming queue/race (_drain_queue deleted)
- Remove _generate_acknowledgment — ack now embedded in intent JSON
- Round 0: await intent (~400ms), stream ack immediately as TTFT,
  then execute tool sequentially; chat-only streams directly
- IntentResult.ack: one-sentence acknowledgment, intent max_tokens 200→350
- _parse_intent extracts and trims ack field

KV cache stability (llm.py, generation_buffer.py, generation_task.py):
- build_context: replace cached_note_ids with include_note_ids
- Auto-found notes populate context_meta["auto_notes"] for sidebar but
  are NOT injected into system prompt (--- Related Notes --- removed)
- Explicitly included notes injected as --- Included Notes ---
- _conv_note_cache dict + get/set/clear functions removed from generation_buffer.py
- All clear_conv_note_cache() calls removed

Cold model retry (llm.py):
- generate_completion (used by classify_intent) retries on HTTP 500:
  3 attempts with 3s/6s delays — prevents intent failure during cold load

API + frontend (routes/chat.py, stores/chat.ts, views/ChatView.vue, components/ChatPanel.vue):
- exclude_note_ids → include_note_ids throughout
- ChatView sidebar: Suggested (auto-found, + to include) + In Context (× to remove)
- ChatPanel: remove exclude button from context pills; no IDs passed to sendMessage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 22:34:54 -05:00
bvandeusen 316a85e13b Phase 20: Dedicated tag field — chip input, explicit tags array
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>
2026-02-26 06:36:35 -05:00
bvandeusen b37e15d59a Add Authentik OAuth/OIDC SSO, email change, and setup docs
Phase 18 changes:

OAuth/OIDC SSO (Authorization Code + PKCE):
- alembic/versions/0015_add_oauth_fields.py: add oauth_sub UNIQUE column,
  drop NOT NULL on password_hash
- src/fabledassistant/services/oauth.py: OIDC discovery (cached), build_auth_url,
  exchange_code, get_userinfo, find_or_create_oauth_user (sub→email auto-link→create)
- src/fabledassistant/routes/auth.py: GET /api/auth/oauth/login and
  GET /api/auth/oauth/callback; LOCAL_AUTH_ENABLED guards on login/register;
  /api/auth/status now returns oauth_enabled + local_auth_enabled
- src/fabledassistant/config.py: OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET,
  OIDC_SCOPES, LOCAL_AUTH_ENABLED, oidc_enabled() classmethod
- src/fabledassistant/models/user.py: password_hash nullable, oauth_sub field,
  has_password bool in to_dict()
- src/fabledassistant/services/auth.py: create_user accepts password=None +
  oauth_sub kwarg; authenticate returns None for OAuth-only users;
  add get_user_by_oauth_sub, link_oauth_sub, update_user_email
- frontend: AuthStatus + User types updated; auth store exposes oauthEnabled +
  localAuthEnabled; LoginView shows SSO button / hides password form accordingly

Email change:
- PUT /api/auth/email: requires password confirmation for local-auth users,
  skips check for OAuth-only users; enforces email uniqueness
- SettingsView.vue: new Email Address section pre-filled with current email,
  updates authStore.user in-place on success

Docs:
- docs/oauth-setup.md: step-by-step Authentik provider setup, example
  docker-compose env vars, account linking explanation, per-provider issuer URL table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 20:12:13 -05:00
bvandeusen 667ccd70cd Security audit, bug fixes, auto-save, and writing assistant improvements
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>
2026-02-19 20:08:44 -05:00
bvandeusen 32e4ee12f2 Add persistent context sidebar, note title fix, and expanded tool suite
Context sidebar + note title:
- ChatView: replace ephemeral context pills with a persistent right-panel sidebar;
  auto-found notes accumulate across turns; attached note shows with pin icon;
  × button excludes a note from future auto-search; hidden on mobile
- routes/chat.py: batch-fetch note titles via get_notes_by_ids() and inject
  context_note_title into each message dict at conversation load time
- notes.py: add get_notes_by_ids() batch fetch helper
- types/chat.ts: add context_note_title field to Message interface
- stores/chat.ts: sendMessage accepts optional 5th arg contextNoteTitle,
  included in optimistic user message
- ChatMessage.vue: context badge shows note title instead of 'Note #N'

Expanded LLM tool suite (all with intent router rules + ToolCallCard display):
- delete_note / delete_task: permanent delete with user confirmation (write tool),
  type-safe (refuse to delete wrong type), clears note context cache on success
- get_note: fetch full note body by query (search_notes returns only 200-char preview)
- list_notes: browse notes by recency/keyword/tags with limit; notes only
- update_note: add tags + tag_mode (replace/add/remove) parameters
- search_notes: add optional type filter ("note" | "task")
- search_todos (CalDAV): keyword-filter todos, companion to list_todos
- caldav.py: add search_todos() built on top of list_todos()
- generation_task.py: register new tools in _WRITE_TOOLS, _TOOL_LABELS, _TOOL_ACTIONS
- llm.py: update available actions list and guidance in system prompt
- intent.py: routing rules for all new tools

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 14:40:34 -05:00
bvandeusen 24d3c5bc68 Enable thinking mode in full chat view, keep disabled in widget/panel
stream_chat_with_tools now accepts a think parameter. run_generation
forwards it to Ollama. The message POST route reads think from the
request body. ChatView passes think=true so qwen3 uses chain-of-thought
reasoning for full conversations; the dashboard widget and ChatPanel
omit it, staying fast. Dashboard button updated to "Think it through
in Chat →" to signal the deeper capability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 21:06:54 -05:00
bvandeusen 815eed2574 Add tool confirmation UI with Accept/Decline for write operations
Before executing any write tool (create/update/delete), the backend now
pauses with an asyncio.Future and emits a tool_pending SSE event. The
frontend displays a ToolConfirmCard with Accept and Decline buttons.
Clicking Accept resolves the Future and proceeds; Decline records a
declined tool_call chip and falls through to regular streaming. Typing
single-word yes/no responses (e.g. "yes", "cancel") also works as
confirmation. 120s timeout auto-declines if no response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 20:43:47 -05:00
bvandeusen 765e99bb24 Fix duplicate message bug and add generation timing instrumentation
Bug fix:
- ChatView.vue onMounted now skips fetchConversation when the conversation
  is already loaded in the store (same guard that the convId watcher uses).
  This prevents duplicate assistant messages when navigating from the
  dashboard inline chat to /chat/:id after streaming completes.

Generation timing:
- logging.py: add log_generation() — persists per-generation timing
  breakdown to app_logs (category=usage, action=generation) including
  model, total_ms, intent_ms, ttft_ms, generation_ms, and per-tool timings.
  Queryable via existing admin log viewer.
- generation_task.py: collect wall-clock timestamps at every pipeline stage:
  intent classification, per-tool execution (both intent-routed and native),
  time-to-first-token (measured from generation start to first content chunk),
  LLM streaming round duration. Logs via log_generation() and includes timing
  in the SSE 'done' event payload.
- types/chat.ts: add GenerationTiming interface; add optional timing field
  to Message.
- chat.ts: capture timing from done event and attach to assistant message.
- ChatMessage.vue: show timing footer on assistant messages with breakdown:
  "⏱ 4.2s total · first token 0.8s · analyzed 0.3s · created event 0.4s
  · generated 3.5s". Visible this session; persisted to app_logs for
  cross-session benchmarking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 18:18:46 -05:00
bvandeusen fbce540638 Add streaming status UX and model load state indicator
Streaming status transparency:
- generation_task.py emits 'status' SSE events at each pipeline stage:
  "Analyzing your request..." before intent classification, tool label
  before each tool execution, "Generating/Composing response..." before
  each LLM streaming round
- chat.ts adds streamingStatus ref; cleared on first chunk or done/error;
  includes fast 5s poll loop after warmModel() until model shows as loaded
- ChatView.vue shows pulsing dot + italic status label above content area;
  falls back to blinking cursor once content arrives
- HomeView.vue shows status label in dashboard panel instead of '...'

Model load state indicator:
- /api/chat/status now queries /api/tags and /api/ps in parallel to
  distinguish installed-but-cold vs loaded-in-VRAM model states
- New model status values: 'not_found' | 'cold' | 'loaded' (was 'ready')
- chatReady true for both 'cold' and 'loaded' (cold models still work)
- AppHeader shows 5 states: gray pulse (checking), red (Ollama down),
  orange (not installed), yellow pulse (cold), green (loaded)
- Inline short label ("Cold", "Ready", "Offline", etc.) visible without
  hovering; detailed tooltip on hover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 17:54:37 -05:00
bvandeusen d7bc3f3222 Add CalDAV calendar integration, LLM-suggested tags, and settings refinements
- 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>
2026-02-15 22:40:20 -05:00
bvandeusen 8996b45e50 Add LLM tool calling for creating tasks, notes, and searching from chat
Ollama tool/function calling integration allows the LLM to create tasks,
create notes, and search existing notes on behalf of the user during chat.
Multi-round tool loop (max 5 rounds) lets the model execute tools then
produce a natural language response. Tool results are persisted in a new
JSONB column on messages and rendered as compact cards with linked titles.

- Migration 0013: add tool_calls JSONB column to messages
- New services/tools.py: tool definitions + execute_tool dispatcher
- llm.py: ChatChunk dataclass, stream_chat_with_tools(), date in system prompt
- generation_task.py: multi-round tool call loop with SSE tool_call events
- Frontend: ToolCallRecord type, streamingToolCalls in store, ToolCallCard
  component, rendering in ChatMessage and ChatView streaming bubble

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 23:34:36 -05:00
bvandeusen f089b16080 Improve model status indicators and tune timeouts
Green/red emoji circles replace Unicode symbols for at-a-glance model
readiness. Increase connect timeouts (10→30s) for cold model loading,
warm timeout (120→300s) for large models, and pull timeout (600→1800s)
to match route-level limit. Show error toast on SSE reconnection failure
instead of silently recovering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 23:01:59 -05:00
bvandeusen 953eaf2feb Add model selection, dashboard chat input, and model warming
- Add GET /api/chat/ps and POST /api/chat/warm endpoints for hot model
  visibility and pre-loading
- Extend PATCH /api/chat/conversations/:id to accept model in addition
  to title
- Add ModelSelector component with hot/cold indicators from Ollama /api/ps
- Add DashboardChatInput component (model selector + note picker + textarea)
  replacing the simple "New Chat" button on the dashboard
- Add model selector dropdown to ChatView header, persisted per-conversation
- Warm default model on dashboard mount via fire-and-forget background task
- Configure Ollama with OLLAMA_MAX_LOADED_MODELS=2 and OLLAMA_KEEP_ALIVE=30m
- Always-visible edit buttons on NoteCard/TaskCard (remove hover-only behavior)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 18:49:06 -05:00
bvandeusen f77b029943 Add registration control, admin user management, and security hardening
- Registration auto-closes after first user; admin can toggle from /admin/users
- Admin user management view with user list and delete
- Password confirmation on registration form
- Password change in Settings (PUT /api/auth/password)
- Session cookie hardening: HttpOnly, SameSite=Lax, optional Secure flag
- Startup warning when SECRET_KEY is default
- Production deployment docs: reverse proxy, rate limiting, CSP headers
- Fix assist prompt to preserve markdown headings in target sections
- Simplify prod compose networking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:49:22 -05:00
bvandeusen cbfdf5289e Add multi-user auth, background generation, and chat UX improvements
Phase 5: Multi-user authentication with session cookies, bcrypt passwords,
first-user-is-admin pattern, per-user data isolation, backup/restore,
Docker Swarm production stack with secrets and network isolation.

Phase 5.1: Chat UX improvements:
- Background generation architecture (GenerationBuffer + asyncio task)
  with SSE fan-out, reconnect support, and periodic DB flushes
- LLM-generated conversation titles (first exchange + every 10th message)
- Stop generation button with cancel_event and partial content preservation
- Relative timestamps in sidebar (5m ago, 3h ago, then dates)
- Empty chat auto-cleanup on navigation away
- Save-as-note uses LLM for title generation, tags notes with "chat"
- Summarize-as-note also tags with "chat"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 14:36:30 -05:00
bvandeusen fb18d2c41d Add note context visibility in chat and standardize UI design tokens
Improve chat context: build_context() now returns metadata about auto-found
notes, emitted as an SSE event so the frontend can display context pills
showing which notes influenced the response. Users can promote notes for
deeper context (+) or exclude irrelevant ones (x). A note picker lets users
manually attach notes. Multi-word search uses per-term AND matching, and
auto-search iterates keywords individually for broader OR-style coverage.

Standardize styling: introduce CSS design tokens (--radius-sm/md/lg/pill,
--color-success/warning/overlay, --focus-ring) and migrate all components
to use them. Fix header alignment to full-width, add active nav link state,
replace hardcoded colors with CSS variables, and normalize button padding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 06:49:12 -05:00
bvandeusen 39bcd7a8fa Stream model pull progress via SSE instead of fire-and-forget
Replace the asyncio.create_task() fire-and-forget pattern for model
downloads with SSE streaming that relays Ollama's pull progress in
real time. The frontend now shows live download percentage instead of
a static "Pulling..." label with blind polling.

- routes/chat.py: SSE streaming endpoint with 1800s timeout
- stores/settings.ts: pullModel uses apiStreamPost with onProgress callback
- SettingsView.vue: live "Downloading 42%" display, removed polling/setTimeout
- summary.md: updated to reflect SSE streaming for model pulls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 22:34:58 -05:00
bvandeusen 38b1ac933e Add settings page, model management, and chat UX improvements
- Settings infrastructure: key-value settings table, GET/PUT API, Pinia store
- Configurable assistant name (default "Fable") in settings and LLM system prompt
- Model catalog with 18 models in 3 categories (General Purpose, Coding,
  Uncensored / Creative Writing) with download/select/remove functionality
- Move Ollama status indicator from chat views to global nav bar
- Chat bubble layout: user messages right-aligned, assistant left-aligned
- Floating dark input bar with auto-focus and circular send button
- Fix HTML entity rendering (&#39; apostrophe issue in marked/DOMPurify pipeline)
- Fix new chat button navigation (fetchConversation before router.push)
- Recent chats section on home page with "New Chat" button
- Update summary.md with Phase 4.5 changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 21:32:02 -05:00
bvandeusen 834fd80640 Add Ollama health check status indicator to chat views
Adds visibility into whether Ollama is reachable and the configured model
is ready before allowing chat. Prevents silent failures when the model is
still downloading or Ollama is unavailable.

- Backend: GET /api/chat/status endpoint checks Ollama /api/tags (5s timeout)
  Returns ollama availability + model readiness + default model name
- Frontend: OllamaStatus type, chat store polling (30s interval)
- ChatView: status dot + label in header (green/yellow/red), disabled send
- ChatPanel: compact status indicator in panel header, disabled send
- summary.md: updated with new endpoint, store changes, and component descriptions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 19:33:42 -05:00
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
bvandeusen 807cde30be 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>
2026-02-10 17:56:12 -05:00
bvandeusen 22a3a3c1d1 Initial commit: note-taking/task-tracking app with LLM integration scaffold
Vue 3 + TypeScript frontend with Pinia stores, markdown rendering (marked + DOMPurify),
wikilink/tag linkification, and autocomplete. Quart async backend with SQLAlchemy 2.0,
PostgreSQL ARRAY columns, task-note companion linking, backlinks, and note-to-task
conversion. Docker Compose setup with PostgreSQL 16 and Ollama.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 23:35:44 -05:00