From c263d97cca36186e5355c0fb4625215e27c9e7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Fri, 27 Mar 2026 20:21:06 +0100 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=C3=9Cbersichtsseite=20=E2=80=93=20?= =?UTF-8?q?globaler=20Override-Banner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../internal/httpapi/manage/templates.go | 62 +++++++++++++++++++ server/backend/internal/httpapi/manage/ui.go | 25 ++++++-- server/backend/internal/httpapi/router.go | 2 +- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/server/backend/internal/httpapi/manage/templates.go b/server/backend/internal/httpapi/manage/templates.go index 6a8beed..1038207 100644 --- a/server/backend/internal/httpapi/manage/templates.go +++ b/server/backend/internal/httpapi/manage/templates.go @@ -1392,6 +1392,29 @@ const screenOverviewTmpl = `

Meine Bildschirme

+ +
+ {{if .GlobalOverride}} +
+ + Alle Monitore {{if eq .GlobalOverride.Type "off"}}ausgeschaltet{{else}}eingeschaltet{{end}} + bis {{.GlobalOverride.Until.Format "02.01.2006 15:04"}} + + +
+ {{else}} +
+ + + +
+ + {{end}} +
{{if gt (len .Cards) 1}}
Alle Displays: @@ -1513,6 +1536,45 @@ function bulkDisplay(state) { }).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(){}); +} ` diff --git a/server/backend/internal/httpapi/manage/ui.go b/server/backend/internal/httpapi/manage/ui.go index 5c94d40..e7dc136 100644 --- a/server/backend/internal/httpapi/manage/ui.go +++ b/server/backend/internal/httpapi/manage/ui.go @@ -281,12 +281,13 @@ func HandleRemoveUserFromScreen(screens *store.ScreenStore) http.HandlerFunc { } type screenCard struct { - Screen *store.Screen - DisplayState string + Screen *store.Screen + DisplayState string + OverrideOnUntil *time.Time } // 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)) return func(w http.ResponseWriter, r *http.Request) { u := reqcontext.UserFromContext(r.Context()) @@ -310,11 +311,23 @@ func HandleScreenOverview(screens *store.ScreenStore, notifier *mqttnotifier.Not cards := make([]screenCard, 0, len(accessible)) for _, sc := range accessible { 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{ - "Cards": cards, - "CSRFToken": csrfToken, + "Cards": cards, + "CSRFToken": csrfToken, + "GlobalOverride": activeOverride, }) } } diff --git a/server/backend/internal/httpapi/router.go b/server/backend/internal/httpapi/router.go index 4dac8c3..f4e1068 100644 --- a/server/backend/internal/httpapi/router.go +++ b/server/backend/internal/httpapi/router.go @@ -165,7 +165,7 @@ func registerManageRoutes(mux *http.ServeMux, d RouterDeps) { // ── Playlist management UI ──────────────────────────────────────────── // authScreen enforces that screen_user only accesses their permitted screens. 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}", authScreen(http.HandlerFunc(manage.HandleManageUI(d.TenantStore, d.ScreenStore, d.ScheduleStore, d.MediaStore, d.PlaylistStore, d.Config, notifier)))) mux.Handle("POST /manage/{screenSlug}/upload",