Display CMS: Upload-Fehler abfangen und kaputte Bilder im Player überspringen.

Mediathek-Uploads legen keinen Datensatz mehr an, wenn der Datei-Write
fehlschlägt (verhindert 404-Einträge auf den Displays). Der Player
überspringt Bilder mit Ladefehler nach 2s statt Schwarzbild zu zeigen.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Adametz 2026-07-07 08:57:31 +00:00
parent 2a34adc515
commit 193d37b1ce
6 changed files with 211 additions and 62 deletions

View file

@ -562,6 +562,37 @@ function escapeHtml(value) {
return div.innerHTML;
}
function detectDisplayPortalBaseUrl() {
const { protocol, hostname, port } = window.location;
const portSuffix = port ? `:${port}` : '';
if (hostname === 'cabinet.b2in.eu') {
return 'https://portal.b2in.eu';
}
if (hostname.startsWith('cabinet.')) {
return `${protocol}//${hostname.replace(/^cabinet\./, 'portal.')}${portSuffix}`;
}
return window.location.origin;
}
const DISPLAY_PORTAL_BASE_URL = detectDisplayPortalBaseUrl();
function resolveDisplayUrl(url) {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://')) return url;
if (url.startsWith('/_cabinet/')) return url;
if (url.startsWith('/') && DISPLAY_PORTAL_BASE_URL !== window.location.origin) {
return `${DISPLAY_PORTAL_BASE_URL}${url}`;
}
if (url.startsWith('/')) return url;
if (url.startsWith('../assets/')) return `/_cabinet/assets/${url.slice('../assets/'.length)}`;
if (!url.includes('/') && url.endsWith('.svg')) return `/_cabinet/assets/${url}`;
if (url.startsWith('../')) return url;
return `../${url}`;
}
function normalizeQrUrl(url) {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://')) return url;
@ -583,7 +614,9 @@ function qrCodeImageUrl(url, size = 300, margin = 8, extraParams = {}) {
}
function setQrImage(img, url, options = {}) {
const baseSrc = options.imageUrl || qrCodeImageUrl(url, options.size || 300, options.margin || 8, options.params || {});
const baseSrc = options.imageUrl
? resolveDisplayUrl(options.imageUrl)
: qrCodeImageUrl(url, options.size || 300, options.margin || 8, options.params || {});
if (!img || !baseSrc) return false;
const maxAttempts = options.maxAttempts || 3;
@ -636,7 +669,7 @@ class DisplayPlayer {
this.displayId = this.detectDisplayId();
// API
this.BASE_URL = this.detectBaseUrl();
this.BASE_URL = DISPLAY_PORTAL_BASE_URL;
this.API_CONFIG = this.detectConfigUrl();
this.API_CHECK = this.detectCheckUrl();
this.API_OVERVIEW = `${this.BASE_URL}/api/display/overview`;
@ -762,11 +795,7 @@ class DisplayPlayer {
}
detectBaseUrl() {
const hostname = window.location.hostname;
if (hostname === 'cabinet.b2in.eu') {
return 'https://portal.b2in.eu';
}
return window.location.origin;
return DISPLAY_PORTAL_BASE_URL;
}
// ========================================
@ -779,27 +808,25 @@ class DisplayPlayer {
try {
if (!this.displayId && !this.previewToken && !this.moduleId) {
await this.fetchOverview();
return;
} else {
await this.fetchConfig();
if (this.playlist.length === 0) {
this.showError('Keine Versionen zugewiesen');
this.scheduleRetry();
} else {
this.hideLoading();
this.startPlaylist();
}
}
await this.fetchConfig();
if (this.playlist.length === 0) {
this.showError('Keine Versionen zugewiesen');
this.scheduleRetry();
return;
}
this.hideLoading();
this.startPlaylist();
} catch (error) {
console.error('[Display] Init failed:', error);
this.showError('Konfiguration konnte nicht geladen werden');
this.scheduleRetry();
}
this.startPolling();
this.recordFileVersion();
this.startConfigPolling();
this.startFileVersionPolling();
this.scheduleAutoReload();
}
@ -832,7 +859,7 @@ class DisplayPlayer {
this.renderOverview(data.displays || []);
}
startPolling() {
startConfigPolling() {
if (!this.API_CHECK) {
return;
}
@ -844,7 +871,7 @@ class DisplayPlayer {
const data = await response.json();
if (data.updated_at !== this.cachedTimestamp) {
console.log('[Display] Change detected, reloading...');
console.log('[Display] Playlist geändert – Seite wird neu geladen');
location.reload();
}
} catch (error) {
@ -857,17 +884,33 @@ class DisplayPlayer {
}
}
}, this.POLL_INTERVAL);
}
startFileVersionPolling() {
this.recordFileVersion();
setInterval(() => this.checkFileVersion(), this.FILE_CHECK_INTERVAL);
}
playerFileCheckUrl() {
const url = new URL(window.location.href);
url.searchParams.set('_player_filecheck', '1');
return url.toString();
}
readPlayerFileVersion(response) {
return response.headers.get('ETag')
|| response.headers.get('Last-Modified')
|| response.headers.get('Content-Length');
}
async recordFileVersion() {
try {
const response = await fetch(window.location.href, {
const response = await fetch(this.playerFileCheckUrl(), {
method: 'HEAD',
cache: 'no-store',
});
this.fileVersion = response.headers.get('ETag') || response.headers.get('Last-Modified');
this.fileVersion = this.readPlayerFileVersion(response);
} catch (e) {
// Ignorieren – wird beim nächsten Check erneut versucht
}
@ -875,14 +918,23 @@ class DisplayPlayer {
async checkFileVersion() {
try {
const response = await fetch(window.location.href, {
const response = await fetch(this.playerFileCheckUrl(), {
method: 'HEAD',
cache: 'no-store',
});
const version = response.headers.get('ETag') || response.headers.get('Last-Modified');
const version = this.readPlayerFileVersion(response);
if (this.fileVersion && version && version !== this.fileVersion) {
console.log('[Display] Datei geändert – Seite wird neu geladen');
if (!version) {
return;
}
if (this.fileVersion === null) {
this.fileVersion = version;
return;
}
if (version !== this.fileVersion) {
console.log('[Display] Player-Datei geändert – Seite wird neu geladen');
location.reload();
}
} catch (e) {
@ -1238,12 +1290,7 @@ class VideoDisplayRenderer {
}
resolveAssetUrl(src) {
if (!src) return '';
if (src.startsWith('http')) return src;
if (src.startsWith('/')) return src;
if (src.startsWith('../')) return src;
// Assets relative to _cabinet
return `../${src}`;
return resolveDisplayUrl(src);
}
destroy() {
@ -1451,15 +1498,31 @@ class B2inRenderer {
// Image
const img = document.createElement('img');
img.style.cssText = 'width:100%;height:100%;object-fit:cover;';
let handled = false;
const finish = () => {
if (handled || this.destroyed) return;
handled = true;
clearTimeout(this.itemTimer);
this.showItem(index + 1);
};
// Broken image (e.g. deleted media, 404): skip after a short grace
// period instead of showing a black frame for the full duration.
// The delay also prevents a rapid loop when every image is broken.
img.onerror = () => {
console.warn(`[B2in] Image error: ${item.media_url}`);
this.hideProgress();
setTimeout(finish, 2000);
};
img.src = this.resolveUrl(item.media_url);
incoming.appendChild(img);
const duration = (item.duration_seconds || this.settings.default_image_duration || 10) * 1000;
this.showProgress(duration);
this.itemTimer = setTimeout(() => {
if (!this.destroyed) this.showItem(index + 1);
}, duration);
this.itemTimer = setTimeout(finish, duration);
}
// Crossfade
@ -1484,13 +1547,7 @@ class B2inRenderer {
}
resolveUrl(url) {
if (!url) return '';
if (url.startsWith('http')) return url;
if (url.startsWith('/')) return url;
if (url.startsWith('../assets/')) return `/_cabinet/assets/${url.slice('../assets/'.length)}`;
if (!url.includes('/') && url.endsWith('.svg')) return `/_cabinet/assets/${url}`;
if (url.startsWith('../')) return url;
return `../${url}`;
return resolveDisplayUrl(url);
}
destroy() {
@ -1856,13 +1913,7 @@ class OffersRenderer {
}
resolveUrl(url) {
if (!url) return '';
if (url.startsWith('http')) return url;
if (url.startsWith('/')) return url;
if (url.startsWith('../assets/')) return `/_cabinet/assets/${url.slice('../assets/'.length)}`;
if (!url.includes('/') && url.endsWith('.svg')) return `/_cabinet/assets/${url}`;
if (url.startsWith('../')) return url;
return `../${url}`;
return resolveDisplayUrl(url);
}
escapeHtml(str) {