- scrollToTop() now respects user scroll position (100px threshold)
- resetScrollLock() called on new messages
- SESSION_TIMEOUT_SECONDS 90 -> 3600 (1 hour) to prevent mid-use expiry
- wrap 10 unprotected authFetch calls in try/catch to kill unhandled rejections
- auto_detect_facts() scans chat turns for factual content (IPs, paths,
services, config changes, hardware refs) using pattern matching
- check_fact_conflicts() cross-references detected facts against stored
FTS5 memories — when a contradiction exists (same topic, diff value)
the system surfaces a rag_update_suggestion in the done SSE payload
- Frontend shows a floating notification banner comparing old vs new
fact with Update/Dismiss buttons
- confirm_fact_update() replaces the memory + re-embeds/re-indexes
the Qdraft entry on user confirmation
- Silent auto-ingest (memories + Qdrant) when no conflict exists
- Frontend: msg-toolbar opacity 0→0.35 for visibility
- Adds .msg-toolbar to each assistant reply after streaming completes
- Copy: copies full response text to clipboard
- Print: opens print-friendly window with formatted response
- Save: downloads response as .md file
- Rate: thumbs up/down toggle (local only, no backend)
- Toolbar fades in on message hover
- Also wired into search-result replies and loaded history
- Paperclip icon left of text input, file preview pill (image thumb or file icon)
- Conversation list shows attachment icon right of trash, opens gallery overlay
- Gallery overlay: scrollable, close (X), delete attachment per item
- DELETE /api/upload/{id} removes from SQLite + Qdrant
- PATCH /api/upload/{id}/link ties upload to conversation
- GET /api/upload/by-conversation/{id} lists attachments
- Chat accepts upload_context_id, injects [ATTACHED DOCUMENT] into system prompt
- Conversation list includes attachment_count field
- 8 new tests: upload context injection, delete, link, by-conversation, image type, attachment count
Still missing (P3): drag-and-drop upload, global attachments page, file download, batch upload
Replace the hover-reveal × on the right with an always-visible 🗑 icon
positioned to the left of the conversation title. Clicking it triggers
the existing deleteConversation() which shows a confirm dialog and
enforces admin-only access.