feat(ui): Admin-Dashboard neu gestaltet (Karten-Grid, Tabs, Modals)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
41e12d1235
commit
a691186d9a
1 changed files with 345 additions and 473 deletions
|
|
@ -197,30 +197,70 @@ function dlFile(content, name) {
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
const adminTmpl = `<!DOCTYPE html>
|
const adminTmpl = `<!DOCTYPE html>
|
||||||
<html lang="de" data-theme="light">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="color-scheme" content="light">
|
<title>Admin – MORZ Infoboard</title>
|
||||||
<title>MORZ Infoboard – Admin</title>
|
|
||||||
<link rel="stylesheet" href="/static/bulma.min.css">
|
<link rel="stylesheet" href="/static/bulma.min.css">
|
||||||
<style>
|
<style>
|
||||||
body { background: #f5f5f5; }
|
:root { --morz-red:#E30613; --morz-red-dark:#B8000F; --nav-bg:#1A1A2E; --bg:#F7F8FA; --surface:#FFFFFF; --shadow-sm:0 1px 4px rgba(0,0,0,.08); --shadow-md:0 4px 16px rgba(0,0,0,.12); --radius:8px; --radius-btn:6px; }
|
||||||
.tab-panel { display: none; }
|
body { background:var(--bg); font-family:system-ui,-apple-system,"Segoe UI",sans-serif; }
|
||||||
.tab-panel.is-active { display: block; }
|
.navbar { background:var(--nav-bg) !important; }
|
||||||
|
.navbar-item,.navbar-link { color:rgba(255,255,255,.85) !important; }
|
||||||
|
.navbar-item:hover,.navbar-link:hover { background:rgba(255,255,255,.08) !important; color:#fff !important; }
|
||||||
|
.morz-brand .accent { color:var(--morz-red); font-weight:800; }
|
||||||
|
.box { border-radius:var(--radius); box-shadow:var(--shadow-sm); }
|
||||||
|
.button.is-primary { background:var(--morz-red) !important; border-color:var(--morz-red) !important; border-radius:var(--radius-btn); }
|
||||||
|
.button.is-primary:hover { background:var(--morz-red-dark) !important; border-color:var(--morz-red-dark) !important; }
|
||||||
|
.button { border-radius:var(--radius-btn) !important; }
|
||||||
|
/* Underline tabs */
|
||||||
|
.tabs li.is-active a { border-bottom:3px solid var(--morz-red) !important; color:var(--morz-red) !important; font-weight:600; }
|
||||||
|
.tabs a { border-bottom:3px solid transparent; color:#374151; }
|
||||||
|
.tab-panel { display:none; }
|
||||||
|
.tab-panel.is-active { display:block; }
|
||||||
|
/* Screen cards */
|
||||||
|
.screen-card { background:var(--surface); border-radius:var(--radius); box-shadow:var(--shadow-sm); padding:1.25rem; display:flex; flex-direction:column; gap:.75rem; transition:box-shadow .15s; }
|
||||||
|
.screen-card:hover { box-shadow:var(--shadow-md); }
|
||||||
|
.screen-card-name { font-weight:700; font-size:1rem; display:flex; align-items:center; gap:.5rem; }
|
||||||
|
.screen-card-slug { font-family:monospace; font-size:.8rem; color:#6b7280; }
|
||||||
|
.screen-card-meta { display:flex; align-items:center; gap:.75rem; flex-wrap:wrap; }
|
||||||
|
.screen-card-actions { display:flex; gap:.5rem; margin-top:auto; }
|
||||||
|
.orient-badge { font-size:.7rem; background:#f3f4f6; color:#374151; padding:.2em .6em; border-radius:99px; font-weight:600; }
|
||||||
|
.status-dot { width:10px; height:10px; border-radius:50%; display:inline-block; }
|
||||||
|
.status-dot.online { background:#22c55e; }
|
||||||
|
.status-dot.stale { background:#f59e0b; }
|
||||||
|
.status-dot.offline { background:#ef4444; }
|
||||||
|
.status-dot.unknown { background:#9ca3af; }
|
||||||
|
/* Modal */
|
||||||
|
.modal-card { border-radius:var(--radius); overflow:hidden; }
|
||||||
|
.modal-card-head { background:var(--nav-bg); }
|
||||||
|
.modal-card-title { color:#fff; font-weight:700; }
|
||||||
|
.modal-card-head .delete { background:rgba(255,255,255,.2); }
|
||||||
|
/* Toasts */
|
||||||
|
.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.is-success { background:#f0fdf4; color:#166534; border:1px solid #bbf7d0; }
|
||||||
|
.morz-toast.is-danger { background:#fef2f2; color:#991b1b; border:1px solid #fecaca; }
|
||||||
|
.morz-toast.is-warning { background:#fffbeb; color:#92400e; border:1px solid #fde68a; }
|
||||||
|
/* Form polish */
|
||||||
|
.input:focus,.select select:focus { border-color:var(--morz-red); box-shadow:0 0 0 3px rgba(227,6,19,.12); outline:none; }
|
||||||
|
details summary { cursor:pointer; user-select:none; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
|
|
||||||
|
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<span class="navbar-item"><strong>📺 MORZ Infoboard</strong></span>
|
<span class="navbar-item morz-brand"><span class="accent">MORZ</span> Infoboard</span>
|
||||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="adminNavbar">
|
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="adminNav">
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="adminNavbar" class="navbar-menu">
|
<div id="adminNav" class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="/status">Diagnose</a>
|
<a class="navbar-item" href="/status">Diagnose</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -228,306 +268,165 @@ const adminTmpl = `<!DOCTYPE html>
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<form method="POST" action="/logout">
|
<form method="POST" action="/logout">
|
||||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
<button class="button is-white is-outlined is-small" type="submit">Abmelden</button>
|
<button class="button is-outlined is-small" style="color:rgba(255,255,255,.85);border-color:rgba(255,255,255,.35)" type="submit">Abmelden</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Lösch-Bestätigungs-Modal (Screens) -->
|
<!-- Delete screen modal -->
|
||||||
<div id="delete-modal" class="modal">
|
<div id="del-modal" class="modal">
|
||||||
<div class="modal-background" onclick="closeDeleteModal()"></div>
|
<div class="modal-background" onclick="closeModal('del-modal')"></div>
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
<p class="modal-card-title">Bildschirm löschen?</p>
|
<p class="modal-card-title">Bildschirm löschen?</p>
|
||||||
<button class="delete" aria-label="Schließen" onclick="closeDeleteModal()"></button>
|
<button class="delete" onclick="closeModal('del-modal')"></button>
|
||||||
</header>
|
</header>
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
<p>Soll <strong id="delete-modal-name"></strong> wirklich gelöscht werden?</p>
|
<p>Soll <strong id="del-name"></strong> wirklich gelöscht werden?</p>
|
||||||
<p class="help has-text-grey mt-2">Alle Playlist-Einträge werden ebenfalls gelöscht.</p>
|
<p class="help has-text-grey mt-2">Alle Playlist-Einträge werden ebenfalls gelöscht.</p>
|
||||||
</section>
|
</section>
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot" style="gap:.5rem">
|
||||||
<form id="delete-modal-form" method="POST">
|
<form id="del-form" method="POST"><button class="button is-danger" type="submit">Wirklich löschen</button></form>
|
||||||
<button class="button is-danger" type="submit">Wirklich löschen</button>
|
<button class="button" onclick="closeModal('del-modal')">Abbrechen</button>
|
||||||
</form>
|
|
||||||
<button class="button" onclick="closeDeleteModal()">Abbrechen</button>
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lösch-Bestätigungs-Modal (User) -->
|
<!-- Delete user modal -->
|
||||||
<div id="delete-user-modal" class="modal">
|
<div id="del-user-modal" class="modal">
|
||||||
<div class="modal-background" onclick="closeDeleteUserModal()"></div>
|
<div class="modal-background" onclick="closeModal('del-user-modal')"></div>
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
<p class="modal-card-title">Benutzer löschen?</p>
|
<p class="modal-card-title">Benutzer löschen?</p>
|
||||||
<button class="delete" aria-label="Schließen" onclick="closeDeleteUserModal()"></button>
|
<button class="delete" onclick="closeModal('del-user-modal')"></button>
|
||||||
</header>
|
</header>
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
<p>Soll Benutzer <strong id="delete-user-modal-name"></strong> wirklich gelöscht werden?</p>
|
<p>Soll <strong id="del-user-name"></strong> wirklich gelöscht werden?</p>
|
||||||
<p class="help has-text-grey mt-2">Alle Screen-Zuordnungen werden ebenfalls entfernt.</p>
|
<p class="help has-text-grey mt-2">Alle Screen-Zuordnungen werden entfernt.</p>
|
||||||
</section>
|
</section>
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot" style="gap:.5rem">
|
||||||
<form id="delete-user-modal-form" method="POST">
|
<form id="del-user-form" method="POST"><button class="button is-danger" type="submit">Wirklich löschen</button></form>
|
||||||
<button class="button is-danger" type="submit">Wirklich löschen</button>
|
<button class="button" onclick="closeModal('del-user-modal')">Abbrechen</button>
|
||||||
</form>
|
|
||||||
<button class="button" onclick="closeDeleteUserModal()">Abbrechen</button>
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Screen-User-Verwaltungs-Modal -->
|
<!-- Screen users modal -->
|
||||||
<div id="screen-users-modal" class="modal">
|
<div id="screen-users-modal" class="modal">
|
||||||
<div class="modal-background" onclick="closeScreenUsersModal()"></div>
|
<div class="modal-background" onclick="closeModal('screen-users-modal')"></div>
|
||||||
<div class="modal-card" style="width:600px;max-width:95vw">
|
<div class="modal-card" style="width:580px;max-width:95vw">
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
<p class="modal-card-title" id="screen-users-modal-title">Benutzer verwalten</p>
|
<p class="modal-card-title" id="su-modal-title">Benutzer verwalten</p>
|
||||||
<button class="delete" aria-label="Schließen" onclick="closeScreenUsersModal()"></button>
|
<button class="delete" onclick="closeModal('screen-users-modal')"></button>
|
||||||
</header>
|
</header>
|
||||||
<section class="modal-card-body" id="screen-users-modal-body">
|
<section class="modal-card-body" id="su-modal-body"></section>
|
||||||
</section>
|
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
<button class="button" onclick="closeScreenUsersModal()">Schließen</button>
|
<button class="button" onclick="closeModal('screen-users-modal')">Schließen</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<section class="section pb-0 pt-4">
|
||||||
(function() {
|
|
||||||
var burger = document.querySelector('.navbar-burger[data-target="adminNavbar"]');
|
|
||||||
if (burger) {
|
|
||||||
burger.addEventListener('click', function() {
|
|
||||||
var target = document.getElementById(burger.dataset.target);
|
|
||||||
burger.classList.toggle('is-active');
|
|
||||||
target.classList.toggle('is-active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function openDeleteModal(action, name) {
|
|
||||||
document.getElementById('delete-modal-form').action = action;
|
|
||||||
document.getElementById('delete-modal-name').textContent = name;
|
|
||||||
document.getElementById('delete-modal').classList.add('is-active');
|
|
||||||
}
|
|
||||||
function closeDeleteModal() {
|
|
||||||
document.getElementById('delete-modal').classList.remove('is-active');
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDeleteUserModal(action, name) {
|
|
||||||
document.getElementById('delete-user-modal-form').action = action;
|
|
||||||
document.getElementById('delete-user-modal-name').textContent = name;
|
|
||||||
document.getElementById('delete-user-modal').classList.add('is-active');
|
|
||||||
}
|
|
||||||
function closeDeleteUserModal() {
|
|
||||||
document.getElementById('delete-user-modal').classList.remove('is-active');
|
|
||||||
}
|
|
||||||
|
|
||||||
function openScreenUsersModal(screenId, screenName, html) {
|
|
||||||
document.getElementById('screen-users-modal-title').textContent = 'Benutzer: ' + screenName;
|
|
||||||
document.getElementById('screen-users-modal-body').innerHTML = html;
|
|
||||||
document.getElementById('screen-users-modal').classList.add('is-active');
|
|
||||||
// Re-inject CSRF tokens into newly added forms
|
|
||||||
injectCSRFNow();
|
|
||||||
}
|
|
||||||
function closeScreenUsersModal() {
|
|
||||||
document.getElementById('screen-users-modal').classList.remove('is-active');
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closeDeleteModal();
|
|
||||||
closeDeleteUserModal();
|
|
||||||
closeScreenUsersModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var msg = new URLSearchParams(window.location.search).get('msg');
|
|
||||||
if (!msg) return;
|
|
||||||
var texts = {
|
|
||||||
'uploaded': '✓ Medium erfolgreich hochgeladen.',
|
|
||||||
'deleted': '✓ Erfolgreich gelöscht.',
|
|
||||||
'saved': '✓ Änderungen gespeichert.',
|
|
||||||
'added': '✓ Erfolgreich hinzugefügt.',
|
|
||||||
'user_added': '✓ Benutzer angelegt.',
|
|
||||||
'user_deleted': '✓ Benutzer gelöscht.',
|
|
||||||
'user_added_to_screen': '✓ Benutzer zum Screen hinzugefügt.',
|
|
||||||
'user_removed_from_screen': '✓ Benutzer vom Screen entfernt.',
|
|
||||||
'error_empty': '⚠ Benutzername und Passwort erforderlich.',
|
|
||||||
'error_exists': '⚠ Benutzername bereits vergeben.',
|
|
||||||
'error_db': '⚠ Datenbankfehler.'
|
|
||||||
};
|
|
||||||
var isError = msg.startsWith('error_');
|
|
||||||
var text = texts[msg] || '✓ Aktion erfolgreich.';
|
|
||||||
var n = document.createElement('div');
|
|
||||||
n.className = 'notification ' + (isError ? 'is-warning' : 'is-success');
|
|
||||||
n.setAttribute('role', 'alert');
|
|
||||||
n.style.cssText = 'position:fixed;top:1rem;right:1rem;z-index:9999;max-width:380px;box-shadow:0 4px 12px rgba(0,0,0,.15)';
|
|
||||||
n.innerHTML = '<button class="delete"></button>' + text;
|
|
||||||
n.querySelector('.delete').addEventListener('click', function() { n.remove(); });
|
|
||||||
document.body.appendChild(n);
|
|
||||||
setTimeout(function() {
|
|
||||||
n.style.transition = 'opacity .5s';
|
|
||||||
n.style.opacity = '0';
|
|
||||||
setTimeout(function() { n.remove(); }, 500);
|
|
||||||
}, 3000);
|
|
||||||
var url = new URL(window.location.href);
|
|
||||||
url.searchParams.delete('msg');
|
|
||||||
history.replaceState(null, '', url.toString());
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="section pb-0 pt-3">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<nav class="breadcrumb" aria-label="breadcrumb">
|
<nav class="breadcrumb mb-3"><ul><li class="is-active"><a>Admin</a></li></ul></nav>
|
||||||
|
<div class="tabs mb-0">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="is-active"><a href="#" aria-current="page">Admin</a></li>
|
<li id="tab-screens" class="{{if eq .ActiveTab "screens"}}is-active{{end}}">
|
||||||
|
<a onclick="switchTab('screens')" style="cursor:pointer">Bildschirme</a>
|
||||||
|
</li>
|
||||||
|
<li id="tab-users" class="{{if eq .ActiveTab "users"}}is-active{{end}}">
|
||||||
|
<a onclick="switchTab('users')" style="cursor:pointer">Benutzer</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section pt-2">
|
<section class="section pt-3">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Panel: Screens -->
|
||||||
<div class="tabs is-boxed mb-0">
|
<div id="panel-screens" class="tab-panel{{if eq .ActiveTab "screens"}} is-active{{end}}">
|
||||||
<ul>
|
|
||||||
<li id="tab-screens" class="{{if eq .ActiveTab "screens"}}is-active{{end}}">
|
<div class="box mb-4">
|
||||||
<a><button type="button" role="tab" aria-selected="{{if eq .ActiveTab "screens"}}true{{else}}false{{end}}" onclick="switchTab('screens')" style="background:none;border:none;padding:0;cursor:pointer;font:inherit;color:inherit">Bildschirme</button></a>
|
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem;margin-bottom:1.25rem">
|
||||||
</li>
|
<h2 class="title is-5 mb-0">Bildschirme</h2>
|
||||||
<li id="tab-users" class="{{if eq .ActiveTab "users"}}is-active{{end}}">
|
<div class="buttons mb-0">
|
||||||
<a><button type="button" role="tab" aria-selected="{{if eq .ActiveTab "users"}}true{{else}}false{{end}}" onclick="switchTab('users')" style="background:none;border:none;padding:0;cursor:pointer;font:inherit;color:inherit">Benutzer</button></a>
|
<a class="button is-primary is-small" href="#add-screen">+ Neuer Bildschirm</a>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
function switchTab(name) {
|
|
||||||
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('is-active'); });
|
|
||||||
document.querySelectorAll('.tabs li').forEach(function(li) {
|
|
||||||
li.classList.remove('is-active');
|
|
||||||
var btn = li.querySelector('[role="tab"]');
|
|
||||||
if (btn) btn.setAttribute('aria-selected', 'false');
|
|
||||||
});
|
|
||||||
document.getElementById('panel-' + name).classList.add('is-active');
|
|
||||||
var tabLi = document.getElementById('tab-' + name);
|
|
||||||
tabLi.classList.add('is-active');
|
|
||||||
var activeBtn = tabLi.querySelector('[role="tab"]');
|
|
||||||
if (activeBtn) activeBtn.setAttribute('aria-selected', 'true');
|
|
||||||
var url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('tab', name);
|
|
||||||
history.replaceState(null, '', url.toString());
|
|
||||||
}
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
var active = '{{.ActiveTab}}';
|
|
||||||
switchTab(active || 'screens');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Panel: Bildschirme -->
|
|
||||||
<div id="panel-screens" class="tab-panel box" style="border-radius:0 4px 4px 4px">
|
|
||||||
|
|
||||||
<h2 class="title is-5">Bildschirme</h2>
|
|
||||||
{{if .Screens}}
|
{{if .Screens}}
|
||||||
<div style="overflow-x: auto">
|
<div class="columns is-multiline">
|
||||||
<table class="table is-fullwidth is-hoverable is-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Slug</th>
|
|
||||||
<th>Format</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Benutzer</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{range .Screens}}
|
{{range .Screens}}
|
||||||
{{$users := index $.ScreenUserMap .ID}}
|
{{$users := index $.ScreenUserMap .ID}}
|
||||||
<tr>
|
<div class="column is-4-desktop is-6-tablet">
|
||||||
<td><strong>{{.Name}}</strong></td>
|
<div class="screen-card">
|
||||||
<td><code>{{.Slug}}</code></td>
|
<div class="screen-card-name">
|
||||||
<td>{{orientationLabel .Orientation}}</td>
|
{{if eq .Orientation "portrait"}}📱{{else}}🖥{{end}}
|
||||||
<td id="status-{{.Slug}}"><span class="has-text-grey" aria-label="Status unbekannt">⚪</span></td>
|
{{.Name}}
|
||||||
<td>
|
</div>
|
||||||
{{$screenID := .ID}}
|
<div class="screen-card-slug">{{.Slug}}</div>
|
||||||
{{$screenName := .Name}}
|
<div class="screen-card-meta">
|
||||||
<button class="button is-small is-light"
|
<span class="orient-badge">{{orientationLabel .Orientation}}</span>
|
||||||
type="button"
|
<span id="status-{{.Slug}}" class="status-dot unknown" title="Unbekannt"></span>
|
||||||
data-screen-id="{{$screenID}}"
|
<button class="button is-small is-light" type="button"
|
||||||
data-screen-name="{{$screenName}}"
|
data-screen-id="{{.ID}}" data-screen-name="{{.Name}}"
|
||||||
onclick="openScreenUsersModal('{{$screenID}}', {{$screenName | printf "%q"}}, buildScreenUsersHTML('{{$screenID}}', {{$screenName | printf "%q"}}))">
|
onclick="openScreenUsersModal('{{.ID}}', {{.Name | printf "%q"}}, buildScreenUsersHTML('{{.ID}}', {{.Name | printf "%q"}}))">
|
||||||
{{len $users}} Benutzer
|
{{len $users}} Benutzer
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</div>
|
||||||
<td>
|
<div class="screen-card-actions">
|
||||||
<a class="button is-small is-link" href="/manage/{{.Slug}}">Playlist verwalten</a>
|
<a class="button is-small is-primary is-fullwidth" href="/manage/{{.Slug}}">Playlist</a>
|
||||||
|
<button class="button is-small is-danger is-outlined" type="button"
|
||||||
<button class="button is-small is-danger is-outlined"
|
onclick="openDelModal('/admin/screens/{{.ID}}/delete','{{.Name}}')">Löschen</button>
|
||||||
type="button"
|
</div>
|
||||||
aria-label="Bildschirm {{.Name}} löschen"
|
</div>
|
||||||
onclick="openDeleteModal('/admin/screens/{{.ID}}/delete', '{{.Name}}')">Löschen</button>
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="notification is-light">Noch keine Bildschirme angelegt. Füge unten den ersten hinzu.</div>
|
<div class="notification is-light">Noch keine Bildschirme angelegt.</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<!-- Add screen form -->
|
||||||
|
<div class="box" id="add-screen">
|
||||||
<h2 class="title is-5">Neuen Bildschirm einrichten</h2>
|
<h2 class="title is-5 mb-1">Neuen Bildschirm einrichten</h2>
|
||||||
<p class="mb-4 has-text-grey">
|
<p class="has-text-grey mb-4" style="font-size:.875rem">Bildschirm anlegen und Ansible-Deployment-Anleitung generieren.</p>
|
||||||
Fülle die Angaben aus. Der Bildschirm wird im Backend angelegt und du erhältst
|
|
||||||
eine <strong>Schritt-für-Schritt-Anleitung</strong> mit allen nötigen Befehlen
|
|
||||||
für das Ansible-Deployment.
|
|
||||||
</p>
|
|
||||||
<form method="POST" action="/admin/screens/provision">
|
<form method="POST" action="/admin/screens/provision">
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Slug / Hostname</label>
|
<label class="label is-small">Slug / Hostname</label>
|
||||||
<div class="control">
|
<input class="input is-small" type="text" name="slug" placeholder="z.B. info12" required pattern="[a-z0-9-]+" title="Kleinbuchstaben, Zahlen, Bindestriche">
|
||||||
<input class="input" type="text" name="slug" placeholder="z.B. info12" required
|
<p class="help">Als <code>screen_id</code> verwendet</p>
|
||||||
pattern="[a-z0-9-]+" title="Nur Kleinbuchstaben, Zahlen und Bindestriche">
|
|
||||||
</div>
|
|
||||||
<p class="help">Eindeutig, URL-sicher — wird als <code>screen_id</code> verwendet</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Anzeigename</label>
|
<label class="label is-small">Anzeigename</label>
|
||||||
<div class="control">
|
<input class="input is-small" type="text" name="name" placeholder="z.B. Kantine EG" required>
|
||||||
<input class="input" type="text" name="name" placeholder="z.B. Kantine EG" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-2">
|
<div class="column is-2">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">IP-Adresse</label>
|
<label class="label is-small">IP-Adresse</label>
|
||||||
<div class="control">
|
<input class="input is-small" type="text" name="ip" placeholder="10.0.0.X" required>
|
||||||
<input class="input" type="text" name="ip" placeholder="10.0.0.X" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-2">
|
<div class="column is-2">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">SSH-User</label>
|
<label class="label is-small">SSH-User</label>
|
||||||
<div class="control">
|
<input class="input is-small" type="text" name="ssh_user" placeholder="morz" value="morz">
|
||||||
<input class="input" type="text" name="ssh_user" placeholder="morz" value="morz">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-2">
|
<div class="column is-2">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Format</label>
|
<label class="label is-small">Format</label>
|
||||||
<div class="control">
|
<div class="select is-small is-fullwidth">
|
||||||
<div class="select is-fullwidth">
|
|
||||||
<select name="orientation">
|
<select name="orientation">
|
||||||
<option value="landscape">Querformat</option>
|
<option value="landscape">Querformat</option>
|
||||||
<option value="portrait">Hochformat</option>
|
<option value="portrait">Hochformat</option>
|
||||||
|
|
@ -536,37 +435,29 @@ document.addEventListener('keydown', function(e) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button class="button is-primary is-small" type="submit">Anlegen & Anleitung generieren →</button>
|
||||||
<button class="button is-primary" type="submit">Anlegen & Anleitung generieren →</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<details class="mt-4">
|
<details class="mt-4">
|
||||||
<summary style="cursor:pointer;font-weight:600;color:#4a4a4a">Bildschirm nur anlegen (ohne Deployment)</summary>
|
<summary class="has-text-grey is-size-7" style="cursor:pointer">Nur anlegen (ohne Deployment)</summary>
|
||||||
<p class="help has-text-grey mt-1 mb-0">Legt nur einen Datenbank-Eintrag an — kein Ansible, kein Agent-Setup. Für Bildschirme, die bereits provisioniert sind oder manuell konfiguriert werden.</p>
|
<form method="POST" action="/admin/screens" class="mt-3">
|
||||||
<form method="POST" action="/admin/screens" class="mt-4">
|
|
||||||
<div class="columns is-vcentered">
|
<div class="columns is-vcentered">
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Slug</label>
|
<label class="label is-small">Slug</label>
|
||||||
<div class="control">
|
<input class="input is-small" type="text" name="slug" placeholder="flur-eg" required pattern="[a-z0-9-]+">
|
||||||
<input class="input" type="text" name="slug" placeholder="z.B. flur-eg" required
|
|
||||||
pattern="[a-z0-9-]+" title="Nur Kleinbuchstaben, Zahlen und Bindestriche">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Name</label>
|
<label class="label is-small">Name</label>
|
||||||
<div class="control">
|
<input class="input is-small" type="text" name="name" placeholder="Flur Erdgeschoss" required>
|
||||||
<input class="input" type="text" name="name" placeholder="z.B. Flur Erdgeschoss" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-2">
|
<div class="column is-2">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Format</label>
|
<label class="label is-small">Format</label>
|
||||||
<div class="control">
|
<div class="select is-small is-fullwidth">
|
||||||
<div class="select is-fullwidth">
|
|
||||||
<select name="orientation">
|
<select name="orientation">
|
||||||
<option value="landscape">Querformat</option>
|
<option value="landscape">Querformat</option>
|
||||||
<option value="portrait">Hochformat</option>
|
<option value="portrait">Hochformat</option>
|
||||||
|
|
@ -574,132 +465,144 @@ document.addEventListener('keydown', function(e) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"> </label>
|
<label class="label is-small"> </label>
|
||||||
<button class="button is-outlined is-fullwidth" type="submit">Nur anlegen</button>
|
<button class="button is-outlined is-small is-fullwidth" type="submit">Nur anlegen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
|
</div>
|
||||||
</div><!-- /panel-screens -->
|
</div><!-- /panel-screens -->
|
||||||
|
|
||||||
<!-- Panel: Benutzer -->
|
<!-- Panel: Users -->
|
||||||
<div id="panel-users" class="tab-panel box" style="border-radius:0 4px 4px 4px">
|
<div id="panel-users" class="tab-panel{{if eq .ActiveTab "users"}} is-active{{end}}">
|
||||||
|
<div class="box">
|
||||||
<h2 class="title is-5">Screen-Benutzer</h2>
|
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem;margin-bottom:1.25rem">
|
||||||
<p class="has-text-grey mb-4">Screen-Benutzer können sich einloggen und nur ihre zugeordneten Bildschirme verwalten.</p>
|
<div>
|
||||||
|
<h2 class="title is-5 mb-0">Screen-Benutzer</h2>
|
||||||
|
<p class="has-text-grey mt-1" style="font-size:.875rem">Können sich einloggen und nur ihre zugeordneten Bildschirme verwalten.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{if .ScreenUsers}}
|
{{if .ScreenUsers}}
|
||||||
<table class="table is-fullwidth is-hoverable is-striped mb-5">
|
<div style="overflow-x:auto;margin-bottom:1.5rem">
|
||||||
|
<table class="table is-fullwidth is-hoverable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr style="font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:#6b7280">
|
||||||
<th>Benutzername</th>
|
<th>Benutzername</th>
|
||||||
<th>Erstellt</th>
|
<th>Erstellt</th>
|
||||||
<th>Aktionen</th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .ScreenUsers}}
|
{{range .ScreenUsers}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>{{.Username}}</strong></td>
|
<td><strong>{{.Username}}</strong></td>
|
||||||
<td>{{.CreatedAt.Format "02.01.2006 15:04"}}</td>
|
<td class="has-text-grey" style="font-size:.875rem">{{.CreatedAt.Format "02.01.2006 15:04"}}</td>
|
||||||
<td>
|
<td style="text-align:right">
|
||||||
<button class="button is-small is-danger is-outlined"
|
<button class="button is-small is-danger is-outlined" type="button"
|
||||||
type="button"
|
onclick="openDelUserModal('/admin/users/{{.ID}}/delete','{{.Username}}')">Löschen</button>
|
||||||
onclick="openDeleteUserModal('/admin/users/{{.ID}}/delete', '{{.Username}}')">Löschen</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="notification is-light mb-4">Noch keine Screen-Benutzer angelegt. Lege unten den ersten an.</div>
|
<div class="notification is-light mb-4">Noch keine Benutzer angelegt.</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<h3 class="title is-6">Neuen Benutzer anlegen</h3>
|
<h3 class="title is-6 mb-3">Neuen Benutzer anlegen</h3>
|
||||||
<form method="POST" action="/admin/users">
|
<form method="POST" action="/admin/users">
|
||||||
<div class="columns is-vcentered">
|
<div class="columns is-vcentered">
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Benutzername</label>
|
<label class="label is-small">Benutzername</label>
|
||||||
<div class="control">
|
<input class="input is-small" type="text" name="username" placeholder="z.B. alice" required autocomplete="off">
|
||||||
<input class="input" type="text" name="username" placeholder="z.B. alice" required
|
|
||||||
autocomplete="off">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Passwort</label>
|
<label class="label is-small">Passwort</label>
|
||||||
<div class="control has-icons-right">
|
<div class="control has-icons-right">
|
||||||
<input class="input" type="password" id="admin-new-password" name="password" placeholder="Passwort (mind. 8 Zeichen)" required
|
<input class="input is-small" type="password" id="admin-new-password" name="password" placeholder="Mindestens 8 Zeichen" required autocomplete="new-password" minlength="8">
|
||||||
autocomplete="new-password" minlength="8">
|
<span class="icon is-small is-right" style="pointer-events:all;cursor:pointer" onclick="var i=document.getElementById('admin-new-password');i.type=i.type==='password'?'text':'password'">
|
||||||
<span class="icon is-small is-right" style="pointer-events:all;cursor:pointer" onclick="togglePwAdmin()" title="Passwort anzeigen/verbergen" aria-label="Passwort anzeigen/verbergen">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
||||||
<circle cx="12" cy="12" r="3"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="help">Mindestens 8 Zeichen</p>
|
<p class="help">Mind. 8 Zeichen</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"> </label>
|
<label class="label is-small"> </label>
|
||||||
<button class="button is-primary" type="submit">Benutzer anlegen</button>
|
<button class="button is-primary is-small" type="submit">Benutzer anlegen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</div><!-- /panel-users -->
|
</div><!-- /panel-users -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Eingebettete Screen-User-Daten für das Modal (als JSON-Strings) -->
|
|
||||||
<script>
|
<script>
|
||||||
var _screenUsers = {{.ScreenUsers | screenUsersJSON}};
|
var _screenUsers = {{.ScreenUsers | screenUsersJSON}};
|
||||||
var _screenUserMap = {{.ScreenUserMap | screenUserMapJSON}};
|
var _screenUserMap = {{.ScreenUserMap | screenUserMapJSON}};
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDelModal(action, name) {
|
||||||
|
document.getElementById('del-form').action = action;
|
||||||
|
document.getElementById('del-name').textContent = name;
|
||||||
|
document.getElementById('del-modal').classList.add('is-active');
|
||||||
|
}
|
||||||
|
function openDelUserModal(action, name) {
|
||||||
|
document.getElementById('del-user-form').action = action;
|
||||||
|
document.getElementById('del-user-name').textContent = name;
|
||||||
|
document.getElementById('del-user-modal').classList.add('is-active');
|
||||||
|
}
|
||||||
|
function openScreenUsersModal(screenId, screenName, html) {
|
||||||
|
document.getElementById('su-modal-title').textContent = 'Benutzer: ' + screenName;
|
||||||
|
document.getElementById('su-modal-body').innerHTML = html;
|
||||||
|
document.getElementById('screen-users-modal').classList.add('is-active');
|
||||||
|
injectCSRF();
|
||||||
|
}
|
||||||
|
function closeModal(id) { document.getElementById(id).classList.remove('is-active'); }
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') ['del-modal','del-user-modal','screen-users-modal'].forEach(function(id) { closeModal(id); });
|
||||||
|
});
|
||||||
|
|
||||||
function buildScreenUsersHTML(screenId, screenName) {
|
function buildScreenUsersHTML(screenId, screenName) {
|
||||||
var users = _screenUserMap[screenId] || [];
|
var users = _screenUserMap[screenId] || [];
|
||||||
var allUsers = _screenUsers || [];
|
var allUsers = _screenUsers || [];
|
||||||
|
var assigned = {};
|
||||||
// Bereits zugeordnete User-IDs
|
users.forEach(function(u) { assigned[u.id] = true; });
|
||||||
var assignedIds = {};
|
|
||||||
users.forEach(function(u) { assignedIds[u.id] = true; });
|
|
||||||
|
|
||||||
// Tabelle der zugeordneten User
|
|
||||||
var html = '';
|
var html = '';
|
||||||
if (users.length > 0) {
|
if (users.length > 0) {
|
||||||
html += '<table class="table is-fullwidth is-narrow mb-4"><thead><tr><th>Benutzer</th><th></th></tr></thead><tbody>';
|
html += '<table class="table is-fullwidth is-narrow mb-4"><thead><tr><th>Benutzer</th><th></th></tr></thead><tbody>';
|
||||||
users.forEach(function(u) {
|
users.forEach(function(u) {
|
||||||
html += '<tr><td>' + escHtml(u.username) + '</td>';
|
html += '<tr><td>' + escHtml(u.username) + '</td><td>';
|
||||||
html += '<td><form method="POST" action="/admin/screens/' + escHtml(screenId) + '/users/' + escHtml(u.id) + '/remove" style="display:inline">';
|
html += '<form method="POST" action="/admin/screens/' + escHtml(screenId) + '/users/' + escHtml(u.id) + '/remove" style="display:inline">';
|
||||||
html += '<button class="button is-small is-danger is-outlined" type="submit">Entfernen</button></form></td></tr>';
|
html += '<button class="button is-small is-danger is-outlined" type="submit">Entfernen</button></form></td></tr>';
|
||||||
});
|
});
|
||||||
html += '</tbody></table>';
|
html += '</tbody></table>';
|
||||||
} else {
|
} else {
|
||||||
html += '<p class="has-text-grey mb-4">Noch keine Benutzer zugeordnet.</p>';
|
html += '<p class="has-text-grey mb-4">Noch keine Benutzer zugeordnet.</p>';
|
||||||
}
|
}
|
||||||
|
var available = allUsers.filter(function(u) { return !assigned[u.id]; });
|
||||||
// Dropdown mit verfügbaren Usern
|
|
||||||
var available = allUsers.filter(function(u) { return !assignedIds[u.id]; });
|
|
||||||
if (available.length > 0) {
|
if (available.length > 0) {
|
||||||
html += '<form method="POST" action="/admin/screens/' + escHtml(screenId) + '/users">';
|
html += '<form method="POST" action="/admin/screens/' + escHtml(screenId) + '/users">';
|
||||||
html += '<div class="field has-addons">';
|
html += '<div class="field has-addons">';
|
||||||
html += '<div class="control is-expanded"><div class="select is-fullwidth"><select name="user_id">';
|
html += '<div class="control is-expanded"><div class="select is-fullwidth"><select name="user_id">';
|
||||||
available.forEach(function(u) {
|
available.forEach(function(u) { html += '<option value="' + escHtml(u.id) + '">' + escHtml(u.username) + '</option>'; });
|
||||||
html += '<option value="' + escHtml(u.id) + '">' + escHtml(u.username) + '</option>';
|
|
||||||
});
|
|
||||||
html += '</select></div></div>';
|
html += '</select></div></div>';
|
||||||
html += '<div class="control"><button class="button is-primary" type="submit">Hinzufügen</button></div>';
|
html += '<div class="control"><button class="button is-primary" type="submit">Hinzufügen</button></div>';
|
||||||
html += '</div></form>';
|
html += '</div></form>';
|
||||||
|
|
@ -708,111 +611,80 @@ function buildScreenUsersHTML(screenId, screenName) {
|
||||||
} else {
|
} else {
|
||||||
html += '<p class="help has-text-grey">Alle Benutzer sind bereits zugeordnet.</p>';
|
html += '<p class="help has-text-grey">Alle Benutzer sind bereits zugeordnet.</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function escHtml(s) {
|
function switchTab(name) {
|
||||||
return String(s)
|
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('is-active'); });
|
||||||
.replace(/&/g, '&')
|
document.querySelectorAll('.tabs li').forEach(function(li) { li.classList.remove('is-active'); });
|
||||||
.replace(/</g, '<')
|
document.getElementById('panel-' + name).classList.add('is-active');
|
||||||
.replace(/>/g, '>')
|
document.getElementById('tab-' + name).classList.add('is-active');
|
||||||
.replace(/"/g, '"');
|
history.replaceState(null, '', '?tab=' + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectCSRFNow() {
|
// Navbar burger
|
||||||
function getCookie(name) {
|
|
||||||
var m = document.cookie.match('(?:^|; )' + name + '=([^;]*)');
|
|
||||||
return m ? decodeURIComponent(m[1]) : '';
|
|
||||||
}
|
|
||||||
var token = getCookie('morz_csrf');
|
|
||||||
if (!token) return;
|
|
||||||
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
|
|
||||||
if (!f.querySelector('input[name="csrf_token"]')) {
|
|
||||||
var inp = document.createElement('input');
|
|
||||||
inp.type = 'hidden'; inp.name = 'csrf_token'; inp.value = token;
|
|
||||||
f.appendChild(inp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
(function() {
|
||||||
fetch('/api/v1/screens/status')
|
var b = document.querySelector('.navbar-burger');
|
||||||
.then(function(r) { return r.ok ? r.json() : null; })
|
if (b) b.addEventListener('click', function() {
|
||||||
.then(function(data) {
|
b.classList.toggle('is-active');
|
||||||
|
document.getElementById(b.dataset.target).classList.toggle('is-active');
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Toast from ?msg=
|
||||||
|
(function() {
|
||||||
|
var msg = new URLSearchParams(window.location.search).get('msg');
|
||||||
|
if (!msg) return;
|
||||||
|
var texts = { uploaded:'✓ Medium hochgeladen.',deleted:'✓ Gelöscht.',saved:'✓ Gespeichert.',added:'✓ Hinzugefügt.',user_added:'✓ Benutzer angelegt.',user_deleted:'✓ Benutzer gelöscht.',user_added_to_screen:'✓ Benutzer zugeordnet.',user_removed_from_screen:'✓ Benutzer entfernt.',error_empty:'⚠ Felder ausfüllen.',error_exists:'⚠ Bereits vergeben.',error_db:'⚠ Datenbankfehler.' };
|
||||||
|
var isErr = msg.startsWith('error_');
|
||||||
|
showToast(texts[msg] || '✓ Aktion erfolgreich.', isErr ? 'is-warning' : 'is-success');
|
||||||
|
history.replaceState(null, '', window.location.pathname + (window.location.search.replace(/[?&]msg=[^&]*/,'') || ''));
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Status dots
|
||||||
|
(function() {
|
||||||
|
function update() {
|
||||||
|
fetch('/api/v1/screens/status').then(function(r) { return r.ok ? r.json() : null; }).then(function(data) {
|
||||||
if (!data || !data.screens) return;
|
if (!data || !data.screens) return;
|
||||||
var dots = {
|
|
||||||
'online': { emoji: '🟢', label: 'Online' },
|
|
||||||
'degraded': { emoji: '🟡', label: 'Eingeschränkt' },
|
|
||||||
'offline': { emoji: '🔴', label: 'Offline' }
|
|
||||||
};
|
|
||||||
data.screens.forEach(function(s) {
|
data.screens.forEach(function(s) {
|
||||||
var cell = document.getElementById('status-' + s.screen_id);
|
var el = document.getElementById('status-' + s.screen_id);
|
||||||
if (cell) {
|
if (!el) return;
|
||||||
var info = dots[s.derived_state] || { emoji: '⚪', label: 'Unbekannt' };
|
var state = s.derived_state || 'unknown';
|
||||||
cell.innerHTML = '<span aria-hidden="true">' + info.emoji + '</span>'
|
el.className = 'status-dot ' + (state === 'online' ? 'online' : state === 'degraded' ? 'stale' : 'offline');
|
||||||
+ '<span class="is-sr-only">' + info.label + '</span>'
|
el.title = state;
|
||||||
+ ' <small>' + s.derived_state + '</small>';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
})
|
}).catch(function(){});
|
||||||
.catch(function() {});
|
}
|
||||||
|
update(); setInterval(update, 30000);
|
||||||
})();
|
})();
|
||||||
</script>
|
|
||||||
<script>
|
// Auto-open screen users modal from ?screen=
|
||||||
// K1: CSRF Double-Submit — füge Token aus Cookie in alle POST-Formulare ein.
|
|
||||||
(function() {
|
|
||||||
function getCookie(name) {
|
|
||||||
var m = document.cookie.match('(?:^|; )' + name + '=([^;]*)');
|
|
||||||
return m ? decodeURIComponent(m[1]) : '';
|
|
||||||
}
|
|
||||||
function injectCSRF() {
|
|
||||||
var token = getCookie('morz_csrf');
|
|
||||||
if (!token) return;
|
|
||||||
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
|
|
||||||
if (!f.querySelector('input[name="csrf_token"]')) {
|
|
||||||
var inp = document.createElement('input');
|
|
||||||
inp.type = 'hidden'; inp.name = 'csrf_token'; inp.value = token;
|
|
||||||
f.appendChild(inp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', injectCSRF);
|
|
||||||
} else {
|
|
||||||
injectCSRF();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
function togglePwAdmin() {
|
|
||||||
var inp = document.getElementById('admin-new-password');
|
|
||||||
if (inp) inp.type = (inp.type === 'password') ? 'text' : 'password';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
// M2: Auto-open Screen-User-Modal wenn ?screen= in URL vorhanden
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var autoScreen = new URLSearchParams(window.location.search).get('screen');
|
var sc = new URLSearchParams(window.location.search).get('screen');
|
||||||
if (!autoScreen) return;
|
if (!sc) return;
|
||||||
// Suche den passenden Screen in den eingebetteten Daten
|
var btn = document.querySelector('[data-screen-id="' + sc + '"]');
|
||||||
var allScreenData = document.querySelectorAll('[data-screen-id]');
|
if (btn) openScreenUsersModal(sc, btn.getAttribute('data-screen-name') || sc, buildScreenUsersHTML(sc, btn.getAttribute('data-screen-name') || sc));
|
||||||
// Fallback: direkt openScreenUsersModal aufrufen falls screenId bekannt
|
history.replaceState(null, '', window.location.href.replace(/[?&]screen=[^&]*/,''));
|
||||||
if (typeof openScreenUsersModal === 'function' && typeof buildScreenUsersHTML === 'function') {
|
|
||||||
// Finde Screenname aus dem Button
|
|
||||||
var btn = document.querySelector('[data-screen-id="' + autoScreen + '"]');
|
|
||||||
if (btn) {
|
|
||||||
var screenName = btn.getAttribute('data-screen-name') || autoScreen;
|
|
||||||
openScreenUsersModal(autoScreen, screenName, buildScreenUsersHTML(autoScreen, screenName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// URL-Parameter entfernen ohne Reload
|
|
||||||
var url = new URL(window.location.href);
|
|
||||||
url.searchParams.delete('screen');
|
|
||||||
history.replaceState(null, '', url.toString());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function showToast(msg, type) {
|
||||||
|
type = type || 'is-success';
|
||||||
|
var t = document.createElement('div');
|
||||||
|
t.className = 'morz-toast ' + type;
|
||||||
|
t.innerHTML = msg + '<button onclick="this.parentElement.remove()" style="background:none;border:none;cursor:pointer;font-size:1rem;margin-left:auto;opacity:.6">✕</button>';
|
||||||
|
document.body.appendChild(t);
|
||||||
|
requestAnimationFrame(function() { t.classList.add('show'); });
|
||||||
|
setTimeout(function() { t.classList.remove('show'); setTimeout(function() { t.remove(); }, 300); }, 3500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCsrfToken() { var m = document.cookie.match('(?:^|; )morz_csrf=([^;]*)'); return m ? decodeURIComponent(m[1]) : ''; }
|
||||||
|
function injectCSRF() {
|
||||||
|
var token = getCsrfToken(); if (!token) return;
|
||||||
|
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
|
||||||
|
if (!f.querySelector('input[name="csrf_token"]')) { var i=document.createElement('input');i.type='hidden';i.name='csrf_token';i.value=token;f.appendChild(i); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (document.readyState==='loading') document.addEventListener('DOMContentLoaded',injectCSRF); else injectCSRF();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue