Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bdc122995 | ||
|
|
2d748458d0 | ||
|
|
bb8cd98e61 | ||
|
|
aafe112c36 | ||
|
|
1f1ad5d61f |
@@ -1,5 +1,6 @@
|
||||
*
|
||||
!package.json
|
||||
!package-lock.json
|
||||
!build/
|
||||
!build/**
|
||||
!src/
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+570
-984
File diff suppressed because it is too large
Load Diff
+11
-8
@@ -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"
|
||||
|
||||
@@ -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')),
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
});
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
Reference in New Issue
Block a user