Compare commits

...
5 Commits
Author SHA1 Message Date
lzstealth 1bdc122995 Target Node 26
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m17s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s
2026-08-15 15:02:28 +01:00
lzstealth 2d748458d0 Remove GHCR publish path 2026-08-15 14:21:54 +01:00
lzstealth bb8cd98e61 Publish Docker images to both registries
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m25s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s
Publish Docker Image / build-and-push-ghcr (./build/Dockerfile, web, pulse-signage-web) (push) Failing after 1m8s
Publish Docker Image / build-and-push-ghcr (./build/Dockerfile.player, player, pulse-signage-player) (push) Failing after 31s
2026-08-15 14:13:49 +01:00
lzstealth aafe112c36 Release 2.7.5
Publish Docker Image / build-and-push (./build/Dockerfile, git.lzstealth.com/lzstealth/pulse-signage-web, web) (push) Successful in 1m18s
Publish Docker Image / build-and-push (./build/Dockerfile.player, git.lzstealth.com/lzstealth/pulse-signage-player, player) (push) Successful in 32s
2026-08-15 13:06:46 +01:00
lzstealth 1f1ad5d61f 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
2026-08-15 12:30:31 +01:00
21 changed files with 881 additions and 1016 deletions
+1
View File
@@ -1,5 +1,6 @@
*
!package.json
!package-lock.json
!build/
!build/**
!src/
+2
View File
@@ -4,6 +4,8 @@
- Treat `package.json` as the source of truth for the application version.
- Keep `package.json`, `build/package.player.json`, and `build/package.web.json` on the same version number.
- Keep dependency versions and package metadata in `package.json`, `package-lock.json`, `build/package.player.json`, and `build/package.web.json` aligned unless a dependency is intentionally omitted from a specific bundle.
- When changing bundled library versions, update the About page source data from the same package metadata or vendored asset banner rather than hardcoding a fresh literal.
- When the app version changes, update `CHANGELOG.md` in the same change.
- Keep database migration versions aligned with the release they actually belong to.
- If only part of a migration batch belongs to a newer release, split that batch into a separate migration entry instead of relabeling the earlier release.
+7 -5
View File
@@ -7,17 +7,18 @@ on:
workflow_dispatch:
jobs:
build-and-push:
build-and-push-existing-registry:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: web
image: git.lzstealth.com/lzstealth/pulse-signage-web
repository: pulse-signage-web
dockerfile: ./build/Dockerfile
- name: player
image: git.lzstealth.com/lzstealth/pulse-signage-player
repository: pulse-signage-player
dockerfile: ./build/Dockerfile.player
steps:
@@ -27,7 +28,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to container registry
- name: Log in to existing package registry
uses: docker/login-action@v3
with:
registry: git.lzstealth.com
@@ -38,7 +39,8 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ matrix.image }}
images: |
git.lzstealth.com/lzstealth/${{ matrix.repository }}
tags: |
type=raw,value=latest
type=ref,event=tag
+29
View File
@@ -2,6 +2,35 @@
All notable changes to this project will be documented in this file.
## 2.7.5 - 2026-08-15
### Changed
- The About page now reads bundled library versions from package metadata and the vendored AdminLTE stylesheet.
- AdminLTE is now reported as 4.3.1.
- Animate.css is now reported as 4.1.1.
- Bootstrap Icons is now reported as 1.13.1.
- Cropper.js is now reported as 1.6.2.
- Express is now reported as 5.2.1.
- Handlebars is now reported as 4.7.8.
- hls.js is now reported as 1.7.0.
- Multer is now reported as 2.2.0.
- MySQL2 is now reported as 3.23.3.
- Sharp is now reported as 0.35.3.
- TinyMCE is now reported from the vendored package metadata.
- ws is now reported as 8.21.3.
- The runtime and container images now target Node.js 26.
## 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
+2 -1
View File
@@ -1,4 +1,4 @@
FROM node:24-alpine
FROM node:26-alpine
WORKDIR /app
@@ -6,6 +6,7 @@ RUN apk add --no-cache chromium nss freetype harfbuzz ttf-freefont
COPY build/package.web.json ./package.json
RUN npm install --omit=dev --no-audit --no-fund
COPY package-lock.json ./package-lock.json
COPY src ./src
COPY scripts ./scripts
+2 -1
View File
@@ -1,4 +1,4 @@
FROM node:24-alpine
FROM node:26-alpine
WORKDIR /app
@@ -6,6 +6,7 @@ RUN apk add --no-cache ffmpeg
COPY build/package.player.json ./package.json
RUN npm install --omit=dev --no-audit --no-fund
COPY package-lock.json ./package-lock.json
COPY src ./src
COPY scripts ./scripts
+8 -5
View File
@@ -1,8 +1,11 @@
{
"name": "pulse-signage-player",
"version": "2.7.3",
"version": "2.7.5",
"private": false,
"description": "Pulse Signage player application bundle",
"engines": {
"node": ">=26.0.0"
},
"main": "src/common.js",
"scripts": {
"start": "node -r dotenv/config src/player.js",
@@ -10,10 +13,10 @@
},
"dependencies": {
"dotenv": "^17.4.2",
"express": "^4.21.2",
"express": "^5.2.1",
"handlebars": "^4.7.8",
"hls.js": "^1.5.15",
"mysql2": "^3.14.3",
"ws": "^8.21.0"
"hls.js": "^1.7.0",
"mysql2": "^3.23.3",
"ws": "^8.21.3"
}
}
+9 -6
View File
@@ -1,22 +1,25 @@
{
"name": "pulse-signage-web",
"version": "2.7.3",
"version": "2.7.5",
"private": false,
"description": "Pulse Signage web and bridge application bundle",
"engines": {
"node": ">=26.0.0"
},
"main": "src/common.js",
"scripts": {
"start": "node -r dotenv/config src/web.js",
"start:web": "node -r dotenv/config src/web.js"
},
"dependencies": {
"@sparticuz/chromium": "^137.0.0",
"@sparticuz/chromium": "^149.0.0",
"dotenv": "^17.4.2",
"express": "^4.21.2",
"express": "^5.2.1",
"handlebars": "^4.7.8",
"multer": "^2.2.0",
"mysql2": "^3.14.3",
"puppeteer-core": "^24.16.0",
"mysql2": "^3.23.3",
"puppeteer-core": "^25.7.0",
"sharp": "^0.35.3",
"ws": "^8.21.0"
"ws": "^8.21.3"
}
}
+569 -983
View File
File diff suppressed because it is too large Load Diff
+11 -8
View File
@@ -1,8 +1,11 @@
{
"name": "pulse-signage",
"version": "2.7.3",
"version": "2.7.5",
"private": false,
"description": "Pulse Signage application with MySQL and media storage",
"engines": {
"node": ">=26.0.0"
},
"repository": {
"type": "git",
"url": "https://git.lzstealth.com/LZStealth/pulse-signage.git"
@@ -17,19 +20,19 @@
"test": "node --test"
},
"dependencies": {
"@sparticuz/chromium": "^137.0.0",
"@sparticuz/chromium": "^149.0.0",
"animate.css": "^4.1.1",
"bootstrap-icons": "1.11.3",
"bootstrap-icons": "1.13.1",
"cropperjs": "^1.6.2",
"dotenv": "^17.4.2",
"express": "^4.21.2",
"express": "^5.2.1",
"handlebars": "^4.7.8",
"hls.js": "^1.5.15",
"hls.js": "^1.7.0",
"multer": "^2.2.0",
"mysql2": "^3.14.3",
"puppeteer-core": "^24.16.0",
"mysql2": "^3.23.3",
"puppeteer-core": "^25.7.0",
"sharp": "^0.35.3",
"ws": "^8.21.0"
"ws": "^8.21.3"
},
"devDependencies": {
"nodemon": "^3.1.10"
+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,
+82
View File
@@ -0,0 +1,82 @@
// Settings page renderer for the About page.
const fs = require('fs');
const path = require('path');
const { renderView } = require('../../../view');
const packageLock = require('#root/package-lock.json');
const tinyMcePackage = require('#root/src/web/public/vendor/tinymce/package.json');
function readVersionFromLockfile(packageName) {
const entry = packageLock && packageLock.packages ? packageLock.packages[`node_modules/${packageName}`] : null;
return entry && entry.version ? String(entry.version) : '';
}
function readPackageVersion(packageName) {
const lockedVersion = readVersionFromLockfile(packageName);
if (lockedVersion) {
return lockedVersion;
}
try {
const packageJsonPath = require.resolve(`${packageName}/package.json`);
return String(JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version || '');
} catch (_error) {
try {
let currentDir = path.dirname(require.resolve(packageName));
for (;;) {
const packageJsonPath = path.join(currentDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (String(parsed && parsed.name || '') === packageName) {
return String(parsed.version || '');
}
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
return '';
}
currentDir = parentDir;
}
} catch (_error2) {
return '';
}
}
}
function readAdminLteVersion() {
const cssPath = path.join(__dirname, '../../../public/adminlte/css/adminlte.min.css');
const cssText = fs.readFileSync(cssPath, 'utf8');
const match = cssText.match(/AdminLTE v([0-9]+\.[0-9]+\.[0-9]+)/i);
return match ? match[1] : '';
}
function buildLibraryList() {
return [
{ name: 'AdminLTE', version: readAdminLteVersion(), usage: 'Admin dashboard layout and components', license: 'MIT', url: 'https://adminlte.io/' },
{ name: 'Animate.css', version: readPackageVersion('animate.css'), usage: 'Reusable animation classes', license: 'MIT', url: 'https://animate.style/' },
{ name: 'Bootstrap Icons', version: readPackageVersion('bootstrap-icons'), usage: 'Icon set used throughout the UI', license: 'MIT', url: 'https://icons.getbootstrap.com/' },
{ name: 'Cropper.js', version: readPackageVersion('cropperjs'), usage: 'Image cropping in the slide editor', license: 'MIT', url: 'https://github.com/fengyuanchen/cropperjs' },
{ name: 'Express', version: readPackageVersion('express'), usage: 'Web server and routing', license: 'MIT', url: 'https://expressjs.com/' },
{ name: 'Handlebars', version: readPackageVersion('handlebars'), usage: 'Server-side view rendering', license: 'MIT', url: 'https://handlebarsjs.com/' },
{ name: 'hls.js', version: readPackageVersion('hls.js'), usage: 'HTTP Live Streaming playback', license: 'Apache-2.0', url: 'https://github.com/video-dev/hls.js' },
{ name: 'Multer', version: readPackageVersion('multer'), usage: 'Multipart file uploads', license: 'MIT', url: 'https://github.com/expressjs/multer' },
{ name: 'MySQL2', version: readPackageVersion('mysql2'), usage: 'Database access layer', license: 'MIT', url: 'https://github.com/sidorares/node-mysql2' },
{ name: 'Sharp', version: readPackageVersion('sharp'), usage: 'Image processing and transforms', license: 'Apache-2.0', url: 'https://sharp.pixelplumbing.com/' },
{ name: 'TinyMCE', version: tinyMcePackage.version, usage: 'Rich text editing for content fields', license: 'GPL-2.0-or-later', url: 'https://www.tiny.cloud/' },
{ name: 'ws', version: readPackageVersion('ws'), 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: []
});
};
module.exports.buildLibraryList = buildLibraryList;
+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>
+65
View File
@@ -0,0 +1,65 @@
const test = require('node:test');
const assert = require('node:assert/strict');
require('../src/common');
const packageJson = require('../package.json');
const pages = require('../src/web/pages');
const registerAboutRoutes = require('../src/web/routes/settings/about/routes');
const renderAboutPage = require('../src/web/routes/settings/about');
test('about page renders the footer link beside the version and lists bundled libraries', () => {
const html = pages.renderAboutPage({ id: 1 });
const libraries = renderAboutPage.buildLibraryList();
const orderedNames = libraries.map(function (library) {
return library.name;
});
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, new RegExp('v' + packageJson.version.split('.').join('\\.')));
assert.match(html, /About Pulse/);
assert.match(html, /href="\/settings\/about"/);
libraries.forEach(function (library) {
assert.ok(library.version, `${library.name} version is missing`);
assert.match(html, new RegExp(`>${library.version}<`));
});
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');
});