feat(ui): Übersichtsseite – globaler Override-Banner

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-27 20:21:06 +01:00
parent 9aabf18aa2
commit c263d97cca
3 changed files with 82 additions and 7 deletions

View file

@ -1392,6 +1392,29 @@ const screenOverviewTmpl = `<!DOCTYPE html>
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<h1 class="title is-4 mb-5">Meine Bildschirme</h1> <h1 class="title is-4 mb-5">Meine Bildschirme</h1>
<!-- Globaler Override-Banner -->
<div id="global-override-section" style="margin-bottom:1rem">
{{if .GlobalOverride}}
<div class="notification {{if eq .GlobalOverride.Type "off"}}is-warning{{else}}is-info{{end}} is-light py-2 px-3" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
<span>
Alle Monitore <strong>{{if eq .GlobalOverride.Type "off"}}ausgeschaltet{{else}}eingeschaltet{{end}}</strong>
bis {{.GlobalOverride.Until.Format "02.01.2006 15:04"}}
</span>
<button class="button is-small" type="button" onclick="deleteGlobalOverride()">Override aufheben</button>
</div>
{{else}}
<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center">
<button class="button is-small is-danger is-light" type="button" onclick="showGlobalOverrideForm('off')">Alle ausschalten bis&#x2026;</button>
<button class="button is-small is-success is-light" type="button" onclick="showGlobalOverrideForm('on')">Alle einschalten bis&#x2026;</button>
<span id="override-result" style="font-size:.8rem;color:#6b7280"></span>
</div>
<div id="global-override-form" style="display:none;margin-top:.5rem;gap:.5rem;align-items:center;flex-wrap:wrap">
<input type="datetime-local" id="global-override-until" class="input is-small" style="width:16rem">
<button class="button is-small is-primary" type="button" onclick="setGlobalOverride()">Setzen</button>
<button class="button is-small is-light" type="button" onclick="hideGlobalOverrideForm()">Abbrechen</button>
</div>
{{end}}
</div>
{{if gt (len .Cards) 1}} {{if gt (len .Cards) 1}}
<div class="bulk-bar"> <div class="bulk-bar">
<span style="font-size:.875rem;font-weight:600;color:#374151">Alle Displays:</span> <span style="font-size:.875rem;font-weight:600;color:#374151">Alle Displays:</span>
@ -1513,6 +1536,45 @@ function bulkDisplay(state) {
}).catch(function(){}); }).catch(function(){});
}); });
} }
var _globalOverrideType = '';
function showGlobalOverrideForm(type) {
_globalOverrideType = type;
var form = document.getElementById('global-override-form');
if (form) form.style.display = 'flex';
}
function hideGlobalOverrideForm() {
var form = document.getElementById('global-override-form');
if (form) form.style.display = 'none';
}
function setGlobalOverride() {
var val = document.getElementById('global-override-until').value;
if (!val) {
document.getElementById('override-result').textContent = 'Bitte Datum und Uhrzeit angeben';
return;
}
var dt = new Date(val);
fetch('/api/v1/global-override', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf(), 'X-Requested-With': 'fetch'},
body: JSON.stringify({type: _globalOverrideType, until: dt.toISOString()})
}).then(function(r) {
if (r.ok) { location.reload(); }
else { document.getElementById('override-result').textContent = 'Fehler beim Setzen'; }
}).catch(function() { document.getElementById('override-result').textContent = 'Netzwerkfehler'; });
}
function deleteGlobalOverride() {
fetch('/api/v1/global-override', {
method: 'DELETE',
headers: {'X-CSRF-Token': getCsrf(), 'X-Requested-With': 'fetch'}
}).then(function(r) {
if (r.ok) { location.reload(); }
}).catch(function(){});
}
</script> </script>
</body> </body>
</html>` </html>`

View file

@ -283,10 +283,11 @@ func HandleRemoveUserFromScreen(screens *store.ScreenStore) http.HandlerFunc {
type screenCard struct { type screenCard struct {
Screen *store.Screen Screen *store.Screen
DisplayState string DisplayState string
OverrideOnUntil *time.Time
} }
// HandleScreenOverview renders a card-based overview of all accessible screens for a screen_user. // HandleScreenOverview renders a card-based overview of all accessible screens for a screen_user.
func HandleScreenOverview(screens *store.ScreenStore, notifier *mqttnotifier.Notifier, cfg config.Config) http.HandlerFunc { func HandleScreenOverview(screens *store.ScreenStore, schedules *store.ScreenScheduleStore, overrides *store.GlobalOverrideStore, notifier *mqttnotifier.Notifier, cfg config.Config) http.HandlerFunc {
t := template.Must(template.New("screenOverview").Funcs(tmplFuncs).Parse(screenOverviewTmpl)) t := template.Must(template.New("screenOverview").Funcs(tmplFuncs).Parse(screenOverviewTmpl))
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
u := reqcontext.UserFromContext(r.Context()) u := reqcontext.UserFromContext(r.Context())
@ -310,11 +311,23 @@ func HandleScreenOverview(screens *store.ScreenStore, notifier *mqttnotifier.Not
cards := make([]screenCard, 0, len(accessible)) cards := make([]screenCard, 0, len(accessible))
for _, sc := range accessible { for _, sc := range accessible {
ds, _ := screens.GetDisplayState(r.Context(), sc.ID) ds, _ := screens.GetDisplayState(r.Context(), sc.ID)
cards = append(cards, screenCard{Screen: sc, DisplayState: ds}) sched, _ := schedules.Get(r.Context(), sc.ID)
var overrideOnUntil *time.Time
if sched != nil && sched.OverrideOnUntil != nil && time.Now().Before(*sched.OverrideOnUntil) {
overrideOnUntil = sched.OverrideOnUntil
} }
cards = append(cards, screenCard{Screen: sc, DisplayState: ds, OverrideOnUntil: overrideOnUntil})
}
var activeOverride *store.GlobalOverride
if o, err := overrides.Get(r.Context()); err == nil && o != nil && time.Now().Before(o.Until) {
activeOverride = o
}
renderTemplate(w, t, map[string]any{ renderTemplate(w, t, map[string]any{
"Cards": cards, "Cards": cards,
"CSRFToken": csrfToken, "CSRFToken": csrfToken,
"GlobalOverride": activeOverride,
}) })
} }
} }

View file

@ -165,7 +165,7 @@ func registerManageRoutes(mux *http.ServeMux, d RouterDeps) {
// ── Playlist management UI ──────────────────────────────────────────── // ── Playlist management UI ────────────────────────────────────────────
// authScreen enforces that screen_user only accesses their permitted screens. // authScreen enforces that screen_user only accesses their permitted screens.
mux.Handle("GET /manage", mux.Handle("GET /manage",
authOnly(http.HandlerFunc(manage.HandleScreenOverview(d.ScreenStore, notifier, d.Config)))) authOnly(http.HandlerFunc(manage.HandleScreenOverview(d.ScreenStore, d.ScheduleStore, d.GlobalOverrideStore, notifier, d.Config))))
mux.Handle("GET /manage/{screenSlug}", mux.Handle("GET /manage/{screenSlug}",
authScreen(http.HandlerFunc(manage.HandleManageUI(d.TenantStore, d.ScreenStore, d.ScheduleStore, d.MediaStore, d.PlaylistStore, d.Config, notifier)))) authScreen(http.HandlerFunc(manage.HandleManageUI(d.TenantStore, d.ScreenStore, d.ScheduleStore, d.MediaStore, d.PlaylistStore, d.Config, notifier))))
mux.Handle("POST /manage/{screenSlug}/upload", mux.Handle("POST /manage/{screenSlug}/upload",