Release 1.6.2

This commit is contained in:
2026-01-26 15:34:52 +01:00
parent c5aa8a5156
commit 0c2720618a
5 changed files with 71 additions and 10 deletions

View File

@@ -210,8 +210,10 @@ Per-display option:
Implementation notes: Implementation notes:
- Headlines are fetched server-side via `GET /api/display/<token>/ticker` and cached briefly. - Headlines are fetched server-side via `GET /api/display/<token>/ticker` and cached in-memory.
- The player reads the company ticker settings via `GET /api/display/<token>/playlist` and refreshes headlines periodically. - The player reads the company ticker settings via `GET /api/display/<token>/playlist`.
- The player auto-refreshes headlines without restart on a **long interval** (default: **12 hours**, override via `?ticker_poll=seconds`).
- Server-side cache TTL defaults to **6 hours** (override via env var `TICKER_CACHE_TTL_SECONDS`).
## SMTP / Forgot password ## SMTP / Forgot password
@@ -290,5 +292,6 @@ If the reset email is not received:

View File

@@ -1,6 +1,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import hashlib import hashlib
import json import json
import os
import time import time
import re import re
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -20,7 +21,18 @@ MAX_ACTIVE_SESSIONS_PER_DISPLAY = 3
SESSION_TTL_SECONDS = 90 SESSION_TTL_SECONDS = 90
# RSS ticker cache (in-memory; OK for this small app; avoids hammering feeds) # RSS ticker cache (in-memory; OK for this small app; avoids hammering feeds)
TICKER_CACHE_TTL_SECONDS = 120 #
# Default is intentionally long because displays can refresh headlines on a long interval
# (e.g., twice per day) and we don't want many displays to re-fetch the same feed.
# Override via env var `TICKER_CACHE_TTL_SECONDS`.
def _env_int(name: str, default: int) -> int:
try:
return int(os.environ.get(name, "") or default)
except (TypeError, ValueError):
return default
TICKER_CACHE_TTL_SECONDS = max(10, _env_int("TICKER_CACHE_TTL_SECONDS", 6 * 60 * 60))
_TICKER_CACHE: dict[str, dict] = {} _TICKER_CACHE: dict[str, dict] = {}

View File

@@ -280,6 +280,13 @@ h1, h2, h3, .display-1, .display-2, .display-3 {
background: #000; background: #000;
} }
/* Mobile: dashboard display previews are heavy (iframes). Hide them on small screens. */
@media (max-width: 768px) {
.display-gallery-card .display-preview {
display: none;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.display-gallery-grid { .display-gallery-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;

View File

@@ -76,7 +76,8 @@
<iframe <iframe
title="Preview — {{ d.name }}" title="Preview — {{ d.name }}"
data-display-id="{{ d.id }}" data-display-id="{{ d.id }}"
src="{{ url_for('display.display_player', token=d.token) }}?preview=1" class="js-display-preview"
data-preview-src="{{ url_for('display.display_player', token=d.token) }}?preview=1"
loading="lazy" loading="lazy"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
style="width: 100%; height: 100%; border: 0;" style="width: 100%; height: 100%; border: 0;"
@@ -241,17 +242,23 @@
function refreshPreviewIframe(displayId) { function refreshPreviewIframe(displayId) {
const iframe = document.querySelector(`iframe[data-display-id="${displayId}"]`); const iframe = document.querySelector(`iframe[data-display-id="${displayId}"]`);
if (!iframe || !iframe.src) return; // Previews are disabled on mobile.
if (window.matchMedia && window.matchMedia('(max-width: 768px)').matches) return;
if (!iframe) return;
try { try {
const u = new URL(iframe.src, window.location.origin); const baseSrc = iframe.dataset.previewSrc || iframe.src;
if (!baseSrc) return;
const u = new URL(baseSrc, window.location.origin);
// Ensure preview flag is present (and bust cache). // Ensure preview flag is present (and bust cache).
u.searchParams.set('preview', '1'); u.searchParams.set('preview', '1');
u.searchParams.set('_ts', String(Date.now())); u.searchParams.set('_ts', String(Date.now()));
iframe.src = u.toString(); iframe.src = u.toString();
} catch (e) { } catch (e) {
// Fallback: naive cache buster // Fallback: naive cache buster
const sep = iframe.src.includes('?') ? '&' : '?'; const baseSrc = iframe.dataset.previewSrc || iframe.src;
iframe.src = `${iframe.src}${sep}_ts=${Date.now()}`; if (!baseSrc) return;
const sep = baseSrc.includes('?') ? '&' : '?';
iframe.src = `${baseSrc}${sep}_ts=${Date.now()}`;
} }
} }
@@ -467,6 +474,31 @@
if (plSaveBtn) { if (plSaveBtn) {
plSaveBtn.addEventListener('click', savePlaylists); plSaveBtn.addEventListener('click', savePlaylists);
} }
// Disable dashboard previews on small screens (mobile): don't even set iframe src.
function loadDashboardPreviewsIfDesktop() {
const isMobile = window.matchMedia && window.matchMedia('(max-width: 768px)').matches;
if (isMobile) return;
document.querySelectorAll('iframe.js-display-preview[data-preview-src]').forEach((iframe) => {
if (!iframe.src || iframe.src === 'about:blank') {
iframe.src = iframe.dataset.previewSrc;
}
});
}
loadDashboardPreviewsIfDesktop();
// If user rotates/resizes from mobile -> desktop, load previews then.
if (window.matchMedia) {
const mql = window.matchMedia('(max-width: 768px)');
const onChange = () => {
if (!mql.matches) loadDashboardPreviewsIfDesktop();
};
if (typeof mql.addEventListener === 'function') {
mql.addEventListener('change', onChange);
} else if (typeof mql.addListener === 'function') {
mql.addListener(onChange);
}
}
})(); })();
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -203,6 +203,14 @@
let tickerInterval = null; let tickerInterval = null;
let tickerLastHeadlines = []; let tickerLastHeadlines = [];
function getTickerPollSeconds() {
// Refresh headlines on a long interval.
// Default: 12 hours (twice per day).
// Override via ?ticker_poll=seconds.
const tp = parseInt(new URLSearchParams(window.location.search).get('ticker_poll') || '', 10);
return Number.isFinite(tp) && tp > 0 ? tp : (12 * 60 * 60);
}
const ANIM_MS = 420; const ANIM_MS = 420;
function getTransitionMode(pl) { function getTransitionMode(pl) {
@@ -391,8 +399,7 @@
clearInterval(tickerInterval); clearInterval(tickerInterval);
tickerInterval = null; tickerInterval = null;
} }
// Refresh every 2 minutes; server caches too. tickerInterval = setInterval(refreshTickerOnce, getTickerPollSeconds() * 1000);
tickerInterval = setInterval(refreshTickerOnce, 120 * 1000);
} }
function clearStage() { function clearStage() {