Content history

Opt-in per-file version history for the site content (SM085). With the plugin enabled, every save - from the manager, WebDAV or an AI connector - becomes a recorded version, and each file gains a History panel on the Files page.

Enabling

One switch: tick the Content history plugin on Plugin Manager. Its enable hook sets git_history: enabled in lazysite.conf, initialises the repository and takes an adoption commit of the current site. Unticking pauses recording; every recorded version is kept, and re-enabling resumes on top of them. The Status / Enable / Pause actions on Plugin Config are the inspection and recovery surface, driving the same machinery as the git-init control-API action (manage_config).

The repository lives at lazysite/git/ - inside the never-served infra tree, so no .git sits under the docroot for a web server to leak - with the docroot as the work tree.

What gets recorded

Every content write is an automatic commit with the acting user as author (user <user@lazysite>) and the action as the message ("edit about.md", "move a.md -> b.md"): manager save/delete/move/copy/migrate, uploads, WebDAV PUT/DELETE/MOVE/COPY, MCP writes, nav and site-config saves, and a content-backup restore. A batched operation is one commit. A git failure never breaks the write - the save proceeds with a warning, and the failure is made visible through the status probe.

Versioned: the content tree plus the two operator-authored config files (lazysite/lazysite.conf, lazysite/nav.conf).

Deliberately excluded (written to the repo's exclude list at init): lazysite/auth/, lazysite/forms/ (submissions), notify-xmpp.conf, git-sync.conf, cache/logs/backups/locks, the manager CSRF secret and upload-rate store, aliases.json, the repo itself, generated *.html and lazysite-assets/ mirrors, and installer state. This exclude list is the security boundary that keeps the history free of secrets and personal data, and therefore safe to sync to a private remote. Full-system backups remain the disaster-recovery mechanism - they carry exactly what the history excludes.

View, diff and restore

Each file row on the Files page gains a History panel: the commit list (when, who, what), a read-only View of any version, a unified Diff against the current file, and Restore. A restore writes the old content back through the normal save path - cache invalidation including host copies, alias re-indexing, audit - so the restore is itself the newest version and nothing is ever lost. The read actions are git-history, git-show and git-status (token clients: manage_content, audit-skipped); git-restore shares the content grant and is audited.

Renames and lineage

History follows a rename (SM175): a move records a structured commit trailer naming the source path, and the per-file log walks across it into the old path's history. The walk stops at the commit that added the file, so history never leaks across a delete-and-recreate at a reused path.

History overview and statistics

Beyond the per-file panel, a git-history-summary action (and the list_content_history MCP tool, SM199) drive a Files-page History overview: per-file revision count, first and last commit dates, last author, and site totals across the tracked tree. It enumerates the committed tree at HEAD and aggregates each path's lineage-aware timeline, so it is rename-aware and leak-safe by the same rules as the per-file view. Each file's count is bounded at 200 revisions, the same cap the per-file view honours.

Status is a health probe

The plugin's Status reports a genuine verdict, beyond a boolean: enabled-and-healthy, paused, half-enabled/inconsistent (the config says on but the repository is missing or unusable), or degraded (the last attempt to record a version failed, or a stale lock is present). A failed commit leaves a breadcrumb file that the status action, the Files page and lazysite check all read - so a history that is silently failing to record is visible without log-diving, and lazysite check --fix repairs it.

Remote sync (the git-sync plugin)

With content history enabled, the opt-in Remote sync plugin syncs it with a private remote repository (Forgejo, Gitea, or similar). Configure the remote address (https://…, git@host:path or ssh:// only), branch (default main) and access token (password-typed; lazysite/git-sync.conf is itself excluded from the versioned set) on Plugin Config, then use Test connection, Push and Pull. Push refuses cleanly when the remote is ahead - it never forces. On a two-sided change, Pull names the pages and offers Keep mine or Take theirs; every apply is preceded by a safety snapshot, invalidates the render caches and re-indexes aliases. The https token reaches git through a transient askpass helper, never a command line or the stored URL.