Commit Graph

150 Commits

Author SHA1 Message Date
bvandeusen 62dbb8d496 refactor: centralise user timezone as a standalone setting
Browser timezone is now synced to user_settings["user_timezone"] on
every login/page load (App.vue). The briefing scheduler and LLM context
both read from this single source, falling back to the legacy
briefing_config.timezone for existing users during migration.

- App.vue: PUT /api/settings with browser IANA timezone on startAppServices
- routes/chat.py: fall back to stored user_timezone when not sent in request
- briefing_scheduler: read user_timezone setting; briefing_config.timezone
  kept as fallback only
- routes/briefing.py: pass tz_override from user_timezone to live-patched scheduler
- Remove timezone field from BriefingConfig interface and all briefing UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 20:17:51 -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 e0c3836fab feat: event cards in chat/briefing, upcoming events on dashboard, briefing uses internal store
- ToolCallCard: event list items replaced with rich clickable cards (color dot,
  title, time, location); clicking opens EventSlideOver for edit/delete; single
  create/update events in header are also clickable; updated all event types to
  use start_dt/end_dt fields from internal store
- HomeView: new upcoming events widget shows today + next 7 days as a card grid
  above the hero project; clicking any card opens EventSlideOver inline
- briefing_pipeline: _gather_internal now queries the internal events store for
  today's events; CalDAV events are still appended (deduped) if configured

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 15:39:13 -04:00
bvandeusen df1194ee96 fix(tools): remove duplicate datetime import in get_weather handler
The local `from datetime import datetime, timezone as _tz` inside
execute_tool() shadowed the module-level datetime import for the entire
function scope, causing UnboundLocalError in all calendar tool handlers
(list_events, create_event, update_event). Fixed by importing only
timezone as _tz — datetime is already available at module level.

Also removes the now-unnecessary noqa: F823 suppression.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:59:05 -04:00
bvandeusen 0277f5744f fix(events): use g.user.id instead of g.user_id in route helper
auth.py sets g.user (the User object), not g.user_id directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:12:12 -04:00
bvandeusen 809d8e0008 fix(lint): suppress ruff F823 false positive in create_event handler
Ruff 0.15.7 incorrectly flags datetime.fromisoformat() as a
"local variable referenced before assignment" inside a try block
within execute_tool(). datetime is imported at module level and
is not a local variable. Added noqa comment at the specific line.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:45:25 -04:00
bvandeusen c3e201d26a feat(calendar): internal event store with FullCalendar UI and CalDAV push sync
- AI calendar tools now always available (moved from _CALDAV_TOOLS to _CORE_TOOLS);
  create/list/search/update/delete events go through the internal DB store first,
  with fire-and-forget CalDAV push sync when the user has CalDAV configured
- Add EventEntry interface and typed API helpers (listEvents, createEvent,
  getEvent, updateEvent, deleteEvent) to client.ts
- Install @fullcalendar/vue3, daygrid, timegrid, interaction, core packages
- Add EventSlideOver.vue: create/edit/delete slide-over with title, start/end,
  all-day toggle, location, description, color picker, and project selector
- Add CalendarView.vue: month/week/day FullCalendar with drag-drop and resize
  wired to PATCH /api/events/:id; click empty date opens create slide-over
- Wire /calendar route, Calendar nav link in AppHeader, g+l keyboard shortcut

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:38:09 -04:00
bvandeusen 8d330afc6d feat(calendar): events REST API blueprint and registration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 12:53:52 -04:00
bvandeusen 90ca667df2 feat(calendar): implement events service with CalDAV push
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 12:49:57 -04:00
bvandeusen b547f47f54 feat(calendar): update Event model and patch caldav.create_event with uid param
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 12:48:40 -04:00
bvandeusen 0c2d9c2f6c feat(briefing): add POST/DELETE /api/briefing/rss-reactions endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:39:55 -04:00
bvandeusen 359b5f0545 feat(briefing): trigger RSS classification after new items are stored
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:39:08 -04:00
bvandeusen dc93e0d39f feat(briefing): wire pre-processing pipeline; run_compilation returns (text, metadata)
- Task change detection via snapshot diff
- RSS scoring/filtering via briefing_preferences
- Weather card via parse_weather_card_data (staleness-gated)
- News card markdown format with ordering constraint
- Metadata stored on Message record via post_message()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:38:28 -04:00
bvandeusen e3c1e97cfa feat(briefing): add task change detection helpers and task_id to _gather_internal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:36:52 -04:00
bvandeusen 3b71549b91 feat(briefing): add briefing_preferences service for RSS scoring and filtering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:35:17 -04:00
bvandeusen 2ad07b5e06 feat(briefing): add rss_classifier service for LLM-based topic tagging
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:34:37 -04:00
bvandeusen 9e1615bd32 feat(briefing): add past_days/current_weather to Open-Meteo; add parse_weather_card_data()
Also adds get_cached_weather_rows() for parallel gather in briefing pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:32:59 -04:00
bvandeusen e44eb185d5 feat(briefing): extend post_message() to accept optional metadata dict
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:31:25 -04:00
bvandeusen 5665f484ed feat(briefing): add Message.msg_metadata and RssItem.topics/classified_at columns
msg_metadata maps to the 'metadata' DB column ('metadata' is reserved
by SQLAlchemy Declarative API). to_dict() exposes the key as 'metadata'
for frontend compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:30:37 -04:00
bvandeusen 553c38200a feat(fable-mcp): build wheel in Docker image, serve download, add admin log tool, and Settings install UI
- Dockerfile: build fable-mcp wheel into /app/dist/ during image build
- routes/fable_mcp_dist.py: GET /api/fable-mcp/info + /download endpoints
- app.py: register fable_mcp_dist_bp
- fable_mcp/tools/admin.py: get_app_logs() hitting /api/admin/logs
- fable_mcp/server.py: fable_get_app_logs MCP tool
- SettingsView: "Fable MCP" section in API Keys tab with download button and install instructions
- client.ts: getFableMcpInfo() helper
- ci.yml: add fable-mcp/** to trigger paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 17:26:05 -04:00
bvandeusen 6564a03c0e feat(settings): model management UI — pull, delete, VRAM status
Backend:
- Enrich GET /api/chat/models to also hit /api/ps and return loaded:bool
  and modified_at alongside name/size, using parallel gather

Frontend (Settings → General):
- Model list: each row shows name (monospace), size (GB/MB), 'in VRAM' badge
  if currently loaded, 'default' badge if it's the configured default
- Delete button per row; disabled while deletion in progress
- Pull form: text input (Enter submits) + Pull button
- Suggestion chips for qwen3:7b/14b/4b, llama3.1:8b, nomic-embed-text;
  disabled if already installed
- Progress display during pull: status text + determinate bar when
  Ollama reports total/completed, indeterminate animation otherwise
- Refresh button reloads the list; list auto-refreshes after pull/delete
- Link to ollama.com/library for model discovery

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 02:52:31 -04:00
bvandeusen 9cd0de3883 fix(briefing): deadlock in scheduler startup
start_briefing_scheduler was called from before_serving (event loop thread)
and used run_coroutine_threadsafe(...).result() which blocks the calling
thread waiting for the coroutine to complete — but since the calling thread
IS the event loop, the coroutine could never run, causing a 10s timeout and
zero jobs scheduled.

Fix: make start_briefing_scheduler async and await _get_briefing_enabled_users()
directly. Also use asyncio.create_task for the catch-up rather than
run_coroutine_threadsafe. The background thread jobs (_run_user_slot_sync)
continue to use run_coroutine_threadsafe correctly since they run on the
APScheduler thread, not the event loop thread.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 02:18:30 -04:00
bvandeusen a2ba90160c feat: kanban status buttons, task back-nav, RSS UI, weather search, briefing fixes
Project view:
- Add inline status advance buttons on kanban task cards (todo→in_progress,
  in_progress→done); buttons reveal on hover, stop link navigation

Task viewer:
- Back button navigates to task's project instead of /tasks when project_id set
- Esc key navigates to project (or /tasks); blurs focused element first

Quick capture:
- Use user's configured model instead of hardcoded Config.OLLAMA_MODEL
- Remove create_project from classifier prompt (tool not offered, caused
  task-shaped inputs to silently fall through to note fallback)

Briefing scheduler:
- Fix get_event_loop() → get_running_loop() so background thread uses the
  correct hypercorn event loop (jobs were scheduling but never executing)
- Suppress bare greeting when both LLM synthesis lanes return empty

RSS feed UI (SettingsView):
- Show last-fetched age, category badge, and feed URL per row
- Category input field when adding a feed
- Refresh all button: fetches latest items, reloads list, toasts with count
- Enter key submits add-feed form; better empty-state hint with example feeds

Weather tool:
- Accept any city/region name in addition to 'home'/'work'/'all'
- Geocodes via Nominatim + fetches live from Open-Meteo for arbitrary queries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 00:42:01 -04:00
bvandeusen a9414cf949 chore: merge main into dev, resolve tools.py conflict
Accepted main's semantic duplicate threshold (>= 80 chars) over dev's
(>= 200 chars) for both note and task body checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:16:09 -04:00
bvandeusen 47b4af281b feat: API key routes, search endpoint, conversation type wiring
- GET/POST/DELETE /api/api-keys blueprint registered
- GET /api/search?q=&content_type=&limit= semantic search endpoint
- create_conversation gains conversation_type param (default "chat")
- cleanup_old_conversations excludes mcp type from retention sweep
- POST /api/chat/conversations accepts conversation_type body field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 21:01:04 -04:00
bvandeusen e8a4ca915a feat: add bearer token auth to _check_auth, falls back to session
Checks Authorization: Bearer header first, hashes token, looks up in
api_keys. Read-only keys get 403 on non-GET. Admin routes inaccessible
to non-admin key owners. Session path unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 20:57:26 -04:00
bvandeusen 583a140485 feat: add ApiKey service with create/list/revoke/lookup
SHA-256 hashed keys, fmcp_ prefix, soft-delete revocation. Tests cover
pure logic (hash, prefix, format, uniqueness, scope validation) and
auth middleware bearer token path (pending auth.py update).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 20:56:51 -04:00
bvandeusen 7358909432 feat: add ApiKey model and migration 0027
Adds api_keys table with user_id FK, key_hash, key_prefix, scope
(read/write), last_used_at, revoked_at. SHA-256 hashed, soft-delete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 20:52:25 -04:00
bvandeusen a58dbe79f2 fix: fuzzy project resolution and guard against accidental project creation
- _resolve_project now uses reverse-substring + SequenceMatcher (≥0.55)
  so partial names like "Famous Supply" match "Famous Supply Co." reliably
- create_project runs similar-project checks before the confirmed gate so
  confirmed=true can't bypass them; threshold lowered to 0.55 to catch
  abbreviated names
- Error messages on project-not-found no longer suggest create_project,
  directing the model to list_projects first
- Tool description updated to explicitly prohibit calling create_project
  in response to a lookup failure
- tasks.py: fire-and-forget embedding update on create/update
- briefing_pipeline.py: await is_caldav_configured (was sync call)
- notes.py: remove erroneous duplicate count_query filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 07:44:27 -04:00
bvandeusen beb57876fb feat: update_milestone tool and workspace milestone refresh
- Add update_milestone LLM tool: accepts project + milestone title to
  look up, then applies any of title/description/status changes
- WorkspaceView SSE watcher now triggers taskPanelRef.reload() on
  create_milestone and update_milestone success, so milestone groups
  appear immediately without a manual refresh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 17:58:13 -04:00
bvandeusen 5ea3bb5aff feat: per-user timezone-aware briefing scheduler
Briefing slots (4am/8am/12pm/4pm) now fire in each user's local timezone
rather than UTC, so the schedule matches the user's actual day.

Backend:
- briefing_scheduler: replaced 4 global UTC cron jobs with per-user
  CronTrigger jobs keyed to the user's IANA timezone; update_user_schedule()
  live-patches the scheduler when config is saved (no restart needed)
- Catchup logic evaluates missed slots in the user's local timezone
- put_config route calls update_user_schedule() after saving

Frontend:
- New Timezone section in Briefing settings: text input pre-filled from
  browser (Intl.DateTimeFormat) with a Detect button for re-detection
- Slot times shown as fixed local times (4:00 am etc.) with the configured
  timezone displayed beneath, replacing the old UTC-conversion display
- timezone field added to BriefingConfig type and default

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 08:34:31 -04:00
bvandeusen 6e57ce4555 feat: temperature unit preference and slot timezone display for briefing
- Add °C/°F toggle in briefing settings; persisted in briefing_config.temp_unit
- briefing_pipeline reads temp_unit and converts Open-Meteo Celsius values
  before passing them to the LLM (both full compilation and slot injection)
- Scheduled Slots section now shows each UTC slot time converted to the
  user's browser local time, plus a line confirming which timezone the
  browser is using

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 08:29:40 -04:00
bvandeusen d2605287f7 fix: briefing pipeline — pass user_id to is_caldav_configured, use p.title not p.get() 2026-03-12 06:43:46 -04:00
bvandeusen d131621de6 feat: APScheduler briefing slots, catch-up logic, profile close-out
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:07:16 -04:00
bvandeusen b66e6e0ad5 feat: briefing conversation management API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:07:13 -04:00
bvandeusen ebbf1a91f2 feat: briefing pipeline — parallel gather, two-lane LLM synthesis
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:07:10 -04:00
bvandeusen bc2119c067 feat: briefing profile note service
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:07:05 -04:00
bvandeusen 42b0d8c4ac feat: filter chat conversations by type; add get_weather and get_rss_items LLM tools
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:46:08 -04:00
bvandeusen ae3dff0952 feat: briefing API routes — feeds CRUD, weather, config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:46:04 -04:00
bvandeusen 9bb054f3d5 feat: RSS service — feedparser fetch, cache, prune
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:46:01 -04:00
bvandeusen bbdcea57a7 feat: weather service — geocoding, Open-Meteo forecast, cache, change detection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:45:59 -04:00
bvandeusen 9d31c1bab7 feat: add RssFeed, RssItem, WeatherCache models
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:45:50 -04:00
bvandeusen fa81509c9f feat: add conversation_type and briefing_date to Conversation model
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:45:47 -04:00
bvandeusen 6d593a0ef6 Move history to sidebar, simplify task assist, add content deduplication
- VersionHistorySection.vue: new sidebar component — collapsible timestamp list,
  click any snapshot to see inline diff + Restore/Back; replaces the modal
- NoteEditorView: remove History toolbar button + HistoryPanel modal, add
  VersionHistorySection at bottom of sidebar
- TaskEditorView: remove floating overlay assist panel + toggle button; add
  Writing Assistant section in sidebar matching note editor (scope selector,
  instruction textarea, generate/proofread/accept/reject); main area now shows
  streaming preview and DiffView like note editor; add VersionHistorySection
- note_versions.py: add content deduplication before time-gap check — identical
  body + title + tags skips snapshot regardless of interval (git semantics)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 16:56:30 -04:00
bvandeusen 2cb4e6d3b2 Add version footer, task history UI, and note version retention policy
- App.vue: quiet version footer (fetches /api/version, 0.45 opacity)
- TaskEditorView: History button + HistoryPanel integration (mirrors NoteEditorView)
- note_versions.py: raise MAX_VERSIONS 20→50; add 5-min minimum interval gate
  to prevent autosave exhausting history slots

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 16:23:56 -04:00
bvandeusen 4636c9a973 Add CalVer build-time version tracking
- Dockerfile: ARG BUILD_VERSION=dev → ENV APP_VERSION baked into image
- ci.yml: BUILD_VERSION passed as build-arg; set to git tag name on v*
  tag builds, "dev" on branch builds
- routes/api.py: GET /api/version returns {"version": APP_VERSION}
- SettingsView: fetches /api/version on mount, displays in About section
  under General tab

Version source of truth is the git tag (YY.MM.DD.N CalVer).
pyproject.toml / package.json versions are no longer maintained.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 16:10:57 -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 878b149f4d feat(backup): v2 backup/restore with all models and FK re-mapping
Export now includes: projects, milestones, task_logs, note_drafts,
note_versions, push_subscriptions; full user fields (oauth_sub,
session_version); full message fields (status, tool_calls).

Restore v2 builds ID maps for all tables and patches FKs in dependency
order (users → projects → milestones → notes → child records). Notes
restored in two passes to handle self-referential parent_id correctly.
v1 backups still restore via _restore_v1 path (parent_id bug also fixed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 11:57:21 -04:00
bvandeusen 810f63e749 Associate research_topic notes with workspace project
run_research_pipeline now accepts project_id; generation_task.py passes
workspace_project_id when the tool is called from a workspace context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 09:29:56 -04:00