DRY refactoring pass: shared mixins, route helpers, composables, CSS

Backend:
- models/base.py: TimestampMixin + CreatedAtMixin; applied to all 10+ models
- routes/utils.py: not_found() + parse_iso_date() helpers; used across all route files
- routes/milestones.py: _milestone_dict() helper replaces 5 repeated to_dict + progress blocks

Frontend:
- 5 new composables: useAutoSave, useEditorGuards, useTagSuggestions,
  useFloatingAssist, useListKeyboardNavigation
- ConfirmDialog.vue: reusable confirm modal replacing inline <teleport> blocks
- editor-shared.css: sidebar CSS consolidated from both editor views
- viewer-shared.css: context-bar/breadcrumb CSS consolidated from both viewer views
- NoteEditorView + TaskEditorView: ~120 lines each replaced with composable calls;
  duplicate scoped sidebar CSS removed
- NotesListView + TasksListView: inline keyboard-nav replaced with composable
- NoteViewerView + TaskViewerView: duplicate context-bar CSS removed

No behaviour changes. Net: -634 lines, +237 lines across 31 files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 15:26:34 -05:00
parent 48f070f773
commit 16ecd6bbeb
32 changed files with 563 additions and 634 deletions
+24
View File
@@ -12,6 +12,30 @@
> Include file-level details in the commit body when the change is non-trivial.
## Last Updated
2026-03-06 — DRY refactoring pass: TimestampMixin, route helpers, shared composables, ConfirmDialog, shared CSS.
**DRY refactoring pass (backend):**
- `src/fabledassistant/models/base.py` (new): `TimestampMixin` (`created_at` + `updated_at`) and `CreatedAtMixin` (`created_at` only) — SQLAlchemy 2.0 `Mapped[]` mixins. Applied to all 10+ models (`Note`, `Project`, `Milestone`, `Conversation`, `Message`, `User`, `PushSubscription`, `TaskLog`, `NoteDraft`, `NoteVersion`), removing ~60 lines of duplicated column definitions.
- `src/fabledassistant/routes/utils.py` (new): `not_found(resource)``(Response, 404)` and `parse_iso_date(value, field)``date | None | (Response, 400)`. Used across `notes.py`, `tasks.py`, `projects.py`, `milestones.py`, `chat.py`, replacing ~18 inline 404 blocks and ~4 duplicate date-parsing try/except blocks.
- `routes/milestones.py`: `_milestone_dict(m)` local async helper replaces 5 repetitions of `entry = m.to_dict(); entry.update(await get_milestone_progress(m.id))`.
**DRY refactoring pass (frontend):**
- `frontend/src/composables/useAutoSave.ts` (new): `useAutoSave(dirty, saving, saveFn, intervalMs?)` — interval-based autosave with `onMounted`/`onUnmounted` lifecycle, guards on dirty+saving state.
- `frontend/src/composables/useEditorGuards.ts` (new): `useEditorGuards(dirty, saveFn)` — registers Ctrl+S handler, `beforeunload` warning, and `onBeforeRouteLeave` confirm guard.
- `frontend/src/composables/useTagSuggestions.ts` (new): `useTagSuggestions(title, body, tags, markDirty)` — encapsulates `/api/notes/suggest-tags` call and suggestion state.
- `frontend/src/composables/useFloatingAssist.ts` (new): `useFloatingAssist(onRequest)` — floating selection-based assist button positioning.
- `frontend/src/composables/useListKeyboardNavigation.ts` (new): `useListKeyboardNavigation<T>(items, navigateTo, rowSelector?, enabled?)` — j/k/Enter navigation with focus-in-input guard and optional `enabled` ref.
- `frontend/src/components/ConfirmDialog.vue` (new): reusable confirm modal. Props: `title`, `message`, `confirmLabel` (default "Delete"), `danger` (default true). Emits `confirm`/`cancel`. Uses `<teleport to="body">` internally.
- `frontend/src/assets/editor-shared.css`: added shared sidebar CSS (`.sidebar-toggle`, `.sidebar-content`, `.sb-field`, `.sb-label`, `.sb-select`, `.sb-input`, `.sb-divider` + mobile collapse rules) used by both editor views.
- `frontend/src/assets/viewer-shared.css` (new): shared context-breadcrumb CSS (`.context-bar`, `.ctx-crumb*`) imported by `NoteViewerView` and `TaskViewerView`.
- `NoteEditorView.vue`, `TaskEditorView.vue`: replaced ~120 lines each of duplicated floating-assist, tag-suggestion, autosave, and route-guard logic with composable calls; replaced inline `<teleport>` delete modal with `<ConfirmDialog>`; removed ~80 lines of duplicated scoped sidebar CSS.
- `NotesListView.vue`, `TasksListView.vue`: replaced inline `activeIndex`/`onKeydown`/`scrollActiveIntoView` keyboard-nav (~25 lines each) with `useListKeyboardNavigation`. TasksListView passes `isFlat` computed ref as `enabled` guard.
- `NoteViewerView.vue`, `TaskViewerView.vue`: removed ~40 lines of duplicate `.context-bar`/`.ctx-crumb*` scoped CSS; added `<style src="@/assets/viewer-shared.css" />`.
- No behaviour changes. Net: 634 lines, +237 lines across 31 files.
---
## Previous Session
2026-03-06 — Project-aware writing assist, link suggestions, project-scoped RAG, semantic note search tool, SSE done-event race fix, RAG snippet increase, sidebar include full body, Enter-to-submit assist.
**SSE done-event race fix:** `routes/notes.py` and `routes/chat.py` — The SSE stream generator had a race condition where `yield buf.format_sse(event)` hands control back to the event loop, allowing the generation task to set `buf.state = COMPLETED` and append the `done` event between the last yield and the state check. The loop would then break without delivering the `done` event (containing `full_text`). Fixed by adding a final drain loop after the `while` exits in both stream generators. Added INFO-level logging to `run_assist_generation` showing `input_chars`, `output_chars`, event count, and done-event index.