Fix: Transition-Race, Auto-Reload nach Deploy, Playlist-Latenz < 1s
- hideAllContent() prüft opacity bevor display=none gesetzt wird (verhindert Race mit displayItem) - Neuer /api/startup-token Endpoint: Browser erkennt Agent-Neustart und reloaded automatisch - MQTT-Debounce von 3s auf 500ms, Browser-Poll von 30s auf 5s reduziert für sub-sekunden Playlist-Updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
585cb83ed0
commit
6931181916
2 changed files with 57 additions and 7 deletions
|
|
@ -12,7 +12,8 @@ import (
|
||||||
const (
|
const (
|
||||||
// debounceDuration is the minimum interval between two callback invocations.
|
// debounceDuration is the minimum interval between two callback invocations.
|
||||||
// Any MQTT message arriving while the timer is still running resets it.
|
// Any MQTT message arriving while the timer is still running resets it.
|
||||||
debounceDuration = 3 * time.Second
|
// 500ms reicht aus um Bursts zu absorbieren, ohne die Latenz merklich zu erhöhen.
|
||||||
|
debounceDuration = 500 * time.Millisecond
|
||||||
|
|
||||||
// playlistChangedTopicTemplate is the topic the backend publishes to.
|
// playlistChangedTopicTemplate is the topic the backend publishes to.
|
||||||
playlistChangedTopic = "signage/screen/%s/playlist-changed"
|
playlistChangedTopic = "signage/screen/%s/playlist-changed"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -48,14 +49,16 @@ type SysInfo struct {
|
||||||
|
|
||||||
// Server serves the local player UI to Chromium.
|
// Server serves the local player UI to Chromium.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
listenAddr string
|
listenAddr string
|
||||||
nowFn func() NowPlaying
|
nowFn func() NowPlaying
|
||||||
|
startupToken string // zufälliger Token der sich bei jedem Start ändert
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a Server. listenAddr is e.g. "127.0.0.1:8090".
|
// New creates a Server. listenAddr is e.g. "127.0.0.1:8090".
|
||||||
// nowFn is called on each request and returns the current playback state.
|
// nowFn is called on each request and returns the current playback state.
|
||||||
func New(listenAddr string, nowFn func() NowPlaying) *Server {
|
func New(listenAddr string, nowFn func() NowPlaying) *Server {
|
||||||
return &Server{listenAddr: listenAddr, nowFn: nowFn}
|
token := fmt.Sprintf("%016x", rand.Uint64())
|
||||||
|
return &Server{listenAddr: listenAddr, nowFn: nowFn, startupToken: token}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the HTTP server and blocks until ctx is cancelled.
|
// Run starts the HTTP server and blocks until ctx is cancelled.
|
||||||
|
|
@ -69,6 +72,7 @@ func (s *Server) Run(ctx context.Context) error {
|
||||||
mux.HandleFunc("GET /player", s.handlePlayer)
|
mux.HandleFunc("GET /player", s.handlePlayer)
|
||||||
mux.HandleFunc("GET /api/now-playing", s.handleNowPlaying)
|
mux.HandleFunc("GET /api/now-playing", s.handleNowPlaying)
|
||||||
mux.HandleFunc("GET /api/sysinfo", handleSysInfo)
|
mux.HandleFunc("GET /api/sysinfo", handleSysInfo)
|
||||||
|
mux.HandleFunc("GET /api/startup-token", s.handleStartupToken)
|
||||||
mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub))))
|
mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub))))
|
||||||
|
|
||||||
srv := &http.Server{Handler: mux}
|
srv := &http.Server{Handler: mux}
|
||||||
|
|
@ -99,6 +103,14 @@ func (s *Server) handleNowPlaying(w http.ResponseWriter, _ *http.Request) {
|
||||||
json.NewEncoder(w).Encode(s.nowFn()) //nolint:errcheck
|
json.NewEncoder(w).Encode(s.nowFn()) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleStartupToken gibt einen zufälligen Token zurück der sich bei jedem
|
||||||
|
// Agent-Start ändert. Der Browser erkennt daran, dass der Agent neu gestartet
|
||||||
|
// wurde und lädt die Seite automatisch neu.
|
||||||
|
func (s *Server) handleStartupToken(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"token": s.startupToken}) //nolint:errcheck
|
||||||
|
}
|
||||||
|
|
||||||
func handleSysInfo(w http.ResponseWriter, _ *http.Request) {
|
func handleSysInfo(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(collectSysInfo()) //nolint:errcheck
|
json.NewEncoder(w).Encode(collectSysInfo()) //nolint:errcheck
|
||||||
|
|
@ -326,6 +338,11 @@ const playerHTML = `<!DOCTYPE html>
|
||||||
// Versteckt alle Content-Elemente vor dem Anzeigen des richtigen Typs.
|
// Versteckt alle Content-Elemente vor dem Anzeigen des richtigen Typs.
|
||||||
// Blendet zunächst auf opacity:0 aus und entfernt display erst nach der
|
// Blendet zunächst auf opacity:0 aus und entfernt display erst nach der
|
||||||
// Transition (500ms), damit der Fade-Out sichtbar ist.
|
// Transition (500ms), damit der Fade-Out sichtbar ist.
|
||||||
|
//
|
||||||
|
// Race-Condition-Fix: Das setTimeout-Callback prüft vor dem display=none,
|
||||||
|
// ob das Element noch opacity=0 hat. Falls displayItem() das Element
|
||||||
|
// inzwischen wieder auf display=block+opacity=1 gesetzt hat, wird es
|
||||||
|
// nicht fälschlicherweise versteckt.
|
||||||
function hideAllContent() {
|
function hideAllContent() {
|
||||||
// Laufendes Video sofort stoppen damit kein Audio weiterläuft.
|
// Laufendes Video sofort stoppen damit kein Audio weiterläuft.
|
||||||
videoView.pause();
|
videoView.pause();
|
||||||
|
|
@ -335,7 +352,14 @@ const playerHTML = `<!DOCTYPE html>
|
||||||
if (el.style.display !== 'none') {
|
if (el.style.display !== 'none') {
|
||||||
el.style.opacity = '0';
|
el.style.opacity = '0';
|
||||||
(function(e) {
|
(function(e) {
|
||||||
setTimeout(function() { e.style.display = 'none'; }, 500);
|
setTimeout(function() {
|
||||||
|
// Nur verstecken wenn das Element noch ausgeblendet ist
|
||||||
|
// (opacity=0 oder leer). Falls displayItem() es inzwischen
|
||||||
|
// wieder sichtbar gemacht hat, nicht anfassen.
|
||||||
|
if (e.style.opacity === '0' || e.style.opacity === '') {
|
||||||
|
e.style.display = 'none';
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
})(el);
|
})(el);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -533,10 +557,11 @@ const playerHTML = `<!DOCTYPE html>
|
||||||
|
|
||||||
function startSlowPoll() {
|
function startSlowPoll() {
|
||||||
if (slowPollInterval) return;
|
if (slowPollInterval) return;
|
||||||
|
// Playlist alle 5s prüfen (fängt MQTT-getriggerte Backend-Änderungen schnell ab).
|
||||||
|
// Sysinfo läuft separat alle 30s (weiter unten).
|
||||||
slowPollInterval = setInterval(function() {
|
slowPollInterval = setInterval(function() {
|
||||||
pollNowPlaying();
|
pollNowPlaying();
|
||||||
pollSysInfo();
|
}, 5000);
|
||||||
}, 30000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pollNowPlaying() {
|
function pollNowPlaying() {
|
||||||
|
|
@ -567,6 +592,30 @@ const playerHTML = `<!DOCTYPE html>
|
||||||
startSlowPoll();
|
startSlowPoll();
|
||||||
}
|
}
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
|
// ── Auto-Reload bei Agent-Neustart ───────────────────────────────
|
||||||
|
// Der Agent gibt bei jedem Start einen neuen zufälligen Token zurück.
|
||||||
|
// Falls sich der Token ändert, hat der Agent neu gestartet → Seite neu laden.
|
||||||
|
var knownStartupToken = null;
|
||||||
|
|
||||||
|
function pollStartupToken() {
|
||||||
|
fetch('/api/startup-token')
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (!d || !d.token) return;
|
||||||
|
if (knownStartupToken === null) {
|
||||||
|
// Erster Aufruf: Token merken, kein Reload.
|
||||||
|
knownStartupToken = d.token;
|
||||||
|
} else if (knownStartupToken !== d.token) {
|
||||||
|
// Token hat sich geändert → Agent wurde neu gestartet.
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function() {}); // Agent offline → ignorieren
|
||||||
|
}
|
||||||
|
|
||||||
|
pollStartupToken();
|
||||||
|
setInterval(pollStartupToken, 5000); // alle 5s prüfen
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue