feat: lock pages against editor changes

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 the 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 assertNotLockedForEditor() guard, called from
  save/saveDraft/schedule.
- getPages responses gain a locked flag; the Pages list gets a Lock/
  Unlock row action and a Locked badge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Geoff Moore
2026-10-02 12:11:06 +01:00
co-authored by Claude Sonnet 5
parent 43200d7dfe
commit 83ae3a08ff
5 changed files with 73 additions and 1 deletions
+10
View File
@@ -447,6 +447,7 @@
const name = $('<td class="page-cell">').append(docIcon());
$('<button type="button" class="page-open">').text(page.name).appendTo(name);
if (page.draft) $('<span class="badge-draft">').text(page.pending ? 'New - unpublished' : 'Draft').appendTo(name);
if (page.locked) $('<span class="badge-draft badge-locked">').text('Locked').appendTo(name);
row.append(name, $('<td class="page-title">').text(page.title), $('<td class="page-date">').text(formatDate(page.modified)));
const actions = $('<td class="row-actions manage-col">').toggle(permissions.manage).appendTo(row);
if (permissions.manage && page.pending) {
@@ -456,6 +457,8 @@
$('<button type="button">').text(action[0]).toggleClass('danger-text', action[1] === 'delete')
.on('click', function (event) { event.stopPropagation(); action[1] === 'template' ? setPageTemplate(page.url, true) : managePage(action[1], page.url); }).appendTo(actions);
});
$('<button type="button">').text(page.locked ? 'Unlock' : 'Lock')
.on('click', function (event) { event.stopPropagation(); lockPage(page.url, !page.locked); }).appendTo(actions);
}
body.append(row);
});
@@ -638,6 +641,13 @@
} catch (error) { showMessage(error.message, 'error'); }
}
async function lockPage(uri, on) {
try {
showMessage((await api('lockPage', {uri: uri, value: on ? '1' : '0'}, 'POST')).message, 'success');
openPages();
} catch (error) { showMessage(error.message, 'error'); }
}
async function discardNewPage(uri) {
if (!window.confirm('Discard this unpublished page and its draft? This cannot be undone.')) return;
try {