# 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-10 — CI/CD optimization: custom runner base image, explicit label naming, pip caching, Node 22. **CI/CD infrastructure overhaul:** - `infra/Dockerfile.runner-base` (new): Ubuntu 24.04 base image with Python 3.12 (native, no PPA) and Node 22 LTS pre-installed. Tagged `py3.12-node22`. Eliminates the ~3-4 min deadsnakes PPA install that ran on every test job. Build/push command in file header. - `infra/act-runner-config.yml`: added `runner.labels` section mapping `py3.12-node22` → `runner-base:py3.12-node22`. This is the authoritative label config — `GITEA_RUNNER_LABELS` env var is only honoured on first registration and was being ignored after `.runner` exists. - `infra/runner-compose.yml`: removed redundant `GITEA_RUNNER_LABELS` env var (labels now owned solely by config.yml). - `.forgejo/workflows/ci.yml`: all jobs changed from `runs-on: ubuntu-latest` → `runs-on: py3.12-node22`; removed deadsnakes Python install steps from test job; added `actions/cache@v3` for pip keyed on `hashFiles('pyproject.toml')`; bumped `node-version` 20 → 22. - `Dockerfile`: bumped frontend build stage `node:20-alpine` → `node:22-alpine`. - `.gitignore`: added `.claude/`, `settings.local.json`, `.vscodium/`. **Runner label design:** Label name (`py3.12-node22`) matches the image tag, making the dependency contract explicit in workflow files. `ubuntu-latest` alias removed — it was opaque and implied a moving target. **To activate on server (one-time):** copy `infra/act-runner-config.yml` → `/nfs/data/fabledsword/act_runner/config.yml`, delete `/data/.runner` in the runner container, restart the stack. Runner will re-register with the `py3.12-node22` label. --- **Previous session (2026-03-06):** Session invalidation on credential change (password reset + password change). **Session invalidation on credential change:** - `models/user.py`: added `session_version: Mapped[int]` column (default 1). `Integer` import added. - `alembic/versions/0024_add_session_version.py`: migration adds `session_version INTEGER NOT NULL DEFAULT 1` to `users`. - `auth.py` (`_check_auth`): after loading user, checks `session["session_version"] == user.session_version`; mismatch → `session.clear()` + 401 "Session expired. Please log in again." - `services/auth.py`: - `change_password` return type changed from `bool` to `int | None` (returns new `session_version` on success, `None` on failure). Increments `user.session_version` before commit. - `reset_password_with_token`: increments `user.session_version` before commit (invalidates all live sessions on password reset). - `routes/auth.py`: - `login`, `register`, `register_with_invite`, `oauth_callback`: now also set `session["session_version"] = user.session_version` alongside `session["user_id"]`. - `update_password`: receives `new_version` from `change_password`; updates `session["session_version"] = new_version` so the current user's own session stays valid while all other sessions are invalidated. - **Effect:** All existing sessions will require re-login once after the migration runs (existing cookies lack `session_version`, so they'll mismatch `1`). Subsequent credential changes immediately evict all other active sessions. --- **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(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 `` 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 `` delete modal with ``; 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 `