Files
FabledScribe/src/fabledassistant/services/note_versions.py
T
bvandeusen 6d593a0ef6 Move history to sidebar, simplify task assist, add content deduplication
- VersionHistorySection.vue: new sidebar component — collapsible timestamp list,
  click any snapshot to see inline diff + Restore/Back; replaces the modal
- NoteEditorView: remove History toolbar button + HistoryPanel modal, add
  VersionHistorySection at bottom of sidebar
- TaskEditorView: remove floating overlay assist panel + toggle button; add
  Writing Assistant section in sidebar matching note editor (scope selector,
  instruction textarea, generate/proofread/accept/reject); main area now shows
  streaming preview and DiffView like note editor; add VersionHistorySection
- note_versions.py: add content deduplication before time-gap check — identical
  body + title + tags skips snapshot regardless of interval (git semantics)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 16:56:30 -04:00

86 lines
3.2 KiB
Python

from datetime import datetime, timezone
from sqlalchemy import select, text
from fabledassistant.models import async_session
from fabledassistant.models.note_version import NoteVersion
# Maximum snapshots retained per note.
MAX_VERSIONS = 50
# Minimum seconds between snapshots. Prevents autosave from consuming all
# slots — with autosave at 60 s intervals, a 5-minute gate means at most
# one snapshot per editing session segment rather than one per save tick.
MIN_VERSION_INTERVAL_SECONDS = 300
async def create_version(
user_id: int, note_id: int, body: str, title: str, tags: list[str] | None = None
) -> NoteVersion | None:
async with async_session() as session:
# Fetch the most recent snapshot once for both checks.
recent = await session.execute(
select(NoteVersion)
.where(NoteVersion.note_id == note_id, NoteVersion.user_id == user_id)
.order_by(NoteVersion.created_at.desc())
.limit(1)
)
last = recent.scalars().first()
if last is not None:
# Skip if content is identical — no change, no snapshot (git semantics).
if (
last.body == body
and last.title == title
and sorted(last.tags or []) == sorted(tags or [])
):
return None
# Skip if within the minimum interval to prevent rapid-fire autosave snapshots.
last_at = last.created_at
if last_at.tzinfo is None:
last_at = last_at.replace(tzinfo=timezone.utc)
age = (datetime.now(timezone.utc) - last_at).total_seconds()
if age < MIN_VERSION_INTERVAL_SECONDS:
return None
version = NoteVersion(note_id=note_id, user_id=user_id, body=body, title=title, tags=tags or [])
session.add(version)
await session.commit()
await session.refresh(version)
# Prune versions beyond MAX_VERSIONS
await session.execute(
text("""
DELETE FROM note_versions
WHERE id IN (
SELECT id FROM note_versions
WHERE note_id = :note_id AND user_id = :user_id
ORDER BY created_at DESC
OFFSET :max_versions
)
""").bindparams(note_id=note_id, user_id=user_id, max_versions=MAX_VERSIONS)
)
await session.commit()
return version
async def list_versions(user_id: int, note_id: int) -> list[NoteVersion]:
async with async_session() as session:
result = await session.execute(
select(NoteVersion)
.where(NoteVersion.note_id == note_id, NoteVersion.user_id == user_id)
.order_by(NoteVersion.created_at.desc())
)
return list(result.scalars().all())
async def get_version(user_id: int, note_id: int, version_id: int) -> NoteVersion | None:
async with async_session() as session:
result = await session.execute(
select(NoteVersion).where(
NoteVersion.id == version_id,
NoteVersion.note_id == note_id,
NoteVersion.user_id == user_id,
)
)
return result.scalars().first()