Release v2.14.1
This commit is contained in:
1 parent
635e1a9942
commit
c3299abaac
174 files changed
+4483
-1687
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
// Admin data aggregation helpers for dashboards, playlists, slides, templates, and screens.
|
||||
|
||||
const { fetchPagedRows } = require('./utils');
|
||||
const { fetchPagedRows } = require('#src/data/utils');
|
||||
|
||||
async function fetchAdminData(pool) {
|
||||
const [playlists] = await pool.query('SELECT id, name, fade_between_slides, skip_unavailable_rtmp, created_at, modified_at, created_by, modified_by FROM c_playlists ORDER BY id DESC');
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Announcement data access helpers.
|
||||
|
||||
const { fetchPagedRows } = require('./utils');
|
||||
const { fetchPagedRows } = require('#src/data/utils');
|
||||
const {
|
||||
ANNOUNCEMENT_ICON_KEYS,
|
||||
DEFAULT_ANNOUNCEMENT_ICON,
|
||||
normalizeAnnouncementIcon: normalizeAnnouncementIconFromConfig
|
||||
} = require('./announcement-icons');
|
||||
const { validateMaxLength } = require('./utils');
|
||||
} = require('#src/data/announcement-icons');
|
||||
const { validateMaxLength } = require('#src/data/utils');
|
||||
|
||||
const SHORT_LABEL_MAX_LENGTH = 255;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const { fetchPagedRows, validateMaxLength } = require('./utils');
|
||||
const { fetchPagedRows, validateMaxLength } = require('#src/data/utils');
|
||||
|
||||
const NAME_MAX_LENGTH = 255;
|
||||
const URL_MAX_LENGTH = 1024;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Application-wide settings defaults and persistence helpers.
|
||||
|
||||
const { DEFAULT_ANNOUNCEMENT_ICON_KEYS } = require('./announcement-icons');
|
||||
const { DEFAULT_ANNOUNCEMENT_ICON_KEYS } = require('#src/data/announcement-icons');
|
||||
|
||||
const SETTING_DEFINITIONS = [
|
||||
{ key: 'app.name', type: 'string', defaultValue: 'Pulse Signage' },
|
||||
|
||||
@@ -48,7 +48,7 @@ const SCREEN_CONTROL_COMMAND_LABELS = Object.freeze({
|
||||
moveclient: 'Move client',
|
||||
setclientname: 'Rename client'
|
||||
});
|
||||
const { fetchAppSettings } = require('./app-settings');
|
||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
|
||||
function formatUserAgentLabel(userAgent) {
|
||||
const value = String(userAgent || '').trim();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Canvas size data access and pagination helpers.
|
||||
|
||||
const { fetchPagedRows } = require('./utils');
|
||||
const { fetchPagedRows } = require('#src/data/utils');
|
||||
const MAX_CANVAS_SIZE_DIMENSION = 16384;
|
||||
|
||||
async function fetchCanvasSizesData(pool) {
|
||||
|
||||
+14
-14
@@ -1,19 +1,19 @@
|
||||
// Aggregated exports for the shared data access layer.
|
||||
|
||||
const { fetchAdminData, fetchPlaylistsPage, fetchSlidesPage, fetchTemplatesPage, fetchCanvasSizesPage, fetchScreensPage } = require('./admin');
|
||||
const { ANNOUNCEMENT_TYPES, ANNOUNCEMENT_COLORS, ANNOUNCEMENT_ICONS, DEFAULT_ANNOUNCEMENT_ICON, normalizeAnnouncementType, normalizeAnnouncementColor, normalizeAnnouncementIcon, fetchAnnouncementsPage, fetchAnnouncementById, fetchActiveAnnouncement, buildAnnouncementPayload } = require('./announcements');
|
||||
const { ANNOUNCEMENT_ICON_OPTIONS, ANNOUNCEMENT_ICON_LABELS } = require('./announcement-icons');
|
||||
const { fetchPlaylistById } = require('./playlists');
|
||||
const { normalizeDisplayMode, fetchTimetablesData, fetchTimetableGroupsPage, fetchTimetableGroupById, fetchTimetableEntriesByGroupId, buildTimetableGroupPayload } = require('./timetables');
|
||||
const { fetchApiSourcesData, fetchApiSourcesPage, fetchApiSourceById, fetchApiSourceResponse, buildApiSourcePayload } = require('./api-sources');
|
||||
const { fetchRssFeedsData, fetchRssFeedsPage, fetchRssFeedById, fetchRssFeedItemsByFeedId, normalizeRssFeedItem, buildRssFeedPayload, fetchRssFeedItems, replaceRssFeedItems } = require('./rss-feeds');
|
||||
const { fetchWeatherLocationsData, fetchWeatherLocationsPage, fetchWeatherLocationById, fetchWeatherLocationSuggestions, fetchWeatherLocationForecast, buildWeatherLocationPayload } = require('./weather');
|
||||
const { slugify, uniqueScreenSlug, fetchScreenById, fetchScreenEditData, fetchScreenPlayerUrls, fetchPlayerPublicBaseUrl, fetchScreenPlayerRecord, fetchPlayerRecordByIdentifier } = require('./screens');
|
||||
const { fetchPlayerRegistrations } = require('./player-registry');
|
||||
const { fetchTemplateById, fetchTemplatesData, extractTemplateRegions, buildTemplatePayload } = require('./templates');
|
||||
const { fetchCanvasSizesData, fetchCanvasSizeById, buildCanvasSizePayload, MAX_CANVAS_SIZE_DIMENSION } = require('./canvas-sizes');
|
||||
const { fetchSlideById, buildSlidePayload } = require('./slides');
|
||||
const { parseJsonSafe, fetchDuplicateName, validateMaxLength, truncateToMaxLength } = require('./utils');
|
||||
const { fetchAdminData, fetchPlaylistsPage, fetchSlidesPage, fetchTemplatesPage, fetchCanvasSizesPage, fetchScreensPage } = require('#src/data/admin');
|
||||
const { ANNOUNCEMENT_TYPES, ANNOUNCEMENT_COLORS, ANNOUNCEMENT_ICONS, DEFAULT_ANNOUNCEMENT_ICON, normalizeAnnouncementType, normalizeAnnouncementColor, normalizeAnnouncementIcon, fetchAnnouncementsPage, fetchAnnouncementById, fetchActiveAnnouncement, buildAnnouncementPayload } = require('#src/data/announcements');
|
||||
const { ANNOUNCEMENT_ICON_OPTIONS, ANNOUNCEMENT_ICON_LABELS } = require('#src/data/announcement-icons');
|
||||
const { fetchPlaylistById } = require('#src/data/playlists');
|
||||
const { normalizeDisplayMode, fetchTimetablesData, fetchTimetableGroupsPage, fetchTimetableGroupById, fetchTimetableEntriesByGroupId, buildTimetableGroupPayload } = require('#src/data/timetables');
|
||||
const { fetchApiSourcesData, fetchApiSourcesPage, fetchApiSourceById, fetchApiSourceResponse, buildApiSourcePayload } = require('#src/data/api-sources');
|
||||
const { fetchRssFeedsData, fetchRssFeedsPage, fetchRssFeedById, fetchRssFeedItemsByFeedId, normalizeRssFeedItem, buildRssFeedPayload, fetchRssFeedItems, replaceRssFeedItems } = require('#src/data/rss-feeds');
|
||||
const { fetchWeatherLocationsData, fetchWeatherLocationsPage, fetchWeatherLocationById, fetchWeatherLocationSuggestions, fetchWeatherLocationForecast, buildWeatherLocationPayload } = require('#src/data/weather');
|
||||
const { slugify, uniqueScreenSlug, fetchScreenById, fetchScreenEditData, fetchScreenPlayerUrls, fetchPlayerPublicBaseUrl, fetchScreenPlayerRecord, fetchPlayerRecordByIdentifier } = require('#src/data/screens');
|
||||
const { fetchPlayerRegistrations } = require('#src/data/player-registry');
|
||||
const { fetchTemplateById, fetchTemplatesData, extractTemplateRegions, buildTemplatePayload } = require('#src/data/templates');
|
||||
const { fetchCanvasSizesData, fetchCanvasSizeById, buildCanvasSizePayload, MAX_CANVAS_SIZE_DIMENSION } = require('#src/data/canvas-sizes');
|
||||
const { fetchSlideById, buildSlidePayload } = require('#src/data/slides');
|
||||
const { parseJsonSafe, fetchDuplicateName, validateMaxLength, truncateToMaxLength } = require('#src/data/utils');
|
||||
|
||||
module.exports = {
|
||||
slugify,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Persistent player registration and heartbeat helpers shared by the web app and bridge.
|
||||
// These utilities intentionally keep URL normalization and heartbeat staleness rules in one place
|
||||
// so the web routing logic and media sync tasks all make the same decision about a live player.
|
||||
|
||||
function normalizeDeviceId(value) {
|
||||
return String(value || '')
|
||||
@@ -11,6 +13,34 @@ function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function isLocalLikeBaseUrl(value) {
|
||||
let host = '';
|
||||
try {
|
||||
host = new URL(String(value || '').trim().replace(/\/$/, '')).hostname.toLowerCase();
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return host === 'localhost'
|
||||
|| host === '127.0.0.1'
|
||||
|| host === '::1'
|
||||
|| host === 'host.docker.internal'
|
||||
|| host === 'player'
|
||||
|| host === 'web'
|
||||
|| host === 'player-bridge'
|
||||
|| host.endsWith('.local')
|
||||
|| host.endsWith('.internal')
|
||||
|| host.endsWith('.docker.internal');
|
||||
}
|
||||
|
||||
function isRecentPlayerRegistration(player, staleSeconds) {
|
||||
const lastSeenAt = player && player.last_seen_at;
|
||||
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
|
||||
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
|
||||
|
||||
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
|
||||
}
|
||||
|
||||
function normalizeIdentifier(value) {
|
||||
return String(value || '').trim().slice(0, 255);
|
||||
}
|
||||
@@ -143,6 +173,9 @@ async function recordPlayerHeartbeat(pool, options) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeBaseUrl: normalizeBaseUrl,
|
||||
isLocalLikeBaseUrl: isLocalLikeBaseUrl,
|
||||
isRecentPlayerRegistration: isRecentPlayerRegistration,
|
||||
normalizeDeviceId: normalizeDeviceId,
|
||||
normalizeIdentifier: normalizeIdentifier,
|
||||
getConfiguredPlayerIdentifier: getConfiguredPlayerIdentifier,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const { fetchPagedRows, validateMaxLength } = require('./utils');
|
||||
const { fetchPagedRows, validateMaxLength } = require('#src/data/utils');
|
||||
|
||||
const NAME_MAX_LENGTH = 255;
|
||||
const URL_MAX_LENGTH = 1024;
|
||||
|
||||
+61
-4
@@ -1,10 +1,10 @@
|
||||
// Slide data access helpers, including rich-text normalization and payload building.
|
||||
|
||||
const { fetchTemplateById } = require('./templates');
|
||||
const { parseJsonSafe, validateMaxLength } = require('./utils');
|
||||
const { fetchTemplateById } = require('#src/data/templates');
|
||||
const { parseJsonSafe, validateMaxLength } = require('#src/data/utils');
|
||||
|
||||
const TITLE_MAX_LENGTH = 255;
|
||||
const { buildQrCodeContent } = require('./qr-code');
|
||||
const { buildQrCodeContent } = require('#src/data/qr-code');
|
||||
|
||||
const DEFAULT_FONT_SIZE = 32;
|
||||
|
||||
@@ -474,7 +474,64 @@ async function buildTemplateContent(pool, template, body, filesByField, existing
|
||||
font_size: style.font_size,
|
||||
font_color: style.font_color
|
||||
};
|
||||
} else if (!['text', 'image', 'video', 'webpage', 'qr-code', 'html', 'rtmp', 'rss', 'api', 'weather'].includes(String(region.region_type || '').trim())) {
|
||||
} else if (region.region_type === 'chart') {
|
||||
const current = existingContent && existingContent[region.region_key] && typeof existingContent[region.region_key] === 'object' ? existingContent[region.region_key] : {};
|
||||
let manualRows = [];
|
||||
let datasets = [];
|
||||
try {
|
||||
const parsedRows = JSON.parse(String(body[`region_chart_manual_rows_${region.id}`] || '[]'));
|
||||
manualRows = Array.isArray(parsedRows) ? parsedRows.slice(0, 500) : [];
|
||||
} catch (_error) {
|
||||
manualRows = Array.isArray(current.manual_rows) ? current.manual_rows : [];
|
||||
}
|
||||
try {
|
||||
const parsedDatasets = JSON.parse(String(body[`region_chart_datasets_${region.id}`] || '[]'));
|
||||
datasets = Array.isArray(parsedDatasets) ? parsedDatasets.slice(0, 12) : [];
|
||||
} catch (_error) {
|
||||
datasets = Array.isArray(current.datasets) ? current.datasets : [];
|
||||
}
|
||||
const colors = String(body[`region_chart_colors_${region.id}`] === undefined ? (Array.isArray(current.colors) ? current.colors.join(',') : '') : body[`region_chart_colors_${region.id}`] || '')
|
||||
.split(',')
|
||||
.map((color) => String(color || '').trim())
|
||||
.filter((color) => /^#[0-9a-fA-F]{3,8}$/.test(color))
|
||||
.slice(0, 24);
|
||||
const chartSourceKind = String(body[`region_chart_source_kind_${region.id}`] || current.source_kind || 'manual') === 'api' ? 'api' : 'manual';
|
||||
const chartOptions = {
|
||||
legend: ['automatic', 'show', 'hide'].includes(String(body[`region_chart_legend_${region.id}`] || current.chart_options?.legend || 'automatic')) ? String(body[`region_chart_legend_${region.id}`] || current.chart_options?.legend || 'automatic') : 'automatic',
|
||||
begin_at_zero: body[`region_chart_begin_at_zero_control_${region.id}`] === undefined ? current.chart_options?.begin_at_zero !== false : body[`region_chart_begin_at_zero_${region.id}`] === 'on',
|
||||
y_axis_min: String(body[`region_chart_y_axis_min_${region.id}`] === undefined ? current.chart_options?.y_axis_min || '' : body[`region_chart_y_axis_min_${region.id}`] || '').trim(),
|
||||
y_axis_max: String(body[`region_chart_y_axis_max_${region.id}`] === undefined ? current.chart_options?.y_axis_max || '' : body[`region_chart_y_axis_max_${region.id}`] || '').trim(),
|
||||
line_points: ['automatic', 'show', 'hide'].includes(String(body[`region_chart_line_points_${region.id}`] || current.chart_options?.line_points || 'automatic')) ? String(body[`region_chart_line_points_${region.id}`] || current.chart_options?.line_points || 'automatic') : 'automatic',
|
||||
line_fill: body[`region_chart_line_fill_${region.id}`] === 'on',
|
||||
bar_orientation: String(body[`region_chart_bar_orientation_${region.id}`] || current.chart_options?.bar_orientation || 'vertical') === 'horizontal' ? 'horizontal' : 'vertical',
|
||||
value_format: ['number', 'percent', 'currency'].includes(String(body[`region_chart_value_format_${region.id}`] || current.chart_options?.value_format || 'number')) ? String(body[`region_chart_value_format_${region.id}`] || current.chart_options?.value_format || 'number') : 'number',
|
||||
decimal_places: Math.max(0, Math.min(4, Number(body[`region_chart_decimal_places_${region.id}`] === undefined ? current.chart_options?.decimal_places || 0 : body[`region_chart_decimal_places_${region.id}`] || 0) || 0)),
|
||||
text_size: Math.max(8, Math.min(48, Number(body[`region_chart_text_size_${region.id}`] === undefined ? current.chart_options?.text_size || 12 : body[`region_chart_text_size_${region.id}`] || 12) || 12)),
|
||||
grid_color: /^#[0-9a-fA-F]{6}$/.test(String(body[`region_chart_grid_color_${region.id}`] || current.chart_options?.grid_color || '')) ? String(body[`region_chart_grid_color_${region.id}`] || current.chart_options?.grid_color) : '#000000',
|
||||
text_color: /^#[0-9a-fA-F]{6}$/.test(String(body[`region_chart_text_color_${region.id}`] || current.chart_options?.text_color || '')) ? String(body[`region_chart_text_color_${region.id}`] || current.chart_options?.text_color) : '#000000'
|
||||
};
|
||||
const sourceOptions = {
|
||||
sort: ['none', 'reverse-original', 'value-ascending', 'value-descending'].includes(String(body[`region_chart_source_sort_${region.id}`] || current.source_options?.sort || 'none')) ? String(body[`region_chart_source_sort_${region.id}`] || current.source_options?.sort || 'none') : 'none',
|
||||
limit: Math.max(0, Math.min(500, Number(body[`region_chart_source_limit_${region.id}`] === undefined ? current.source_options?.limit || 0 : body[`region_chart_source_limit_${region.id}`] || 0) || 0)),
|
||||
missing: ['skip', 'zero', 'gap'].includes(String(body[`region_chart_source_missing_${region.id}`] || current.source_options?.missing || 'gap')) ? String(body[`region_chart_source_missing_${region.id}`] || current.source_options?.missing || 'gap') : 'gap'
|
||||
};
|
||||
content[region.region_key] = {
|
||||
type: 'chart',
|
||||
chart_type: ['line', 'bar', 'doughnut', 'pie'].includes(String(body[`region_chart_type_${region.id}`] || current.chart_type || 'bar')) ? String(body[`region_chart_type_${region.id}`] || current.chart_type || 'bar') : 'bar',
|
||||
source_kind: chartSourceKind,
|
||||
source_id: chartSourceKind === 'api' ? (body[`region_chart_source_id_${region.id}`] === undefined ? (current.source_id || null) : (body[`region_chart_source_id_${region.id}`] === '' ? null : Number(body[`region_chart_source_id_${region.id}`]))) : null,
|
||||
items_path: String(body[`region_chart_items_path_${region.id}`] === undefined ? current.items_path || '' : body[`region_chart_items_path_${region.id}`] || '').trim(),
|
||||
label_path: String(body[`region_chart_label_path_${region.id}`] === undefined ? current.label_path || '' : body[`region_chart_label_path_${region.id}`] || '').trim(),
|
||||
value_path: String(body[`region_chart_value_path_${region.id}`] === undefined ? current.value_path || '' : body[`region_chart_value_path_${region.id}`] || '').trim(),
|
||||
dataset_label: String(body[`region_chart_dataset_label_${region.id}`] === undefined ? current.dataset_label || 'Value' : body[`region_chart_dataset_label_${region.id}`] || 'Value').trim() || 'Value',
|
||||
color: /^#[0-9a-fA-F]{3,8}$/.test(String(body[`region_chart_color_${region.id}`] || current.color || '')) ? String(body[`region_chart_color_${region.id}`]) : (current.color || '#36a269'),
|
||||
datasets,
|
||||
colors,
|
||||
manual_rows: manualRows,
|
||||
chart_options: chartOptions,
|
||||
source_options: sourceOptions
|
||||
};
|
||||
} else if (!['text', 'image', 'video', 'webpage', 'qr-code', 'html', 'rtmp', 'rss', 'api', 'weather', 'chart'].includes(String(region.region_type || '').trim())) {
|
||||
const current = existingContent && existingContent[region.region_key] && typeof existingContent[region.region_key] === 'object' ? existingContent[region.region_key] : {};
|
||||
const suffix = '_' + region.id;
|
||||
const generic = {};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Template data access helpers and region normalization logic.
|
||||
|
||||
const { parseJsonSafe, readFormArray, validateMaxLength } = require('./utils');
|
||||
const animationPresets = require('../web/public/js/templates/animation-presets');
|
||||
const { parseJsonSafe, readFormArray, validateMaxLength } = require('#src/data/utils');
|
||||
const animationPresets = require('#src/web/public/js/templates/animation-presets');
|
||||
|
||||
const TEMPLATE_NAME_MAX_LENGTH = 255;
|
||||
const REGION_NAME_MAX_LENGTH = 255;
|
||||
@@ -49,7 +49,8 @@ function normalizeBackgroundGradient(value) {
|
||||
|
||||
function normalizeTemplateRegionType(value) {
|
||||
const rawType = String(value || 'text').trim();
|
||||
return rawType || 'text';
|
||||
const normalizedType = rawType.toLowerCase();
|
||||
return ['text', 'image', 'video', 'webpage', 'qr-code', 'html', 'rtmp', 'rss', 'api', 'weather', 'timetable', 'time-date', 'chart'].includes(normalizedType) ? rawType : 'text';
|
||||
}
|
||||
|
||||
function normalizeTemplateRegionLockRatio(value) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Timetable group and entry data access helpers.
|
||||
|
||||
const { fetchPagedRows, validateMaxLength } = require('./utils');
|
||||
const { fetchPagedRows, validateMaxLength } = require('#src/data/utils');
|
||||
|
||||
const NAME_MAX_LENGTH = 255;
|
||||
const DESCRIPTION_MAX_LENGTH = 255;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
// Weather location data access and form normalization.
|
||||
|
||||
const { fetchPagedRows, validateMaxLength } = require('./utils');
|
||||
const { fetchAppSettings } = require('./app-settings');
|
||||
const { fetchPagedRows, validateMaxLength } = require('#src/data/utils');
|
||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
|
||||
const NAME_MAX_LENGTH = 255;
|
||||
const LOCATION_MAX_LENGTH = 255;
|
||||
|
||||
Reference in new issue
Block a user