# Fabled Assistant - Project Context > **Purpose:** This file is the canonical reference for re-initializing Claude Code > context on this project. **Update this file after every change session.** ## Session Checklist > **IMPORTANT:** At the end of every change session, you MUST: > 1. **Update this file** (`summary.md`) to reflect all architectural, data model, > API, file structure, and roadmap changes made during the session. > 2. **Commit all changes** with a descriptive commit message summarizing what was > done (e.g., "Merge tasks into notes: single table with task attributes"). > Include file-level details in the commit body when the change is non-trivial. ## Last Updated 2026-03-16 — Fuzzy project resolution, accidental project creation guard, task embedding, briefing CalDAV fix, notes count query fix. --- **2026-03-16 — Fuzzy project resolution + create_project guard:** - `services/tools.py` — `_resolve_project` rewritten with 4-step lookup: (1) exact DB match, (2) `query in title` substring, (3) `title in query` reverse substring, (4) SequenceMatcher ≥ 0.55 fallback. Partial names like "Famous Supply" now reliably resolve to "Famous Supply Co." without the model creating a duplicate project. - `services/tools.py` — `create_project` handler: similar-project checks (threshold 0.55) now run **before** the `confirmed` gate, so `confirmed=true` cannot bypass them. All call sites that duplicated the old inline substring logic replaced with `_resolve_project`. - `services/tools.py` — Error messages on project-not-found no longer say "or create_project to create one"; they direct the model to `list_projects` instead. Tool description explicitly says never call `create_project` in response to a lookup failure. - `routes/tasks.py` — Fire-and-forget `upsert_note_embedding` added to create and update task routes (was missing; tasks created via REST API weren't getting embeddings). - `services/briefing_pipeline.py` — `await is_caldav_configured()` (was called without await — was a latent bug since `is_caldav_configured` became async). - `services/notes.py` — Removed erroneous duplicate `count_query.where(Note.parent_id == parent_id)` that was applied inside the `no_project` branch. --- **CI release process (2026-03-12):** - `.forgejo/workflows/ci.yml`: removed `main` from `branches` push trigger. CI now fires only on `dev` pushes, `v*` tags, and pull requests. Main branch merges no longer trigger a run — the PR check covers pre-merge safety. Production release process: create a release via Forgejo UI on `main` with a `v*` tag → tag push event fires CI → build job pushes `:latest` + `:` Docker images to registry. Branch protection is not an obstacle because the UI release creates the tag through the API. --- **Daily Briefing — full feature (2026-03-11):** Backend: - Migration `0026_add_briefing_tables.py`: `rss_feeds` (url, name, user_id), `rss_items` (feed_id FK, guid, title, url, summary, pub_date), `weather_cache` (user_id UNIQUE, lat, lon, location_name, forecast_json, fetched_at); added `conversation_type` TEXT and `briefing_date` DATE to `conversations`. - `models/rss_feed.py`, `models/rss_item.py`, `models/weather_cache.py`: SQLAlchemy models with `to_dict()`. - `services/weather.py`: geocoding via Nominatim (Open-Meteo), forecast fetch via Open-Meteo API, per-user DB cache with change detection. `get_weather(user_id)` returns structured dict with location, current, and 5-day forecast. - `services/rss.py`: feedparser-based fetch, per-feed DB cache with prune-to-100. `get_rss_items(user_id, limit)` returns recent items across all user feeds. - `services/briefing_profile.py`: service that reads user briefing config (CalDAV, weather, RSS, office days, tasks) and composes a profile note used as context for generation. - `services/briefing_pipeline.py`: two-lane parallel gather (async `asyncio.gather` over weather, RSS, tasks, calendar) → LLM synthesis. Streams result into a `GenerationBuffer` using the same SSE infrastructure as chat. - `routes/briefing.py`: RSS CRUD at `/api/briefing/feeds`, weather config at `/api/briefing/weather`, briefing config at `/api/briefing/config`, `POST /api/briefing/trigger` to manually fire a slot. Registered as `briefing_bp` in `app.py`. - `routes/chat.py`: `GET /api/briefing/conversations/today` — creates the day's briefing conversation if absent (type=`briefing`, `briefing_date=today`); `GET /api/briefing/conversations` — lists all past briefing conversations. - `services/generation_task.py` / `routes/chat.py`: `conversation_type` filter support so briefing conversations are excluded from the main chat list and vice versa. - APScheduler integration: `services/scheduler.py` runs briefing slots (morning, midday, evening) on schedule with catch-up logic — if a slot was missed while the server was down, it runs immediately on next startup. - LLM tools: `get_weather` and `get_rss_items` added to `_CORE_TOOLS` in `services/tools.py`. Frontend: - `frontend/src/views/BriefingView.vue` (new): primary briefing page. Soft chat-style layout. Top section shows the digest card (expandable) followed by the conversation thread. Reply bar at bottom — sends via standard chat endpoints and streams via SSE. Manual "Refresh" button triggers a new briefing generation. Conversation history dropdown (past briefing dates) in header. - `frontend/src/components/BriefingSetupWizard.vue` (new): 4-step setup wizard shown on first visit when briefing is not configured (welcome → location → office days → RSS feeds). Completes by enabling briefing in settings. - `frontend/src/views/SettingsView.vue`: Briefing tab added (`briefing` in `VALID_TABS`): enable toggle, location geocoding input (live preview), office days checkboxes, time slot toggles (morning/midday/evening), RSS feed CRUD (add URL + name, list, delete), push notification toggle for briefing completion. - `frontend/src/router/index.ts`: `/briefing` route added → `BriefingView`. - `frontend/src/components/AppHeader.vue`: "Briefing" nav link added. --- **Previous session (2026-03-11):** **Version footer + CalVer tracking:** - `4636c9a` Add CalVer build-time version tracking: `Dockerfile` injects `BUILD_VERSION` ARG as `VITE_APP_VERSION` env var during frontend build. Frontend reads `import.meta.env.VITE_APP_VERSION` and displays it in the Settings footer. Docker image tags include `:` on release builds. - `2cb4e6d` Version footer, task history UI, note version retention policy: Settings page shows `v` in footer. Task history now displays inline in a sidebar within the task editor (previously required navigation). Note version retention policy configurable (max versions per note, default 20 — enforced at write time in `services/note_versions.py`). **Content deduplication + history sidebar:** - `6d593a0` Move history to sidebar, simplify task assist, add content deduplication: `HistoryPanel` moved to a collapsible right sidebar in `NoteEditorView` (previously a modal overlay). Writing assist panel simplified — removed multi-step UX, direct instruction input. `build_context()` in `services/llm.py` deduplicates injected content (RAG-found notes, sidebar includes, context notes) by ID so the same note cannot appear in multiple context blocks. **ShareDialog CSS fix:** - `a63f067` Fix ShareDialog transparent background — replaced undefined CSS custom property references with explicit values. --- **Previous session (2026-03-11 — Multi-user & backup):** Multi-user sharing & collaboration, in-app notifications, groups management, backup rewrite. **Multi-user sharing & collaboration:** - `alembic/versions/0025_add_sharing_and_notifications.py` (new): creates `groups`, `group_memberships`, `project_shares`, `note_shares`, `notifications` tables. Partial unique indexes via raw SQL. CHECK constraint enforces exclusive user/group target on share rows. - `src/fabledassistant/models/group.py` (new): `Group` + `GroupMembership` SQLAlchemy models. - `src/fabledassistant/models/share.py` (new): `ProjectShare` + `NoteShare` models with `shared_with_user_id`, `shared_with_group_id`, `permission`, `invited_by` columns. - `src/fabledassistant/models/notification.py` (new): `Notification` model with `type`, `payload` (JSONB), `read_at`. - `src/fabledassistant/models/__init__.py`: exports all three new model groups. - `src/fabledassistant/services/access.py` (new): `PERMISSION_RANK` dict (`viewer=1 editor=2 admin=3 owner=4`). `get_project_permission(uid, project_id)` and `get_note_permission(uid, note_id)` — check ownership, direct share, group-based share, and note→project inheritance; return highest permission string or `None`. Helper predicates: `can_read_*`, `can_write_*`, `can_admin_*`. - `src/fabledassistant/services/groups.py` (new): `create_group`, `list_groups`, `update_group`, `delete_group`, `add_member`, `update_member_role`, `remove_member`. - `src/fabledassistant/routes/groups.py` (new): CRUD at `/api/groups` and `/api/groups/:id`; member management at `/api/groups/:id/members/:uid`. - `src/fabledassistant/services/sharing.py` (new): `share_project/note`, `update_project/note_share`, `remove_project/note_share`, `list_project/note_shares`, `list_shared_with_me`. - `src/fabledassistant/routes/shares.py` (new): project shares at `/api/projects/:id/shares`, note shares at `/api/notes/:id/shares`, `GET /api/shared-with-me`. - `src/fabledassistant/services/notifications.py`: extended with `create_in_app_notification`, `notify_project_shared`, `notify_note_shared`, `notify_group_added`, `list_in_app_notifications`, `unread_notification_count`, `mark_notification_read`, `mark_all_notifications_read`. - `src/fabledassistant/routes/in_app_notifications.py` (new): `GET /api/notifications`, `GET /api/notifications/count`, `POST /api/notifications/:id/read`, `POST /api/notifications/read-all`. - `src/fabledassistant/routes/users.py` (new): `GET /api/users/search?q=` — username/email prefix search, min 2 chars, excludes self. - `src/fabledassistant/services/projects.py`: added `get_project_for_user(accessing_uid, project_id)` → `(Project, perm) | None` and `list_projects_for_user(uid)` (owned + shared). - `src/fabledassistant/services/notes.py`: added `get_note_for_user(accessing_uid, note_id)` → `(Note, perm) | None`. - `src/fabledassistant/routes/projects.py`: `list_projects_route` uses `list_projects_for_user`; `get_project_route` uses `get_project_for_user` and adds `permission` field; `get_project_notes_route` access-checks via `get_project_for_user`. - `src/fabledassistant/routes/notes.py`, `routes/tasks.py`: `get_note_route`/`get_task_route` use `get_note_for_user` and add `permission` field. - `src/fabledassistant/app.py`: registers `groups_bp`, `shares_bp`, `notifications_bp`, `users_bp`. **Frontend — sharing & notifications:** - `frontend/src/api/client.ts`: added `ShareEntry`, `GroupEntry`, `GroupMember`, `NotificationEntry`, `UserSearchResult` TypeScript interfaces and API helper functions for all new endpoints. - `frontend/src/stores/notifications.ts` (new): Pinia store — `count`, `items`, `fetchCount`, `fetchAll`, `markRead`, `markAll`. - `frontend/src/components/NotificationBell.vue` (new): bell SVG icon with unread count badge, document-click-outside close, 60s polling interval, toggles `NotificationsPanel`. - `frontend/src/components/NotificationsPanel.vue` (new): absolute-positioned dropdown listing unread notifications by type icon, bold inviter name, relative timestamp. "Mark all read" button. Click navigates to `payload.url`. - `frontend/src/components/ShareDialog.vue` (new): `` modal. Props: `resourceType: 'project' | 'note'`, `resourceId`, `resourceTitle`. User tab: debounced user search dropdown + permission select + Add button. Group tab: group picker + permission select. Current shares list with inline permission change and remove. - `frontend/src/views/SharedWithMeView.vue` (new): `/shared` route. Grid of shared projects (color bar, title, owner, permission badge). List of shared notes/tasks (icon, title, owner, permission badge). - `frontend/src/components/AppHeader.vue`: `NotificationBell` added to `nav-right` before the `?` button; "Shared" nav link added to desktop center nav and mobile menu. - `frontend/src/views/ProjectView.vue`: "Share" button + `showShare` ref + `` at template bottom. - `frontend/src/views/NoteViewerView.vue`: "Share" button in toolbar + `` after layout root. - `frontend/src/views/TaskViewerView.vue`: "Share" button in toolbar + `` after layout root. - `frontend/src/views/SettingsView.vue`: "groups" added to `VALID_TABS`; Groups admin tab added to sidebar. Groups panel: create group form, groups list with member count, per-group "Manage" panel with user search + role select + add, member list with per-member remove. - `frontend/src/router/index.ts`: `/shared` → `SharedWithMeView` route added. **Backup rewrite:** - `src/fabledassistant/services/backup.py`: full rewrite to version 2. `export_full_backup()` and `export_user_backup()` now include all tables: projects, milestones, task logs, note drafts (AI draft model), note versions, push subscriptions. `restore_full_backup()` dispatches to `_restore_v1()` or `_restore_v2()` by version key. `_restore_v2()` builds ID maps (`user_id_map`, `project_id_map`, `milestone_id_map`, `note_id_map`); two-pass note restore patches `parent_id` after all notes are inserted. --- **Previous session (2026-03-10):** Feature batch: workspace view, graph view, settings tabs, message queue persistence, ToolCallCard direct-API confirm, duplicate detection scoping, push notification fixes. **UI polish pass — design language:** - `frontend/src/assets/theme.css`: added Fraunces Google Font import; `h1/h2/h3` use `font-family: 'Fraunces', Georgia, serif`; `--radius-md` 8→12px, `--radius-lg` 12→18px; added thin indigo-tinted scrollbars (4px); added bubble-specific CSS variables (`--color-bubble-user-*`, `--color-bubble-asst-shadow`); dark palette shifted from generic navy (#1a1a2e family) to slate-indigo (#111113 family). - `frontend/src/components/AppLogo.vue`: logo book fill changed from `var(--color-text)` (black in light mode) to `var(--color-primary)` (indigo). Stroke updated to `color-mix(in srgb, var(--color-primary) 70%, transparent)`. - `frontend/src/components/AppHeader.vue`: removed `border-bottom` from `.app-header`; nav brand uses Fraunces font; active nav link uses `inset 0 -2px 0 var(--color-primary)` underline instead of background fill. - `frontend/src/components/ChatMessage.vue`: user bubble ghosted (transparent bg, thin border, muted text); assistant bubble elevated with left `2px solid var(--color-primary)` accent + shadow; assistant role label in Fraunces italic indigo; thinking block uses left-accent instead of full border. - `frontend/src/views/ChatView.vue`: removed sidebar/header/context-sidebar borders; streaming bubble matches assistant style; active conversation item uses 15% indigo tint; send button and queued bubbles use indigo gradient + glow. - `frontend/src/components/NoteCard.vue`, `TaskCard.vue`: removed `border: 1px solid`; replaced with whisper shadow (`0 1px 4px + 0 0 0 1px indigo tint`); elevated shadow on hover. - `frontend/src/views/HomeView.vue`: `dashboard-response` uses left indigo accent + elevation shadow (no border); `dashboard-response-actions` border-top removed; quick-action-chip hover adds indigo bg tint; project-mini-card uses whisper shadow; ms-fill progress bars use `var(--color-primary)`; `.btn-cta` and `.btn-open-chat.prominent` use indigo gradient + glow; `.empty-state::before` adds decorative `✦` marker. - `frontend/src/views/NotesListView.vue`: `.btn-new` and `.btn-cta` use indigo gradient + glow on hover. **Design intent:** "Illuminated Transcript" — LLM responses are primary artifacts (elevated, card-like, indigo accent left-border), user prompts are subordinate margin notes (ghosted, recessed). Buttons with the gradient carry intentional weight. Borders replaced by shadow depth throughout. --- **Previous session (2026-03-10):** Feature batch: workspace view, graph view, settings tabs, message queue persistence, ToolCallCard direct-API confirm, duplicate detection scoping, push notification fixes. **Project Workspace (`/workspace/:projectId`):** - `frontend/src/views/WorkspaceView.vue` (new): 3-panel layout (tasks / chat / notes) with CSS grid collapse. Panel toggles in ws-header. Conversation persisted to `localStorage` as `workspace_conv_{projectId}`; deleted on unmount only if brand-new and empty. `sendMessage()` passes `projectId` as both `ragProjectId` and `workspaceProjectId`; `think: true` always enabled. SSE watcher auto-loads note on `create_note`/`update_note`, refreshes task panel on `create_task`/`update_task`. - `frontend/src/components/WorkspaceTaskPanel.vue` (new): tasks grouped by milestone (No Milestone first), collapsible groups, task detail slide-over with status cycle + milestone `