Administrators can lock a page from the Pages list to stop editors publishing, saving a draft, or scheduling a change to it. Administrators are never restricted by a lock.
State is stored per-page URI in a new cms/data/pagelocks.json, following the existing pagetemplates.json pattern: it moves with the page on rename, is cleared on delete, and is never copied onto a duplicate.
New lockPageAction and an assertNotLockedForEditor() guard on save/saveDraft/schedule. getPages responses gain a locked flag; the Pages list gets a Lock/Unlock row action and a Locked badge.
Documentation catch-up: README, the changelog and the built-in welcome page now also cover this feature and the bootstrap/LoginRateLimiter/Logger hardening from the earlier security-audit PR (#41), which had landed in code but not in the docs.
Test plan
php -l on every changed PHP file
node --check cms/js/cms.js
php tests/run.php (full suite, including new lock/unlock, rename-moves-lock, delete-clears-lock, and duplicate-never-copies-lock assertions)
Manual: lock a page as an administrator, confirm an editor is blocked from saving/drafting/scheduling it with a clear message, confirm an administrator is never blocked, unlock and confirm the editor can save again
## Summary
- Administrators can lock a page from the Pages list to stop editors publishing, saving a draft, or scheduling a change to it. Administrators are never restricted by a lock.
- State is stored per-page URI in a new `cms/data/pagelocks.json`, following the existing `pagetemplates.json` pattern: it moves with the page on rename, is cleared on delete, and is never copied onto a duplicate.
- New `lockPageAction` and an `assertNotLockedForEditor()` guard on `save`/`saveDraft`/`schedule`. `getPages` responses gain a `locked` flag; the Pages list gets a Lock/Unlock row action and a Locked badge.
- Documentation catch-up: README, the changelog and the built-in welcome page now also cover this feature and the bootstrap/LoginRateLimiter/Logger hardening from the earlier security-audit PR (#41), which had landed in code but not in the docs.
## Test plan
- [x] `php -l` on every changed PHP file
- [x] `node --check cms/js/cms.js`
- [x] `php tests/run.php` (full suite, including new lock/unlock, rename-moves-lock, delete-clears-lock, and duplicate-never-copies-lock assertions)
- [ ] Manual: lock a page as an administrator, confirm an editor is blocked from saving/drafting/scheduling it with a clear message, confirm an administrator is never blocked, unlock and confirm the editor can save again
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
cms/data/pagelocks.json, following the existingpagetemplates.jsonpattern: it moves with the page on rename, is cleared on delete, and is never copied onto a duplicate.lockPageActionand anassertNotLockedForEditor()guard onsave/saveDraft/schedule.getPagesresponses gain alockedflag; the Pages list gets a Lock/Unlock row action and a Locked badge.Test plan
php -lon every changed PHP filenode --check cms/js/cms.jsphp tests/run.php(full suite, including new lock/unlock, rename-moves-lock, delete-clears-lock, and duplicate-never-copies-lock assertions)🤖 Generated with Claude Code