Phase 8: version history + export (trust foundation)
Version history: new document_versions table (migration 0003) holding full-body snapshots that cascade with the doc. Throttled auto-snapshots on save (>=3min apart, max 40/doc, pruned), explicit manual restore points, and a pre_restore safety copy taken before each restore so restoring is itself undoable. Endpoints under /api/docs/:id/versions, all owner-scoped. Empties and bare renames never snapshot. Export: pure-Go Tiptap-JSON -> Markdown / HTML / plain-text / docx (no cgo/pandoc, single-binary intact), CJK-safe with RFC 5987 filenames. docx is a hand-built OOXML zip. PDF is handled client-side via the browser print dialog + an @media print stylesheet so CJK renders with the reader's own fonts. Frontend: ExportMenu (downloads + Print/PDF) and HistoryPanel (snapshot list, preview, restore) wired into the title row; bilingual zh-first to match chrome. Restore remounts the editor via editorEpoch. Stop saving empty docs: blank Untitled drafts now reuse-on-create and self-discard when navigated away from (refs avoid stale closures). Tests: versions_test.go, export_test.go (incl. valid-zip docx). go build/vet/test, tsc, vite build all clean; live end-to-end smoke verified snapshot/throttle/restore/export. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -176,6 +176,31 @@ CREATE INDEX idx_plagiarism_doc_id ON plagiarism_reports(doc_id);
|
||||
name: "0002_document_tone",
|
||||
stmt: `ALTER TABLE documents ADD COLUMN tone TEXT NOT NULL DEFAULT 'general';`,
|
||||
},
|
||||
{
|
||||
// Version history: point-in-time snapshots of a document's body so a
|
||||
// bad edit or LLM mishap is always recoverable. `kind` distinguishes
|
||||
// throttled background snapshots ('auto'), explicit user restore
|
||||
// points ('manual'), and the safety copy taken right before a restore
|
||||
// ('pre_restore') so restoring is itself undoable. Snapshots cascade
|
||||
// with the document. Stored fully (content + content_text) so a
|
||||
// restore is a plain copy with no re-derivation.
|
||||
name: "0003_document_versions",
|
||||
stmt: `
|
||||
CREATE TABLE document_versions (
|
||||
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
||||
doc_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
content_text TEXT NOT NULL,
|
||||
word_count INTEGER NOT NULL DEFAULT 0,
|
||||
kind TEXT NOT NULL DEFAULT 'auto'
|
||||
CHECK(kind IN ('auto','manual','pre_restore')),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_versions_doc_id ON document_versions(doc_id, created_at DESC);
|
||||
`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,30 @@ type Document struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// DocumentVersion is a point-in-time snapshot of a document's body, captured so
|
||||
// a writer can recover from a bad edit or an unwanted change. `Content` mirrors
|
||||
// the document's Tiptap JSON at snapshot time; `Kind` records why it was taken
|
||||
// (see the kind constants). List responses omit the heavy Content/ContentText
|
||||
// fields (the `omitempty`-friendly zero strings) and load them only on preview
|
||||
// or restore.
|
||||
type DocumentVersion struct {
|
||||
ID string `json:"id"`
|
||||
DocID string `json:"doc_id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content,omitempty"` // Tiptap JSON; omitted in list view
|
||||
ContentText string `json:"content_text,omitempty"` // plain text; omitted in list view
|
||||
WordCount int `json:"word_count"`
|
||||
Kind string `json:"kind"` // auto | manual | pre_restore
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// Document version kinds, mirrored from the schema CHECK constraint.
|
||||
const (
|
||||
VersionKindAuto = "auto" // throttled background snapshot on save
|
||||
VersionKindManual = "manual" // explicit "save a restore point"
|
||||
VersionKindPreRestore = "pre_restore" // safety copy taken just before a restore
|
||||
)
|
||||
|
||||
// Suggestion is a single LLM-proposed edit anchored to a span of the document.
|
||||
//
|
||||
// FromPos/ToPos are plaintext offsets into ContentText for server-side use only;
|
||||
|
||||
Reference in New Issue
Block a user