Add _build_briefing_article_context() helper to llm.py that reads
rss_item_ids from briefing message metadata and injects article content
into the system prompt. Pass conv_id through build_context() and
generation_task.py.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TaskStatus enum was missing 'cancelled' — the LLM tried to use it and
hit TaskStatus("cancelled") raising ValueError → 500. Added the value,
a migration to extend the task_status Postgres enum, and proper 400
validation guards on both create and update task routes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously metadata only stored rss_item_ids (integers); the full item data
was discarded after LLM synthesis. Now rss_items (id, title, url, source,
snippet, published_at) is also stored so clients can render per-story cards
without additional API calls.
Web BriefingView: replaces bare reaction-row buttons with news cards showing
source, headline (linked), 2-line snippet, and 👍/👎 per card.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The MCP fable_update_task tool calls PATCH /api/tasks/{id} but the route
only declared PUT. Adding PATCH to the same handler fixes the 405.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add parse_pagination() to routes/utils.py; replace 6 duplicate limit/offset extractions in notes, tasks, chat, projects, milestones routes
- Extract _enrich_shares() in sharing.py; eliminates identical 12-line loop in list_project_shares and list_note_shares
- Extract _deduplicate_by_permission() in sharing.py; eliminates identical deduplication blocks in list_shared_with_me for projects and notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract milestoneColor to utils/palette.ts; remove duplicate in HomeView + ProjectListView
- Create useBackgroundRefresh composable; wire into HomeView + BriefingView (removes manual setInterval/clearInterval boilerplate)
- Extract _loadTabContent() in SettingsView so watch and onMounted share one tab→loader mapping
- Move raw fetch() api-key calls to typed helpers in api/client.ts (listApiKeys, createApiKey, revokeApiKey)
- Drop unused onUnmounted import from BriefingView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The watch(activeTab) handler loads tab data on navigation, but not on
initial mount when localStorage restores a tab. Three more gaps:
- briefing: was inside the isAdmin guard — non-admin users who last
visited the Briefing tab would see an empty form
- users: no onMounted equivalent — admin user list never loaded
- logs: no onMounted equivalent — admin log viewer never loaded
Moves briefing outside the admin guard and adds users/logs inside it.
fetchApiKeys() and loadMcpInfo() were only wired to the activeTab watcher,
which fires on changes but not on initial mount. If localStorage had
'apikeys' as the last tab, both calls were skipped entirely — causing
an empty key list and no whl download button.
HomeView: setInterval every 90s refreshes events, orphan tasks/notes,
and hero next-up task. Never touches loading ref, so the page content
stays stable — only the data silently swaps when the fetch completes.
Skips when document.hidden or initial load is still in progress.
BriefingView: setInterval every 60s refetches today's messages, but
only when viewing today's conversation and not currently streaming.
Compares message count and last message content before updating to
avoid unnecessary re-renders.
Both timers are cleared on unmount.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
npm install -g npm@latest corrupts npm's own module tree inside Alpine,
breaking subsequent installs. Use npm ci instead (faster, deterministic).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a comprehensive instructions block to the FastMCP server covering
the data model hierarchy, valid enum values, tag format, integer-or-none
conventions, when to use fable_send_message vs direct CRUD tools, and
admin key requirements.
All tool docstrings expanded with full argument descriptions, valid
values, and return shape notes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The briefing was formatting event start_dt directly in UTC instead of
converting to the user's local timezone. Also, the day_start/day_end
query window was naive (UTC), so events at the edges of the user's day
could be missed or included incorrectly.
Now reads user_timezone setting, uses it for today's date boundary and
for converting each event's start_dt before formatting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marked encodes " to " in text nodes, causing linkifyWikilinks to
double-escape it (& → &) so the visible link text showed "
instead of the actual character.
Decode marked's entities on the matched title/label before running
escapeHtmlAttr so the output is correct in both the href attribute
and the visible link text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
- 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>
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>
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>
- 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>
Adds docker-compose.quickstart.yml that pulls the pre-built image from
the registry so users can get started without a local build. Updates
README Quick Start to use the new file as the default path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
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>
Also comments out nvidia GPU reservation in docker-compose.yml
(no nvidia-container-toolkit on this host).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docs/api-reference.md: complete REST API endpoint reference (~60+ routes)
- docs/android-app.md: Flutter companion app stack, architecture, feature status
- docs/architecture.md: detailed file-by-file reference for all backend services and frontend components
- docs/features.md: LLM pipeline internals (intent routing, tool loop, duplicate guards, image search, research pipeline), roadmap
- docs/development.md: full migration chain (0001–0026) with naming and caveats
- README.md: link to new api-reference and android-app docs
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>
Register the Esc keydown listener in capture phase (useCapture=true) and
call stopPropagation() so App.vue's document-level handler never fires.
Without this, both handlers ran: App.vue pushed "/" and the component
pushed "/projects/:id", with non-deterministic winner. Also fixes the
blur-then-navigate issue where App.vue blurring an input caused the
component's handler to see body as the active element and navigate
immediately instead of stopping at the blur step.
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>
All code goes through dev first where CI runs on push. PR runs
(dev→main) were duplicating already-validated checks and causing
queue contention. Build job was already gated to push-only events.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
navigator.clipboard.writeText() requires a secure context (HTTPS) and
silently fails on http://. Add an execCommand fallback so the API key
copy button works on non-secure dev instances.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After creating a key, two download buttons appear:
- 'Download .env' — pre-filled FABLE_URL + FABLE_API_KEY
- 'Download Claude config' — ready-to-paste mcpServers JSON block
Also adds Future Task A (in-app install instructions) and Future Task B
(Forgejo MCP) to the implementation plan.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pyproject.toml with mcp[cli]/httpx/python-dotenv deps, entry point
fable-mcp=fable_mcp.server:main, dev deps for testing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
17-task TDD plan covering Phase 1 (API key auth, search endpoint,
conversation type wiring, Settings UI) and Phase 2 (fable-mcp Python
package with all tool modules, tests, and Claude Code registration).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Design for two sub-projects: API key auth support in fabledassistant
(bearer tokens, read/write scope, Settings UI) and a standalone Python
MCP server at fable-mcp/ exposing notes, tasks, projects, milestones,
semantic search, and chat tools to Claude Code.
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>
The watch(activeTab) only fires on changes, not the initial value.
When localStorage had settings_tab="briefing", onMounted skipped
loadBriefingTab() so the form always showed default empty values.
Follows the same pattern already used for the groups panel (line 338).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BriefingSetupWizard and SettingsView had hardcoded initial objects that
predated the new fields, causing TS2345 type errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Watch currentConversation.messages.length so the chat panel scrolls to
the bottom when user messages are appended or assistant messages land,
not only while streaming content is updating.
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>