Release 1.6.2
This commit is contained in:
@@ -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:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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] = {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user