feat(ui): Display-Buttons und Sammelschalter in Screen-Übersicht

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-27 07:10:40 +01:00
parent 68fc0bf4cf
commit bdd99d10bd

View file

@ -1304,6 +1304,12 @@ const screenOverviewTmpl = `<!DOCTYPE html>
.morz-toast { position:fixed; top:1rem; right:1rem; z-index:9999; max-width:380px; border-radius:24px; box-shadow:var(--shadow-md); padding:.75rem 1.25rem; display:flex; align-items:center; gap:.75rem; font-size:.9rem; transform:translateX(120%); transition:transform .25s ease; } .morz-toast { position:fixed; top:1rem; right:1rem; z-index:9999; max-width:380px; border-radius:24px; box-shadow:var(--shadow-md); padding:.75rem 1.25rem; display:flex; align-items:center; gap:.75rem; font-size:.9rem; transform:translateX(120%); transition:transform .25s ease; }
.morz-toast.show { transform:translateX(0); } .morz-toast.show { transform:translateX(0); }
.morz-toast.is-success { background:#f0fdf4; color:#166534; border:1px solid #bbf7d0; } .morz-toast.is-success { background:#f0fdf4; color:#166534; border:1px solid #bbf7d0; }
.display-btn-row { display:flex; gap:.4rem; margin-top:.5rem; }
.bulk-bar { background:var(--surface); border-radius:var(--radius); box-shadow:var(--shadow-sm); padding:.85rem 1rem; margin-bottom:1.25rem; display:flex; align-items:center; gap:.75rem; flex-wrap:wrap; }
.display-state-badge { font-size:.7rem; padding:.15em .55em; border-radius:99px; font-weight:700; }
.display-state-badge.on { background:#dcfce7; color:#166534; }
.display-state-badge.off { background:#fee2e2; color:#991b1b; }
.display-state-badge.unknown { background:#f3f4f6; color:#6b7280; }
</style> </style>
</head> </head>
<body> <body>
@ -1326,6 +1332,14 @@ 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>
{{if gt (len .Cards) 1}}
<div class="bulk-bar">
<span style="font-size:.875rem;font-weight:600;color:#374151">Alle Displays:</span>
<button class="button is-small is-success is-light" type="button" onclick="bulkDisplay('on')">Alle einschalten</button>
<button class="button is-small is-danger is-light" type="button" onclick="bulkDisplay('off')">Alle ausschalten</button>
<span id="bulk-result" style="font-size:.8rem;color:#6b7280"></span>
</div>
{{end}}
<div class="columns is-multiline"> <div class="columns is-multiline">
{{range .Cards}} {{range .Cards}}
<div class="column is-one-third-desktop is-half-tablet"> <div class="column is-one-third-desktop is-half-tablet">
@ -1341,6 +1355,15 @@ const screenOverviewTmpl = `<!DOCTYPE html>
</div> </div>
<div class="screen-card-sub">{{orientationLabel .Screen.Orientation}} · {{.Screen.Slug}}</div> <div class="screen-card-sub">{{orientationLabel .Screen.Orientation}} · {{.Screen.Slug}}</div>
<a class="button is-primary is-fullwidth" href="/manage/{{.Screen.Slug}}">Verwalten </a> <a class="button is-primary is-fullwidth" href="/manage/{{.Screen.Slug}}">Verwalten </a>
<div class="display-btn-row">
<span id="ds-{{.Screen.Slug}}" class="display-state-badge {{.DisplayState}}">
{{if eq .DisplayState "on"}}An{{else if eq .DisplayState "off"}}Aus{{else}}?{{end}}
</span>
<button class="button is-small is-success is-light" type="button"
onclick="sendDisplayCmd('{{.Screen.Slug}}','on')">Ein</button>
<button class="button is-small is-danger is-light" type="button"
onclick="sendDisplayCmd('{{.Screen.Slug}}','off')">Aus</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -1381,6 +1404,55 @@ function injectCSRF() {
}); });
} }
if (document.readyState==='loading') document.addEventListener('DOMContentLoaded',injectCSRF); else injectCSRF(); if (document.readyState==='loading') document.addEventListener('DOMContentLoaded',injectCSRF); else injectCSRF();
// ─── Display control ─────────────────────────────────────────────
function sendDisplayCmd(slug, state) {
fetch('/api/v1/screens/' + slug + '/display', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrf(),
'X-Requested-With': 'fetch'
},
body: JSON.stringify({state: state})
}).then(function(r) {
var badge = document.getElementById('ds-' + slug);
if (r.ok && badge) {
badge.className = 'display-state-badge ' + state;
badge.textContent = state === 'on' ? 'An' : 'Aus';
}
}).catch(function(){});
}
function bulkDisplay(state) {
var slugs = [];
document.querySelectorAll('[id^="ds-"]').forEach(function(el) {
slugs.push(el.id.replace('ds-', ''));
});
var result = document.getElementById('bulk-result');
var done = 0;
slugs.forEach(function(slug) {
fetch('/api/v1/screens/' + slug + '/display', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrf(),
'X-Requested-With': 'fetch'
},
body: JSON.stringify({state: state})
}).then(function(r) {
if (r.ok) {
var badge = document.getElementById('ds-' + slug);
if (badge) {
badge.className = 'display-state-badge ' + state;
badge.textContent = state === 'on' ? 'An' : 'Aus';
}
done++;
if (result) result.textContent = done + '/' + slugs.length + ' geschaltet';
}
}).catch(function(){});
});
}
</script> </script>
</body> </body>
</html>` </html>`