Commit Graph
59 Commits
Author SHA1 Message Date
bvandeusen 1ec44071d2 feat(ui): a note's check is editable, dated and sweepable (#3167, milestone 317 step 4)
CI & Build / TypeScript typecheck (push) Successful in 39s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 1m12s
CI & Build / integration (push) Successful in 1m41s
CI & Build / Build & push image (push) Successful in 1m1s
Rule 27: no UI, no ship. Three surfaces.

THE EDITOR ASKS, but only where the answer can be saved: the fields appear
for a plain note and not for a task or a snippet, matching the service gate
from step 2 so the form never offers a write the save would reject. The
labels are phrased as the QUESTION rather than the field name — "how would
someone check this is still true?" and, underneath, "could this become false
without anyone editing it?". "Verify with" gets filled in on every note; the
question gets filled in on the few that can go stale. `expires_when` appears
only once a check exists, and asks for a state rather than a date in the
placeholder itself.

THE NOTE SHOWS ITS AGE beside the field — "checked 2026-08-28" or "never
checked", italic, and nothing at all when no check exists. No red/amber ramp,
matching RuleSweepPane: a colour scale would restate the sweep's ordering and
force an invented staleness threshold. "Never" is marked because it is
categorically different from a date, not a worse one.

THE SWEEP is a pane in the Knowledge view, not beside the rules sweep —
operator's call, taken over a unified "everything due" surface and over a
second pane under /rules. Notes stay where notes live. The cost, accepted
knowingly: no single screen shows every unconfirmed record. It REPLACES the
feed rather than filtering it, because a facet answers "show me this kind"
and this answers "show me what nobody has confirmed" — a question the type
chips cannot narrow without under-reporting.

Two REST routes for it, since step 3 built only the service and the MCP door.

Along the way: NoteEditorView spelled its write payload out at three call
sites (save, create, auto-save), so every new field had to be added three
times — which is how one of them ends up not carrying it. Now one `payload()`
and one `snapshot()`.

Known and filed, not fixed: NoteSweepPane copies ~12 scoped CSS rules from
RuleSweepPane (#3207). The clean extraction needs prefixed names, because
`.age`, `.row-title`, `.lede` and `.actions` all exist scoped in other
components and an unscoped global would leak into them — which means editing
the shipped rules sweep, blind, inside a step whose acceptance is the
operator looking at a different surface.
2026-08-28 22:04:00 -04:00
bvandeusenandClaude Opus 5 69d93898d9 fix(tasks): a task's kind is correctable — the Kind select stops lying (#3129)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 48s
CI & Build / Build & push image (push) Skipped
`kind` was accepted at CREATE on both doors and dropped at UPDATE on both:
update_task had no such parameter, and the REST PATCH allow-list never read
the field. So a task filed under the wrong kind could never be corrected.

The frontend made it worse by looking like it worked. TaskEditorView binds a
Kind select, marks the form dirty, and HAS ALWAYS SENT `kind` in the update
payload — the store even types it. The route ignored it, returned 200, the
view optimistically updated, the toast said "Task saved", and the old value
came back on reload. Silent success, same class as #2709.

Found by trying to re-file #3126 as a spike after deploying 0091. It could
not be done; the task had to be recreated as #3128 and the original
cancelled.

One seam, not two doors. `minted_kind()` lives in services/notes.py because
the REST route cannot import an MCP tool module and a second spelling of the
list is how the doors would come to disagree. Both create and update route
through it, so a bogus kind is now a readable error rather than a
CheckViolationError surfacing as a 500.

TaskKind joins TaskStatus and TaskPriority as a real enum, and update_note
validates task_kind exactly as it already validated those two — the field
had been reaching setattr through the hasattr guard with no validation at
all, unnoticed only because no door ever offered it.

The `-> plan` question #3129 raised is answered in code rather than left
implicit: MINTABLE_KINDS is work/issue/spike, deliberately NARROWER than the
column's CHECK. `plan` stays a valid stored value because historical
plan-tasks carry it and must stay writable; it is simply not a value any
door hands out, and the refusal names start_planning because a caller
reaching for it wants a plan. The whitelist and the policy answer different
questions and are not the same list.

Every new test reads the value BACK. One that only asserted the call
succeeded would have passed against the broken code — the route returned 200
while dropping the field, which is how this survived long enough to be found
by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 15:55:04 -04:00
bvandeusenandClaude Opus 5 c83bedf3be feat(rules): the check is editable, visible, and sweepable in the UI (#3098, milestone 312 step 4)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m13s
CI & Build / Build & push image (push) Successful in 37s
Rule 27 — the milestone was backend-only until this. Four surfaces:

RULE EDITOR — verify_with and expires_when under a legend that asks the
actual question ("Can this rule go stale?") and says empty is the normal
answer, because most rules are decisions and a form that implies a missing
field would get them filled in out of tidiness. When the SAVED rule carries
a check, the stamp shows with Still true / No longer true beside it. The
stamp reads the stored value, not the draft: an unsaved edit to the textarea
has not been run against anything.

SWEEP PANE — its own surface, not a filter on the rule list. That list can
only ever show one topic of one rulebook, and a rule that has gone false
belongs to no one rulebook; filtering it would under-report, which is the
failure this whole surface exists to catch. Reached from the rulebook list,
below the rulebooks, because that is where you go to look at rules.

RULE ROWS — a chip only on rules carrying a check, so its presence is the
signal. PROJECT RULES TAB — the check shows beside `why` when a rule has
one, read-only: that tab is the project's view of what binds it.

NO AGE-GRADED COLOUR anywhere, deliberately. The sweep is already ordered by
urgency, so a red/amber ramp would restate the ordering AND require an
invented "stale after N days" threshold — a magic number nobody could defend
and the first thing to go out of date. --fs-overdue is error red and reserved
for a broken promise like a missed due date; a verification age is not one,
and colouring it that way makes a rule someone just wrote look broken. Only
"never" is marked, because it is categorically different from a date rather
than a worse one — and it is marked by weight, not hue.

An empty sweep says "Nothing to check", not nothing: good news must not read
as a broken page.

Two chips (tier, then verification) turned out byte-identical, so .rule-chip
moves to rules-shared.css and snippet #2906 is updated to match rather than
left describing a file that has moved on. Its header comment counted the
panes it served; that count went stale the moment a fourth arrived, so it no
longer counts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 11:53:35 -04:00
bvandeusenandClaude Opus 5 682bea5257 fix(rules): the third row literal — fetchRules builds a list row too (#3029)
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 20s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Python tests (push) Successful in 1m3s
CI & Build / Build & push image (push) Successful in 40s
vue-tsc caught what I missed: there were THREE places hand-building a rule
list row, not two. fetchRules mapped full rules down to the same four fields
in a spot far from the other two, so consolidating the pair I could see left
this one behind — which is precisely how the server side ended up with three
divergent trim dicts in the first place.

All three now go through toHeader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 14:26:20 -04:00
bvandeusenandClaude Opus 5 8b60d552d2 feat(rules): the rule editor asks when it applies, and the list shows its age (#3029, milestone 307 step 3, UI)
CI & Build / Python lint (push) Successful in 6s
CI & Build / integration (push) Successful in 38s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Failing after 27s
CI & Build / Python tests (push) Successful in 1m27s
CI & Build / Build & push image (push) Skipped
Rule 27 — the schema and both doors shipped with no human surface, so step 3
was not shippable until this.

RuleEditorSlideOver gains the trigger, the tier, the areas, and a read-only
view of the rule's edges. The tier is a radio pair carrying the test itself
rather than a bare toggle: can you name the trigger WITHOUT naming a system,
an artifact type or a moment? If the honest answer is "whenever you are
working", it is always on. It also says why conditional is not a demotion —
it costs nothing when irrelevant, which is what lets a rule be as long as it
needs to be. The relations block states the rule the whole milestone turns on:
rules that FAIL TOGETHER are linked, never merged.

RuleListPane shows the trigger and the LAST-CHANGED DATE on every row, and
marks conditional only — always_on is the default and badging every row would
say nothing. The date is the cheap triage the FabledCurator case wanted: a
rule whose age predates the capability it duplicates is visible at a glance
instead of needing a get_rule to find out.

ProjectRulesTab's inline create form gains the same two fields, because a
project rule bloats exactly the way a family one does — FabledCurator has 23
of them.

Two type fixes the new shapes forced, both worth keeping:
- toHeader() in the store: a list row is the server's rule_brief, so patching
  a list locally has to mirror every field it carries or the two disagree.
  There were two hand-built four-field literals doing that job.
- ApplicableRules.rules / .project_rules are now described AS RuleHeader
  rather than as two more hand-written shapes — the same builder produces
  them, so the same type should describe them.

groupByRulebookAndTopic skips a null-topic rule rather than widening
TopicGroup to accept one: a rule carries topic_id XOR project_id, so a null
topic in that list means something is wrong upstream, and a widened type would
hide it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 14:23:40 -04:00
bvandeusenandClaude Opus 5 c58529718b feat(systems): the catalog reaches the moment a name is minted, and gets a face (#3028, milestone 307 step 2)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 48s
CI & Build / integration (push) Successful in 38s
CI & Build / Python tests (push) Failing after 57s
CI & Build / Build & push image (push) Skipped
Step 1 found the reason the standard names never held, and it is sharper than
"prose doesn't fire": the list WAS real and it WAS seeded — but only on the
inception path, for a project with zero Systems. Ad-hoc create_system never
consulted it, which is how Forge minted "CI and Release" and Portal minted
"CI & release" after the constant already existed. This wires the vocabulary to
the moment that mints a name.

- services/systems.assess_system_name: the local duplicate gate AND the
  catalog lookup, in ONE service function both doors call. The gate lived only
  in the MCP tool, which is exactly how the web UI shipped without a check the
  agent surface enforced (#2482). REST now answers 409 with the System that
  already covers the area.
- An `exact` catalog hit is APPLIED (mechanical — the names differ only in
  spelling). An `overlap` is only OFFERED, on both doors: applying a judgment
  call silently is how a cross-project rule surfaces in the wrong project.
- canonical_systems.best_overlap is the ONE scorer behind the create-time
  offer and the review sweep, so the two surfaces can never name different
  areas for one System. It also takes the catalog the caller already holds,
  so the review is not an N+1.

UI (folded in from step 1 — rule 27, that step shipped with no human surface):
- SystemsSection: a Shared area picker on create and edit, the area on each
  card, and a collapsed review of proposals that appears only when there is
  something to decide. `exact` and `overlap` never share a style — one is
  mechanical, the other is the reviewer's judgment, and presenting them alike
  is how a wrong mapping gets waved through.
- Settings → Admin → Areas: the catalog itself, showing each entry's slug,
  because the slug is what decides whether two names are the same area and a
  rename moves it.
- A picker rather than a live matcher: reproducing the slug rule in TypeScript
  would give this feature two matchers to keep in step — the exact drift the
  catalog exists to end. The server stays authoritative.

tests/helpers.fake_system gains canonical_id=None: an unnamed attribute is an
auto-MagicMock and therefore truthy, which is the trap that helper exists for
(note 2109) and a nullable FK walks straight into it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 12:58:30 -04:00
bvandeusenandClaude Opus 4.8 12f71fabdf refactor(scribe): remove calendar + entity surfaces from web UI (frontend)
CI & Build / integration (push) Successful in 28s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 46s
CI & Build / Build & push image (push) Successful in 44s
Frontend half of the narrowing (milestone #194); matches backend b49efdc.

- delete CalendarView, EventSlideOver, WeatherCard (orphan)
- drop /calendar route + nav link + the g→l keyboard shortcut
- strip the calendar-events API client + event/metadata bits from note
  types and the notes store
- KnowledgeView: remove People/Places/Lists tabs, entity cards, create
  buttons and the upcoming-events widget; keep notes/tasks/plans/processes
  + the overdue-task badge
- NoteEditorView: remove person/place/list forms + list-builder + entity
  metadata; keep note + process editors (type select = Note/Process)
- DashboardView: drop the "Upcoming · 7 days" events rail card
- SettingsView: remove the CalDAV integration card + save/test (its
  endpoints were deleted backend-side)
- prune the now-dead entity/event CSS

RecurrenceEditor + task recurrence rules are kept (task machinery, not
calendar). Verified by a full dangler sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPtbSzA4JLMAKgFZ8VTg7Q
2026-07-19 16:10:24 -04:00
bvandeusenandClaude Opus 4.8 94d32c524a feat(issues): S4b frontend — open-issues lists + issue plumbing
CI & Build / Python tests (push) Successful in 1m36s
CI & Build / Python lint (push) Successful in 5s
CI & Build / TypeScript typecheck (push) Successful in 40s
CI & Build / Build & push image (push) Successful in 59s
- types/note.ts: Note gains systems? + arose_from_id?; TaskKind includes 'issue'.
- stores/tasks.ts: create/update accept IssueFields (kind/system_ids/arose_from_id).
- api/systems.ts: getProjectIssues + TaskLike.
- DashboardView.vue: 'Open issues' rail section from dashboard.open_issues
  (links to /tasks/<id>, project + status).
- SystemsSection.vue (project Systems tab): 'Open issues' list via getProjectIssues,
  with system chips, links to /tasks/<id>. Both reuse existing CSS tokens.

Issue-editor controls (kind selector / system multi-select / arose-from picker
in WorkspaceTaskPanel) are the remaining S4b piece. NEEDS operator browser
verification (CI typechecks only).

Refs plan 825 (S4b frontend — lists).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:50:41 -04:00
bvandeusenandClaude Opus 4.8 4da29562bd feat(issues): S4a UI — Systems management section in project view
CI & Build / Python tests (push) Successful in 51s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Failing after 23s
CI & Build / Build & push image (push) Has been skipped
Frontend foundation for Issues + Systems (spec #825, S4a).

- frontend/src/api/systems.ts: typed client (System + list/create/update/delete)
  over /api/projects/<id>/systems, matching the rulebooks api style.
- frontend/src/stores/systems.ts: Pinia store keyed by project (fetch/create/
  update/archive/unarchive/delete), toast-on-error.
- frontend/src/components/SystemsSection.vue: a Systems management section —
  cards (color swatch, name, description, 'N open' issue-count badge) with
  inline create/edit, archive (hidden behind a 'show archived' toggle), and a
  delete-confirm modal. v1 quality: loading skeleton, empty state, error toasts,
  keyboard a11y, focus rings; reuses existing CSS tokens (no new colors).
- ProjectView.vue: new 'Systems' tab (between Notes and Rules), rendering
  <SystemsSection :project-id>, wired like the existing rules tab.

S4b (next) adds issue-editor controls (kind=issue/system multi-select/arose-from),
open-issues lists, and the dashboard surface. NEEDS operator browser verification
(CI typechecks but can't render).

Refs plan 825 (S4a).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:42:40 -04:00
bvandeusenandClaude Opus 4.8 64bc50c788 chore(frontend): drop dead settings toggle, types, and store list-surface
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 56s
CI & Build / Build & push image (push) Has been cancelled
Drift-audit Group 7 frontend cleanup (no behavioral change):

- SettingsView: remove the 'auto-consolidate task bodies' toggle and its
  saveAutoConsolidate handler. The auto_consolidate_tasks setting has zero
  backend readers (curator removed in Phase 8); the control did nothing.
- AppSettings type: drop the dead assistant_name / default_model hints (kept
  the open string index signature the store actually uses). Delete the fully
  orphaned types/chat.ts (zero importers).
- notes/tasks Pinia stores: remove the list/filter/sort/pagination surface
  that backed the removed /notes and /tasks list views (verified no consumer
  uses the tasks/notes arrays, refresh, or any filter/sort/pagination method).
  Kept currentNote/currentTask, loading, fetch/create/update/delete, convert,
  patchStatus, startPlanning, backlinks, tags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:47:28 -04:00
bvandeusenandClaude Opus 4.7 658348f208 feat(rules): always_on rulebook flag + Scribe-first prompt
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 46s
CI & Build / Build & push image (push) Successful in 1m1s
Adds rulebooks.always_on (migration 0058) and a new list_always_on_rules
MCP tool so a session-start eager pull can fetch standing rules without
needing an active-project notion. Updates _INSTRUCTIONS so Claude calls
the new tool at session start and codifies engineering rules in Scribe
rather than CLAUDE.md / auto-memory.

Seeds FabledSword family rulebook to always_on=true on migrate, matching
its design role as the cross-project standards rulebook.

Frontend: badge in RulebookListPane for always-on rulebooks; toggle in
RulebookDetailPane header bound to a new toggleAlwaysOn store action.

This is S1+S2 of the rules-consolidation plan (Scribe task #508). S3
(project-scoped rules) and S4 (enter_project handshake) follow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 00:56:08 -04:00
bvandeusen e5796b6f5c feat(trash): frontend trash API client + Pinia store 2026-05-29 12:07:44 -04:00
bvandeusen 311322fdc8 fix(plan): export startPlanning from tasks store (type-check) 2026-05-28 11:20:44 -04:00
bvandeusen 1d5f49fe3b feat(plan): frontend startPlanning API + store action + task_kind type 2026-05-28 10:41:24 -04:00
bvandeusen 605dd0a13a feat(rulebook): frontend API client + Pinia store 2026-05-27 21:59:23 -04:00
bvandeusenandClaude Opus 4.7 91bafb641f refactor: Phase 8 — backend deletion (chat / voice / push / journal / curator)
Mega-commit. Strips all server-side LLM machinery now that Phase 7 has
removed the corresponding UI surfaces and the MCP HTTP endpoint is the
sole assistant interface.

Deleted (services/):
  chat, generation_buffer, generation_log, generation_task, llm, tools/
  (entire package), stt, tts, voice_config, voice_library, push,
  journal_closeout, journal_pipeline, journal_prep, journal_scheduler,
  journal_search, curator, curator_scheduler, consolidation,
  tag_suggestions, research, weather, article_fetcher, pending_actions,
  moments, assist, wikipedia.

Deleted (routes/):
  chat, voice, push, journal, quick_capture, fable_mcp_dist.

Deleted (models/):
  conversation, generation_tool_log, push_subscription,
  pending_curator_action, moment, weather_cache.

Deleted (tests/):
  test_generation_log, test_journal_*, test_consolidation, test_lookup_tool,
  test_notes_consolidation_trigger, test_record_moment_guards,
  test_research_pipeline, test_tools_*, test_tool_use_fixes,
  test_voice_library, test_weather_service, test_calendar_tool_tz,
  test_wikipedia.

Deleted (top-level):
  fable-mcp/ (legacy standalone stdio package — wheel-build pipeline
  also removed from Dockerfile).

app.py:
  - blueprint registrations for the 6 deleted routes
  - startup hook trimmed: no more Ollama warmup, KV-cache priming,
    journal/curator schedulers, voice model loading
  - shutdown hook simplified
  - httpx import dropped (was for Ollama calls)

pyproject.toml:
  - removed deps: pywebpush, feedparser, html2text, trafilatura
  - removed [voice] extras entirely
  - description updated for the MCP-first architecture

Dockerfile:
  - removed faster-whisper / piper-tts install steps
  - removed bundled piper voice download stage
  - removed fable-mcp wheel build stage

Surviving-file edits:
  - services/auth.py: drop Conversation table claim on first-user setup
  - services/backup.py: drop conversation / push-subscription export+restore;
    v1/v2 restore now silently skip pre-pivot conversation data
  - services/notes.py: drop maybe_consolidate trigger on task done/cancelled;
    drop _maybe_trigger_project_summary (LLM auto-summary)
  - services/projects.py: drop generate_project_summary + backfill_project_summaries
    (both LLM-driven)
  - services/user_profile.py: drop append_observations / consolidate /
    clear_learned_data (curator-tied) and build_profile_context
    (was LLM system-prompt builder)
  - services/notifications.py: stub out _fire_push_notif (was send_push_notification)
  - services/event_scheduler.py: drop event-reminder push + chat-retention
    cleanup job; keep CalDAV pull-sync + reminders job (in-app)
  - services/diagnostics.py: _curator_busy() always False
  - routes/notes.py: drop /assist, /assist/stream, /suggest-tags endpoints
  - routes/tasks.py: drop /<id>/consolidate endpoint
  - routes/settings.py: drop /models, KV-cache-prime-on-save, journal-schedule
    timezone hook, and the SearXNG search-test endpoint; inline _is_private_url
    (was in services/llm.py)
  - routes/admin.py: drop /voice, /voice/reload endpoints
  - routes/profile.py: drop /consolidate, /observations (GET, DELETE)
  - models/__init__.py: drop the 6 dead model imports

Frontend cascade:
  - stores/push.ts: deleted entirely (no callers after Phase 7)
  - stores/settings.ts: drop checkVoiceStatus + voice-status state
  - views/SettingsView.vue: drop Locations section + journalConfig state
    (was tied to /api/journal/config); drop JournalConfig + journal/voice
    api/client imports
  - frontend/api/client.ts: orphaned voice/journal/profile-observation/
    fable-mcp-dist exports are left as dead but harmless (call them and
    they 404; type-check is clean).

Pre-existing v1 backups that contained conversations/messages still
restore — those tables are silently dropped from the import path.
Anyone pulling the new image with a populated database will need the
Phase 9 migration to drop the dead tables (coming next).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:47:18 -04:00
bvandeusenandClaude Opus 4.7 18eb1e7ab2 refactor(ui): Phase 7 — strip chat/voice/journal/workspace/home surfaces
Frontend deletion phase of the MCP-first pivot. All in-app
conversational surfaces are gone — Claude/MCP is the assistant now.

Deleted views:
  ChatView, JournalView, WorkspaceView, HomeView

Deleted components:
  ChatPanel, ChatInputBar, ChatMessage, ChatStreamingBubble,
  ToolCallCard, ToolConfirmCard, WorkspaceChatWidget

Deleted composables + store:
  useVoiceRecorder, useVoiceAudio, useStreamingTts, stores/chat

Router changes:
  - / now redirects to /knowledge (was /journal)
  - dropped /chat, /chat/:id, /journal, /workspace/:projectId
  - /tasks still redirects to / (→ /knowledge)
  - /notes still redirects to /knowledge

KnowledgeView:
  - removed ChatPanel + ChatInputBar embeds
  - removed minichat floating widget + state + handlers
  - removed Chat link from today bar
  - removed `chatStore` driven auto-refresh-on-tool-call watch

App.vue:
  - removed useChatStore + startStatusPolling/stopStatusPolling
  - removed VAD ONNX preloader (voice subsystem dead)
  - removed visibilitychange listener (only did voice status re-check)
  - removed `c` single-key shortcut (focus chat / goto chat)
  - removed `g+c` two-key sequence (goto chat)
  - removed Chat section from shortcuts overlay
  - removed `.chat-page` / `.workspace-root` CSS overflow rule

AppHeader.vue:
  - removed useChatStore + status indicator (Ollama model status)
  - removed Chat / Journal nav links (desktop + mobile)

SettingsView.vue (4598 → 4079 lines):
  - removed Voice tab entirely
  - Notifications tab: dropped Push Notifications + Chat History
    + About sections (kept Email Notifications)
  - General tab: dropped Assistant (name + model pickers) +
    Model Management sections (kept Tasks + Timezone)
  - Profile tab: dropped Journal + Observations sections
  - VALID_TABS + tab list array no longer include "voice"
  - removed `loadVoiceTab()` activation trigger

Service worker (frontend/public/sw.js):
  - dropped push and notificationclick handlers (push subsystem
    only fired on internal generation completion, which is gone)
  - kept empty fetch handler as PWA installability shell

Script-level dead code (state refs, helper functions referencing
removed APIs) remains in SettingsView and stores/push.ts and
stores/settings.ts for now — Phase 8 backend deletion will clean
those up alongside the matching backend route removals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:14:00 -04:00
bvandeusenandClaude Opus 4.7 dbd9f00061 refactor: hard-cut RSS infrastructure (scope C)
Removes the entire RSS feature surface — feeds, items, embeddings, reactions,
discussion-note flow, briefing news context, settings, env-vars, and DB
tables. Keeps the URL-generic article-reader (the read_article LLM tool)
under a clean module so the LLM can still fetch arbitrary article content
from URLs the user provides.

Backend:
- New services/article_fetcher.py — single source of trafilatura URL→text
- New services/tools/article.py — read_article tool (was nested under tools/rss)
- Delete services/rss.py, rss_classifier.py, rss_filtering.py, article_context.py
- Delete services/tools/rss.py
- Delete models/rss_feed.py (RssFeed, RssItem), models/rss_item_embedding.py
- services/embeddings.py: drop upsert/semantic_search/backfill RSS helpers
- services/llm.py: remove _build_briefing_article_context, briefing-conv branch,
  ARTICLE_DISCUSS_SEED skip-RAG branch; drop get_rss_items / add_rss_feed from
  the actions list
- services/generation_task.py: drop _maybe_save_article_discussion_note + caller
- routes/chat.py: drop /api/chat/from-article/<id> endpoint
- routes/journal.py: re-import via web.py refactor (article_fetcher path)
- services/tools/__init__.py: register `article`, drop `rss`
- services/tools/_registry.py: drop the requires=='rss' check
- app.py: drop backfill_rss_item_embeddings + backfill_rss_article_content tasks
- config.py: prose-only edit (no env var change — RSS env vars were never first-class)

Frontend:
- stores/settings.ts: drop rssEnabled
- SettingsView.vue: drop the RSS-classification mention
- api/client.ts: drop openArticleInChat (the from-article endpoint is gone)

Tests:
- Delete tests/test_rss_service.py, test_news_api.py, test_article_reading.py

Migration:
- 0042_drop_rss: DROP TABLE rss_item_embeddings, rss_item_reactions, rss_items,
  rss_feeds; DELETE settings rows for rss_enabled / briefing_*_topics

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 12:33:30 -04:00
bvandeusenandClaude Opus 4.6 3ac32dc3bc feat: add rss_enabled user setting to toggle RSS functionality
RSS is now off by default. When disabled:
- Scheduler skips RSS feed sync during compilation slot
- Briefing pipeline skips RSS item gathering
- RSS LLM tools (get_rss_items, add_rss_feed) are hidden
- API routes return empty results for feeds/news
- Frontend hides News nav link, RSS Feeds and News Preferences in settings
- Briefing view hides news sidebar section

Toggle in Settings > Briefing > RSS / News.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 13:58:50 -04:00
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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 DeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Sonnet 4.6 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
bvandeusenandClaude Opus 4.6 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
bvandeusenandClaude Opus 4.6 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
bvandeusenandClaude Opus 4.6 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
bvandeusenandClaude Opus 4.6 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