Backend:
- tools.py: apply UTC normalization to update_event datetime fields
(matched create_event which already did this)
- events.py service: allow end_dt/recurrence/project_id to be cleared
via update_event by permitting None for nullable fields
- events.py service: find_events_by_query now returns upcoming events
first, falling back to past — prevents AI tools from mutating stale
past events when a future match exists
- events.py service: list_events now uses overlap logic (start <= to
AND end >= from) so multi-day events spanning the query boundary
are included; previously only start_dt was checked
Frontend:
- ToolCallCard: fire fable:calendar-changed on created/updated/deleted
so CalendarView refetches without requiring a manual page refresh
- KnowledgeView: replace raw apiGet('/api/events') with listEvents()
client function; also fix today bar which was reading .events off a
flat array (always empty) — now correctly receives EventEntry[]
- HomeView: use full ISO strings for event date range instead of naive
UTC-midnight strings; deduplicate inline date math via _dateRange()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UTC midnight passed to FullCalendar's timeZone:'local' was being
converted to local time, shifting all-day events back by 1+ days for
users in UTC-X zones. The edit form had the same bug via new Date().
Fix: pass YYYY-MM-DD slices (UTC date) for all-day events in both
toFcEvent and EventSlideOver resetForm, bypassing timezone conversion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- GET /api/knowledge/ids: returns up to 100 note IDs cheaply (no body
parsing), supports same filters as /api/knowledge, includes has_more
- GET /api/knowledge/batch?ids=...: fetches full items for given IDs in
order; used by frontend to load content in controlled batches
Frontend (KnowledgeView):
- Fetch 100 IDs upfront, load first 50 as content on mount
- IntersectionObserver sentinel (root: null) triggers 24-item content
batches as user scrolls
- Proactive ID refill when queue drops below 48 unloaded IDs
- fetchGen counter invalidates stale in-flight responses on filter reset
- IDs claimed before async fetch to prevent double-loading
- sentinelVisible ref drives post-load re-check when content doesn't
push sentinel off screen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Coalesces rapid scroll events into one check per animation frame so
that page++ can only fire once per frame, eliminating the window where
multiple events slip through before loading=true is observed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Swap IntersectionObserver (race-prone, fired immediately on creation)
for a passive scroll listener on the grid container — eliminates
duplicate page loads caused by observer re-creation after DOM updates
- Increase card min-height 100px → 160px so tags + snippet are visible
- Increase snippet line-clamp 3 → 4 for more content preview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sentinel was first in DOM with order:9999, causing layout recalculation to
trigger IntersectionObserver multiple times (intersecting→not→intersecting)
as items were appended, producing duplicate pages. Move sentinel to AFTER
the v-for items so it's naturally last in both DOM and visual order.
Remove overflow:hidden from .k-card-tags — .k-card overflow:hidden already
clips escaping content; the extra overflow on the tags container was
collapsing its height to zero and hiding all tag pills.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added overflow:hidden to .k-card so wrapped tags are clipped within the
card border-radius. Added min-width:0 + overflow:hidden on .k-card-tags
so the flex item can shrink properly and doesn't push past the card width.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The IntersectionObserver fires as soon as it's created (sentinel immediately
intersecting after page 1 renders), while the removed backup check also fires
in the same tick — two concurrent fetchItems(page 2) calls produced duplicate
cards. With sentinel now properly inside the scrolling root, the observer
alone handles progressive loading.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KnowledgeView: sentinel was OUTSIDE the card-grid div, making
IntersectionObserver (root: cardGridEl) never fire since the target must
be a descendant of the root. Moved sentinel inside card-grid with
grid-column:1/-1 + order:9999 so it spans all columns and sits at the
bottom. Fixed backup check to compare against container bounds not viewport.
tools.py list_events: apply same UTC normalization as create_event (treat
naive datetimes as UTC, handle Z suffix). Update tool description to
explicitly request full-day UTC ranges so the LLM doesn't send local time
without offsets, which caused the recall query to miss UTC-stored events.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Click the month name in the FullCalendar toolbar to open a popover with
prev/next year arrows and a 4×3 month grid. Clicking a month jumps the
calendar to that month via gotoDate(). Current month highlighted. Picker
closes on outside click. Title gains hover highlight + pointer cursor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KnowledgeView: IntersectionObserver was watching the viewport instead
of the card-grid scroll container, causing infinite scroll to stop
loading after only ~29 items. Pass card-grid element as `root`.
- CalendarView: listen for 'fable:calendar-changed' custom event and
call refetchEvents() so tool-created events appear without navigation.
- ChatPanel: dispatch 'fable:calendar-changed' when create_event,
update_event, or delete_event tool calls succeed.
- tools.py: normalize naive datetimes to UTC before storing events so
timezone comparisons in list_events queries are always consistent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, await audio.play() resolves immediately after source.start(),
so the playQueue chains the next sentence before the current one finishes,
causing overlapping / interrupted playback.
Exposes OLLAMA_BACKGROUND_MODEL as a per-user setting in General settings,
alongside the Chat Model selector. Includes an inline warning when the same
model is selected for both, explaining the KV cache performance impact.
All background task callers (title generation, tag suggestions, project
summaries, RSS classification) now read background_model from user settings,
falling back to OLLAMA_BACKGROUND_MODEL env var.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Background tasks (title generation, tag suggestions, project summaries,
RSS classification) were using qwen3:8b and wiping its KV cache after
every response, preventing prefix cache hits on subsequent user messages.
Adds OLLAMA_BACKGROUND_MODEL (default: qwen2.5:0.5b) config var and
routes all background LLM calls to it, keeping qwen3:8b's KV cache
warm between user messages for consistent sub-second TTFT.
Also adds infinite scroll to KnowledgeView (replaces load-more button)
and bakes spaCy en_core_web_sm into the Docker image to eliminate the
pip install on every startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KnowledgeView minichat: render assistant messages through renderMarkdown
so headers, bold, lists etc. display correctly instead of raw markdown
- get_weather tool: read user's temp_unit from briefing_config and convert
temperatures to °F when preferred; also include temp_unit in the
returned payload so the model can label values correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KnowledgeView minichat: add margin-inline: auto so the widget centers
within the content area when max-width is reached on wide screens
- weather get_weather tool: return success: true on both the arbitrary
location path and the cached locations path so ToolCallCard shows
the correct success state instead of always flagging as error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chat section and inline response now have max-width 720px centered
within the page, and quick action chips are centered. Prevents the
widget from stretching the full 1200px content width on wide screens.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- renderMarkdown() accepts interactiveCheckboxes option: removes disabled=""
and stamps data-task-index on each checkbox in the marked HTML output
- NoteViewerView detects list notes by body content (- [ ] / - [x] pattern)
and passes interactiveCheckboxes: true when rendering
- onBodyChange() handles checkbox change events: toggles the matching line
in the body, optimistically updates the store, then PATCHes the note
- prose.css adds .prose--checklist rules for marked output: no bullet,
flex row, accent-color, line-through on checked items via :has()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KnowledgeView:
- Watch streamingToolCalls; call fetchItems+fetchTags on create/update
note or task so the card grid reflects changes made via the minichat
- Cap minichat to max-width: var(--page-max-width) so it matches chat column width
WorkspaceView + WorkspaceNoteEditor:
- Expose reload() from WorkspaceNoteEditor via defineExpose
- Call noteEditorRef.reload() alongside taskPanelRef.reload() when
create_note/update_note tools succeed in the SSE watcher
KnowledgeView list cards:
- Backend: parse markdown task list into list_items [{text, checked}] + body
- Card renders up to 6 items with real checkboxes; toggleListItem()
does an optimistic update then PATCHes /api/notes/:id
- Progress bar kept below items; "+N more" shown when list is long
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Drop max-width from .knowledge-root so the graph panel can use the full
viewport width without hitting a cap
- Drop max-width from .chat-page (message bubbles are already self-constraining)
- Increase normal graph panel width 420px → 500px
- Increase expanded width to min(960px, 60vw) so it scales with the viewport
and updates minichat right-offset to match
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Layout:
- Chat and Knowledge views now max out at 1600px and center on wide
screens, consistent with the rest of the app; app-content overflow
is set to hidden for both so they manage their own scroll
Graph panel (Knowledge):
- Open/closed state persisted to localStorage (fa_knowledge_graph_open)
— stays open across navigation and page refreshes
- Expanded state persisted (fa_knowledge_graph_expanded): chevron button
in the panel header toggles between 420px (normal) and 700px (expanded)
- Minichat right offset follows the panel width with a matching transition
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs:
1. Manual 'Refresh' button didn't refresh weather/RSS before compiling.
The daily scheduler calls refresh_all_feeds + refresh_location_cache
before run_compilation; the manual trigger route called run_compilation
directly, reading a stale cache. Manual trigger now mirrors the
scheduler: refresh feeds and all configured weather locations first.
2. Navigating to WorkspaceView during a long briefing compilation caused
the workspace chat to show the briefing content. triggerNow awaits
~30-60s; on completion it called loadAll() → chatStore.fetchConversation
which overwrote the workspace's currentConversation in the shared store.
Fixed with a _mounted flag — all post-async state writes in BriefingView
are now guarded so they no-op if the component has been unmounted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Define --color-surface in theme.css (light: #f0f0f8, dark: #1a1b22)
— was used across 10+ components with no-op fallbacks; now properly
defined alongside --color-bg and --color-bg-card
- Extract shared date formatters into utils/dateFormat.ts:
fmtTime, fmtDateTime, fmtRelativeDateTime, fmtDayLabel, fmtCompact
- Replace duplicate inline formatters in CalendarView, HomeView
(formatUpcomingTime), and KnowledgeView (formatEventDate)
- CalendarView: replace hardcoded rgba(99,102,241,0.4) hover colour
with color-mix(in srgb, var(--color-primary) 40%, transparent);
fix --color-input-bg fallback to use var(--color-bg); remove
hardcoded hex fallbacks now that --color-surface is defined
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
B — Event popover: clicking a calendar event shows a compact overlay with
full details (title, time range, location, description, color accent)
and Edit/Close actions; positioned relative to the click, closes on
outside click; Edit opens the existing EventSlideOver
C — Upcoming strip: scrollable section below the calendar showing the
next 4 weeks of events grouped by day (Today/Tomorrow/date label),
each card with color accent bar, title, time, location, description
snippet; clicking a card opens EventSlideOver for edit
Both features stay in sync with create/update/delete operations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract listen mode into a shared useListenMode() composable backed by
localStorage ('fa_listen_mode'). ChatView and BriefingView both use it,
so toggling auto-read on in one view keeps it on after navigation or
page refresh — no need to re-enable it each visit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs in discussArticle():
- Scroll selector was '.briefing-chat' (doesn't exist) → '.briefing-center';
the panel never scrolled into view so the response was invisible until refresh
- Only 300-char snippet was sent to the LLM; now passes the full stored
content (up to 2000 chars) from the backend
- User message wasn't shown until streaming ended; now added optimistically
to messages[] immediately on click so it appears straight away
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KnowledgeView mini-chat: replace harsh border-top with upward box-shadow
and rounded top corners (16px); remove padding-bottom from content area
so widget truly overlays cards without pushing layout; add collapse
toggle (chevron) that hides messages without closing the conversation
- WeatherCard: show precip_mm as fallback when precipitation_probability_max
is null but actual rainfall is expected (Open-Meteo omits probability for
some forecast days even when rain is shown)
- Pass precip_mm through weather service → frontend type definitions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Mini-chat input bar now uses shared --color-input-bar-* CSS variables and
the same chat-input-bar pill pattern as all other chat interfaces
- chatInputEl focused on mount (autofocus on page load)
- Graph panel: replaced iframe (blocked by X-Frame-Options: DENY) with
inline GraphView component; CSS :deep override constrains its height
to fill the panel instead of 100vh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Explicitly instruct the LLM to respond conversationally and not use any
research/search tools when summarizing a shared article excerpt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ChatView: listen mode toggle (auto-reads new responses via TTS), volume popup
with range slider persisted per-device in localStorage via GainNode
- useVoiceAudio: shared module-level _volume ref with localStorage persistence,
GainNode for volume control, exported setVoiceVolume()
- tts.py: pre-warm all Kokoro voices at pipeline load to eliminate HuggingFace
HEAD requests at synthesis time (reduces TTS latency)
- BriefingView: discuss article button now auto-sends instead of just filling input;
prompt capped to 15 sentences; send() accepts optional overrideText
- llm.py: instruct LLM not to proactively search notes or comment on note absence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
synthesiseSpeech() called without explicit params now omits voice/speed/
blend from the request body. The backend detects this and auto-loads all
three from the user's saved settings (voice_tts_voice, voice_tts_speed,
voice_tts_blend), so briefing listen mode respects the voice the user
configured in Settings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fetch precipitation_probability_max from Open-Meteo (replaces precip_sum
in the card display — probability is more useful at a glance than mm)
- Show WMO condition description text on each forecast day
- Convert wind speed to mph when temp unit is F; pass wind_unit in response
- Display 💧 X% chance of rain; 💨 X mph/km/h wind per day
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking 💬 on a news card in the briefing panel pre-fills the briefing
chat input with the article title, snippet, and source so the user can
ask the briefing LLM to summarize or discuss it directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
new Audio().play() after an async await loses the user gesture context and
is silently blocked. Creating AudioContext synchronously before the fetch
preserves the permission, then decode/play through it after the await.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>