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>
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>
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>
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>
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>
- _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>
- 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>
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>
- 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>
- 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>
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>
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>
create_note now only checks against existing notes (is_task=False);
create_task only checks against existing tasks (is_task=True). All three
checks (exact title, fuzzy title, semantic similarity) pass the type
filter through to list_notes and semantic_search_notes.
Adds is_task param to semantic_search_notes() so callers can restrict
results to notes or tasks independently.
Prevents a note titled "Design enemy AI" from blocking a task with the
same title, and vice versa.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Raises body character limit from 80 → 200 (skips short/stub notes)
and threshold from 0.87 → 0.90 (only flags near-identical content).
Enemy design notes sharing a template will pass through cleanly;
accidentally duplicated notes with the same detailed content will
still trigger the confirmation prompt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- 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>
- sw.js: suppress notification when the target chat tab is already focused
(clients.matchAll visibility check before showNotification)
- generation_task.py: provide meaningful body for tool-only responses
(lists tool names instead of sending an empty string that browsers discard);
promote scheduling failure from debug to warning
- push.py: promote send errors from warning to error with exc_info;
log successful sends at INFO so they're visible in normal operation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The semantic similarity check was flagging unrelated short-title tasks
as duplicates (e.g. "Lore: Shell 0" matching "Lore: Reinitialization 0"
at 91%) because with no body, the embedding is purely title-based and
co-domain tasks in the same project share a tight embedding neighborhood.
Only run the semantic check when the body is ≥ 80 chars — enough
content to make a meaningful comparison. The fuzzy title check already
covers exact/near-exact title duplicates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 404 handler was unconditionally serving index.html (200) for all
non-API, non-static paths, including scanner probes for .php, .asp, .cgi
etc. Added _SPA_EXTENSIONS set so paths with unknown extensions get a
real 404 instead of a misleading 200.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The semantic similarity check was flagging unrelated short-title tasks
as duplicates (e.g. "Lore: Shell 0" matching "Lore: Reinitialization 0"
at 91%) because with no body, the embedding is purely title-based and
co-domain tasks in the same project share a tight embedding neighborhood.
Only run the semantic check when the body is ≥ 80 chars — enough
content to make a meaningful comparison. The fuzzy title check already
covers exact/near-exact title duplicates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 404 handler was unconditionally serving index.html (200) for all
non-API, non-static paths, including scanner probes for .php, .asp, .cgi
etc. Added _SPA_EXTENSIONS set so paths with unknown extensions get a
real 404 instead of a misleading 200.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WorkspaceTaskPanel: add milestone <select> in task detail (PATCH /api/notes/:id),
replace delete ✕ with trash can SVG icon
- WorkspaceView: scroll to bottom when streaming ends so final message is visible
without a page refresh
- ToolCallCard: fix search_notes result count (was reading data.total; tool returns
data.count), so results no longer show "0 found"
- push.py: switch from deprecated WebPusher().send(vapid_private_key=...) to
webpush() function (pywebpush 2.x API compatibility)
- app.py: downgrade /api/health, /api/chat/status, and static asset requests from
INFO to DEBUG in after_request logger to reduce log noise at default log level
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /graph route with D3 force simulation (GraphView.vue)
- Tag nodes as first-class graph nodes (string IDs "tag:name") — clicking
navigates to /notes?tags=name; tags shown by default
- Invisible project hub nodes attract project members into clusters
- Physics panel with live sliders: repulsion, link distance, link strength,
project pull, gravity (forceX/forceY, not forceCenter)
- Wikilink edges retain directed arrowheads; tag edges are thin, no arrowhead
- Graph nav link in AppHeader; `g` shortcut in App.vue
- Backend: build_note_graph() emits tag nodes + note→tag edges instead of
O(n²) note→note shared-tag mesh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add session_version to users table. Sessions now carry session_version
alongside user_id; @login_required rejects any session where the version
doesn't match the DB value.
- migration 0024: session_version INTEGER NOT NULL DEFAULT 1
- models/user.py: session_version Mapped[int] column
- auth.py: version mismatch → session.clear() + 401
- services/auth.py: change_password and reset_password_with_token both
increment session_version, evicting all other live sessions
- routes/auth.py: login/register/oauth_callback store session_version;
update_password rehydrates current session with new version so the
user who changed their password stays logged in
Existing sessions will require one re-login after the migration runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>