Release v2.20.2
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m23s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s

This commit is contained in:
lzstealth committed 2026-10-25 19:08:10 +00:00
1 parent 220c3f401e
commit 61d553f74b
14 files changed
+395 -94

No files matched your search

+62 -18
View File
@@ -649,33 +649,77 @@
gap: 0.5rem;
}
/* Audit log change comparisons */
/* Audit log change details */
.audit-change-list {
.audit-change-summary {
display: grid;
padding: 0.08rem 0.3rem;
border-radius: 0.2rem;
gap: 0.2rem;
min-width: 18rem;
gap: 0.4rem;
min-width: 12rem;
}
.audit-change-row {
display: grid;
grid-template-columns: minmax(8rem, 0.7fr) minmax(5rem, 1fr) auto minmax(5rem, 1fr);
gap: 0.35rem;
.audit-change-paths {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: baseline;
font-size: 0.78rem;
}
.audit-change-to {
.audit-change-path {
font-size: 0.72rem;
overflow-wrap: anywhere;
}
.audit-diff-changes {
display: grid;
gap: 1rem;
}
.audit-diff-change {
min-width: 0;
padding-bottom: 1rem;
border-bottom: 1px solid var(--bs-border-color);
}
.audit-diff-path {
overflow-wrap: anywhere;
}
.audit-diff-pair {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
}
.audit-diff-column {
min-width: 0;
}
.audit-diff-label {
margin-bottom: 0.25rem;
color: var(--bs-secondary-color);
font-size: 0.8rem;
font-weight: 600;
}
.audit-diff-pane {
max-height: 24rem;
min-height: 8rem;
margin: 0;
overflow: auto;
padding: 0.75rem;
border: 1px solid var(--bs-border-color);
border-radius: 0.25rem;
background: var(--bs-tertiary-bg);
color: var(--bs-body-color);
font-size: 0.8rem;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.audit-change-from {
overflow-wrap: anywhere;
white-space: pre-wrap;
.audit-diff-removed {
background: var(--bs-danger-bg-subtle);
color: var(--bs-danger-text-emphasis);
text-decoration: line-through;
}
.audit-change-from del,
.audit-change-to ins {
text-decoration-thickness: 2px;
.audit-diff-added {
background: var(--bs-success-bg-subtle);
color: var(--bs-success-text-emphasis);
text-decoration: none;
}
@media (max-width: 767.98px) {
.audit-diff-pair {
grid-template-columns: minmax(0, 1fr);
}
}
/* Template preview states and permission tables */
+171
View File
@@ -0,0 +1,171 @@
(function () {
function buildDiffSegments(beforeValue, afterValue) {
var before = Array.from(String(beforeValue == null ? '' : beforeValue));
var after = Array.from(String(afterValue == null ? '' : afterValue));
var prefixLength = 0;
var suffixLength = 0;
while (prefixLength < before.length && prefixLength < after.length && before[prefixLength] === after[prefixLength]) {
prefixLength += 1;
}
while (suffixLength < before.length - prefixLength && suffixLength < after.length - prefixLength &&
before[before.length - suffixLength - 1] === after[after.length - suffixLength - 1]) {
suffixLength += 1;
}
function toSegments(characters) {
var segments = [];
var changedEnd = characters.length - suffixLength;
if (prefixLength) segments.push({ text: characters.slice(0, prefixLength).join(''), changed: false });
if (changedEnd > prefixLength) segments.push({ text: characters.slice(prefixLength, changedEnd).join(''), changed: true });
if (suffixLength) segments.push({ text: characters.slice(changedEnd).join(''), changed: false });
return segments;
}
return { before: toSegments(before), after: toSegments(after) };
}
function appendSegments(element, segments, changeClass) {
segments.forEach(function (segment) {
if (!segment.changed) {
element.appendChild(document.createTextNode(segment.text));
return;
}
var mark = document.createElement(changeClass === 'audit-diff-removed' ? 'del' : 'ins');
mark.className = changeClass;
mark.textContent = segment.text;
element.appendChild(mark);
});
}
function synchronizeScroll(beforePane, afterPane) {
var isSyncing = false;
function sync(source, target) {
if (isSyncing) return;
isSyncing = true;
var sourceVerticalRange = source.scrollHeight - source.clientHeight;
var targetVerticalRange = target.scrollHeight - target.clientHeight;
var sourceHorizontalRange = source.scrollWidth - source.clientWidth;
var targetHorizontalRange = target.scrollWidth - target.clientWidth;
target.scrollTop = sourceVerticalRange > 0 ? source.scrollTop / sourceVerticalRange * targetVerticalRange : 0;
target.scrollLeft = sourceHorizontalRange > 0 ? source.scrollLeft / sourceHorizontalRange * targetHorizontalRange : 0;
window.requestAnimationFrame(function () { isSyncing = false; });
}
beforePane.addEventListener('scroll', function () { sync(beforePane, afterPane); }, { passive: true });
afterPane.addEventListener('scroll', function () { sync(afterPane, beforePane); }, { passive: true });
}
function createDiffPane(label, value, segments, changeClass) {
var paneWrapper = document.createElement('section');
paneWrapper.className = 'audit-diff-column';
var heading = document.createElement('div');
heading.className = 'audit-diff-label';
heading.textContent = label;
var pane = document.createElement('pre');
pane.className = 'audit-diff-pane';
pane.tabIndex = 0;
pane.setAttribute('aria-label', label + ' value');
if (segments) {
appendSegments(pane, segments, changeClass);
} else {
pane.textContent = value == null ? '' : String(value);
}
paneWrapper.appendChild(heading);
paneWrapper.appendChild(pane);
return { wrapper: paneWrapper, pane: pane };
}
function createChangeBlock(change) {
var block = document.createElement('section');
block.className = 'audit-diff-change';
var path = document.createElement('h4');
path.className = 'audit-diff-path h6';
path.textContent = String(change.path || 'Changed value');
var pair = document.createElement('div');
pair.className = 'audit-diff-pair';
var segments = buildDiffSegments(change.from, change.to);
var before = createDiffPane('Before', change.from, segments.before, 'audit-diff-removed');
var after = createDiffPane('After', change.to, segments.after, 'audit-diff-added');
pair.appendChild(before.wrapper);
pair.appendChild(after.wrapper);
synchronizeScroll(before.pane, after.pane);
block.appendChild(path);
block.appendChild(pair);
return block;
}
function renderRows(container, changeRows) {
container.replaceChildren();
changeRows.forEach(function (change) {
container.appendChild(createChangeBlock(change));
});
}
function initializeAuditDetails() {
var modal = document.getElementById('audit-change-detail-modal');
if (!modal) return;
var status = modal.querySelector('[data-audit-detail-status]');
var content = modal.querySelector('[data-audit-detail-content]');
var rows = content;
var subtitle = modal.querySelector('[data-audit-detail-subtitle]');
var requestNumber = 0;
document.addEventListener('click', function (event) {
var button = event.target.closest('[data-audit-detail-url]');
if (!button) return;
var currentRequest = ++requestNumber;
var detailUrl = button.getAttribute('data-audit-detail-url');
rows.replaceChildren();
content.hidden = true;
status.textContent = 'Loading change details...';
status.hidden = false;
subtitle.textContent = button.getAttribute('data-audit-detail-title') || '';
if (!window.pulseModal || !window.pulseModal.show(modal)) {
status.textContent = 'Change details are unavailable.';
return;
}
button.disabled = true;
fetch(detailUrl, {
credentials: 'same-origin',
headers: { Accept: 'application/json' }
}).then(function (response) {
if (!response.ok) throw new Error('Audit details request failed.');
return response.json();
}).then(function (result) {
if (currentRequest !== requestNumber) return;
var changeRows = result && Array.isArray(result.changeRows) ? result.changeRows : [];
renderRows(rows, changeRows);
if (changeRows.length) {
content.hidden = false;
status.hidden = true;
} else {
status.textContent = 'No change details are available.';
}
}).catch(function () {
if (currentRequest === requestNumber) {
status.textContent = 'Unable to load change details.';
}
}).finally(function () {
button.disabled = false;
});
});
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { buildDiffSegments: buildDiffSegments };
}
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeAuditDetails);
} else {
initializeAuditDetails();
}
}
}());