Release 2.7.4
Publish Docker Image / build-and-push (./build/Dockerfile, git.lzstealth.com/lzstealth/pulse-signage-web, web) (push) Successful in 1m15s
Publish Docker Image / build-and-push (./build/Dockerfile.player, git.lzstealth.com/lzstealth/pulse-signage-player, player) (push) Successful in 32s

This commit is contained in:
2026-08-15 12:30:31 +01:00
parent f0177e6628
commit 1f1ad5d61f
16 changed files with 198 additions and 11 deletions
+10
View File
@@ -2,6 +2,16 @@
All notable changes to this project will be documented in this file.
## 2.7.4 - 2026-08-15
### Added
- A new About page.
### Changed
- Refreshed the vendored AdminLTE assets to 4.3.1.
## 2.7.3 - 2026-08-15
### Changed
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pulse-signage-player",
"version": "2.7.3",
"version": "2.7.4",
"private": false,
"description": "Pulse Signage player application bundle",
"main": "src/common.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pulse-signage-web",
"version": "2.7.3",
"version": "2.7.4",
"private": false,
"description": "Pulse Signage web and bridge application bundle",
"main": "src/common.js",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "pulse-signage",
"version": "2.7.3",
"version": "2.7.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pulse-signage",
"version": "2.7.3",
"version": "2.7.4",
"dependencies": {
"@sparticuz/chromium": "^137.0.0",
"animate.css": "^4.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pulse-signage",
"version": "2.7.3",
"version": "2.7.4",
"private": false,
"description": "Pulse Signage application with MySQL and media storage",
"repository": {
+1
View File
@@ -9,6 +9,7 @@ function routePath(...segments) {
module.exports = {
renderLoginPage: require(routePath('auth', 'login')),
renderAccountPage: require(routePath('account', 'password')),
renderAboutPage: require(routePath('settings', 'about', 'index')),
renderUsersPage: require(routePath('settings', 'users', 'list')),
renderUsersAddPage: require(routePath('settings', 'users', 'add')),
renderUsersEditPage: require(routePath('settings', 'users', 'edit')),
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -15,6 +15,7 @@ const registerRssFeedRoutes = require('./data-sources/rss-feeds/routes');
const registerTimetableRoutes = require('./data-sources/timetables/routes');
const registerSettingsRoutes = require('./settings/background-tasks');
const registerFontRoutes = require('./settings/fonts');
const registerAboutRoutes = require('./settings/about/routes');
const registerInternalSyncRoutes = require('./internal/sync');
const registerScreensRoutes = require('./signage/screens/routes');
const { PERMISSIONS, normalizePermissionKeys } = require('#src/rbac');
@@ -24,6 +25,7 @@ function registerRoutes(app, deps) {
registerAuthAndAccountRoutes(app, deps);
registerSignageRoutes(app, deps);
registerSettingsAndContentRoutes(app, deps);
registerAboutRoutes(app, deps);
registerInternalSyncRoutes(app, {
backgroundTaskQueue: deps.backgroundTaskQueue,
uploadSyncService: deps.uploadSyncService,
+31
View File
@@ -0,0 +1,31 @@
// Settings page renderer for the About page.
const { renderView } = require('../../../view');
function buildLibraryList() {
return [
{ name: 'AdminLTE', version: '4.3.1', usage: 'Admin dashboard layout and components', license: 'MIT', url: 'https://adminlte.io/' },
{ name: 'Animate.css', version: '4.1.1', usage: 'Reusable animation classes', license: 'MIT', url: 'https://animate.style/' },
{ name: 'Bootstrap Icons', version: '1.11.3', usage: 'Icon set used throughout the UI', license: 'MIT', url: 'https://icons.getbootstrap.com/' },
{ name: 'Cropper.js', version: '1.6.2', usage: 'Image cropping in the slide editor', license: 'MIT', url: 'https://github.com/fengyuanchen/cropperjs' },
{ name: 'Express', version: '4.21.2', usage: 'Web server and routing', license: 'MIT', url: 'https://expressjs.com/' },
{ name: 'Handlebars', version: '4.7.8', usage: 'Server-side view rendering', license: 'MIT', url: 'https://handlebarsjs.com/' },
{ name: 'hls.js', version: '1.5.15', usage: 'HTTP Live Streaming playback', license: 'Apache-2.0', url: 'https://github.com/video-dev/hls.js' },
{ name: 'Multer', version: '2.2.0', usage: 'Multipart file uploads', license: 'MIT', url: 'https://github.com/expressjs/multer' },
{ name: 'MySQL2', version: '3.14.3', usage: 'Database access layer', license: 'MIT', url: 'https://github.com/sidorares/node-mysql2' },
{ name: 'Sharp', version: '0.35.3', usage: 'Image processing and transforms', license: 'Apache-2.0', url: 'https://sharp.pixelplumbing.com/' },
{ name: 'TinyMCE', version: '7.7.0', usage: 'Rich text editing for content fields', license: 'GPL-2.0-or-later', url: 'https://www.tiny.cloud/' },
{ name: 'ws', version: '8.21.0', usage: 'WebSocket transport', license: 'MIT', url: 'https://github.com/websockets/ws' }
];
}
module.exports = function renderAboutPage(currentUser) {
return renderView('settings/about/index', {
title: 'About',
active: 'about',
currentUser: currentUser || null,
repositoryUrl: 'https://git.lzstealth.com/LZStealth/pulse-signage.git',
libraries: buildLibraryList(),
scripts: []
});
};
+13
View File
@@ -0,0 +1,13 @@
// Settings route registration for the About page.
module.exports = function registerAboutRoutes(app, deps) {
const pages = deps.pages;
if (!pages) {
throw new Error('registerAboutRoutes requires pages.');
}
app.get('/settings/about', function (req, res) {
res.send(pages.renderAboutPage(req.currentUser));
});
};
+68
View File
@@ -0,0 +1,68 @@
<div class="page-header">
<div>
<h2>About Pulse Signage</h2>
<p>Version details and the main open-source libraries bundled with the app.</p>
</div>
</div>
<div class="row g-3">
<div class="col-12 col-xl-4">
<div class="card card-outline card-primary h-100">
<div class="card-body">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<div>
<div class="text-uppercase text-muted small fw-semibold">Application</div>
<div class="fs-3 fw-semibold">Pulse Signage</div>
</div>
<div class="text-end">
<div class="text-muted small text-uppercase">Version</div>
<div class="fs-4 fw-semibold">v{{appVersion}}</div>
</div>
</div>
<p class="text-muted mb-3">Pulse is a self-hosted signage platform for managing screens, playlists, slides, and data sources from one web interface.</p>
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-primary" href="/dashboard"><i class="bi bi-speedometer2 me-1" aria-hidden="true"></i>Open dashboard</a>
<a class="btn btn-outline-secondary" href="{{repositoryUrl}}" target="_blank" rel="noreferrer"><i class="bi bi-github me-1" aria-hidden="true"></i>Repository</a>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-8">
<div class="card card-outline card-primary h-100 overflow-hidden">
<div class="card-header">
<h3 class="card-title">Third-party libraries</h3>
</div>
<div class="table-responsive">
<table class="table table-striped align-middle mb-0">
<thead>
<tr>
<th>Library</th>
<th>Version</th>
<th>Use</th>
<th>License</th>
</tr>
</thead>
<tbody>
{{#each libraries}}
<tr>
<td class="fw-semibold">{{name}}</td>
<td>{{version}}</td>
<td>{{usage}}</td>
<td>
{{#if url}}
<a href="{{url}}" target="_blank" rel="noreferrer">{{license}}</a>
{{else}}
{{license}}
{{/if}}
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
</div>
+3 -2
View File
@@ -326,8 +326,9 @@
</ul>
</nav>
<div class="sidebar-version mt-auto pt-2 text-secondary small">
<div class="sidebar-version__inner border-top pt-2">
v{{appVersion}}
<div class="sidebar-version__inner border-top pt-2 d-flex align-items-center justify-content-between gap-3">
<span>v{{appVersion}}</span>
<a href="/settings/about" class="text-secondary text-decoration-none text-nowrap">Pulse Signage</a>
</div>
</div>
</div>
+59
View File
@@ -0,0 +1,59 @@
const test = require('node:test');
const assert = require('node:assert/strict');
require('../src/common');
const pages = require('../src/web/pages');
const registerAboutRoutes = require('../src/web/routes/settings/about/routes');
test('about page renders the footer link beside the version and lists bundled libraries', () => {
const html = pages.renderAboutPage({ id: 1 });
const orderedNames = ['AdminLTE', 'Animate.css', 'Bootstrap Icons', 'Cropper.js', 'Express', 'Handlebars', 'hls.js', 'Multer', 'MySQL2', 'Sharp', 'TinyMCE', 'ws'];
let previousIndex = -1;
orderedNames.forEach(function (name) {
const currentIndex = html.indexOf(name);
assert.ok(currentIndex >= 0, `missing ${name}`);
assert.ok(currentIndex > previousIndex, `${name} is out of order`);
previousIndex = currentIndex;
});
assert.match(html, /v2\.7\.4/);
assert.match(html, /About Pulse/);
assert.match(html, /href="\/settings\/about"/);
assert.match(html, /Cropper\.js/);
assert.match(html, /TinyMCE/);
assert.match(html, /7\.7\.0/);
assert.match(html, /card card-outline card-primary h-100 overflow-hidden/);
assert.doesNotMatch(html, /rounded-3 overflow-hidden border/);
});
test('about route sends the about page', async () => {
const handlers = {};
const app = {
get(path, ...routeHandlers) {
handlers[path] = routeHandlers;
}
};
registerAboutRoutes(app, {
pages: {
renderAboutPage(currentUser) {
return currentUser && currentUser.id === 7 ? 'about-page' : 'unexpected';
}
}
});
const response = {
body: null,
send(value) {
this.body = value;
return this;
}
};
await handlers['/settings/about'][0]({ currentUser: { id: 7 } }, response, function () {});
assert.equal(response.body, 'about-page');
});