281 lines
14 KiB
Go
281 lines
14 KiB
Go
package tenant
|
||
|
||
const tenantDashTmpl = `<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Dashboard – {{.Tenant.Name}}</title>
|
||
<link rel="stylesheet" href="/static/bulma.min.css">
|
||
<style>
|
||
: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; }
|
||
body { background:var(--bg); font-family:system-ui,-apple-system,"Segoe UI",sans-serif; min-height:100vh; }
|
||
.navbar { background:var(--nav-bg) !important; }
|
||
.navbar-item { color:rgba(255,255,255,.85) !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; }
|
||
.button { border-radius:var(--radius-btn) !important; }
|
||
.input:focus,.select select:focus { border-color:var(--morz-red); box-shadow:0 0 0 3px rgba(227,6,19,.12); outline:none; }
|
||
/* 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-content { display:none; }
|
||
.tab-content.is-active { display:block; }
|
||
/* Screen cards */
|
||
.sc-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; }
|
||
.sc-card:hover { box-shadow:var(--shadow-md); }
|
||
.sc-name { font-weight:700; font-size:1.05rem; display:flex; align-items:center; gap:.5rem; }
|
||
.sc-sub { font-size:.8rem; color:#6b7280; display:flex; align-items:center; gap:.5rem; }
|
||
.status-dot { width:10px; height:10px; border-radius:50%; display:inline-block; flex-shrink:0; }
|
||
.status-dot.online { background:#22c55e; }
|
||
.status-dot.stale { background:#f59e0b; }
|
||
.status-dot.offline{ background:#ef4444; }
|
||
.status-dot.unknown{ background:#9ca3af; }
|
||
/* Upload zone */
|
||
.upload-zone { border:2px dashed #d1d5db; border-radius:var(--radius); padding:1.5rem; text-align:center; cursor:pointer; transition:border-color .15s; }
|
||
.upload-zone:hover,.upload-zone.dragover { border-color:var(--morz-red); background:#fff5f5; }
|
||
.upload-zone p { color:#9ca3af; font-size:.875rem; margin:.25rem 0; }
|
||
/* Toast */
|
||
.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; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<nav class="navbar" role="navigation">
|
||
<div class="navbar-brand">
|
||
<span class="navbar-item morz-brand"><span class="accent">MORZ</span> Infoboard</span>
|
||
</div>
|
||
<div class="navbar-menu">
|
||
<div class="navbar-end">
|
||
<div class="navbar-item">
|
||
<form method="POST" action="/logout">
|
||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<section class="section">
|
||
<div class="container">
|
||
|
||
<h1 class="title is-4 mb-4">{{.Tenant.Name}}</h1>
|
||
|
||
{{if .Flash}}
|
||
<div class="notification is-success is-light mb-4" role="alert">
|
||
<button class="delete" onclick="this.parentElement.remove()"></button>{{.Flash}}
|
||
</div>
|
||
{{end}}
|
||
|
||
<div class="tabs mb-0" id="dash-tabs">
|
||
<ul>
|
||
<li class="is-active" data-tab="monitors"><a>Meine Monitore</a></li>
|
||
<li data-tab="media"><a>Mediathek</a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Tab: Monitors -->
|
||
<div id="tab-monitors" class="tab-content is-active">
|
||
<div class="box" style="border-radius:0 var(--radius) var(--radius) var(--radius)">
|
||
{{if .Screens}}
|
||
<div class="columns is-multiline">
|
||
{{range .Screens}}
|
||
<div class="column is-4-desktop is-6-tablet">
|
||
<div class="sc-card">
|
||
<div class="sc-name">
|
||
{{if eq .Orientation "portrait"}}📱{{else}}🖥{{end}}
|
||
{{.Name}}
|
||
</div>
|
||
<div class="sc-sub">
|
||
<span class="status-dot unknown" id="status-{{.Slug}}" title="Unbekannt"></span>
|
||
<span>{{orientationLabel .Orientation}}</span>
|
||
</div>
|
||
<a class="button is-primary is-fullwidth" href="/manage/{{.Slug}}?from=tenant">Playlist bearbeiten →</a>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{else}}
|
||
<p class="has-text-grey">Noch keine Monitore zugewiesen.</p>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Media -->
|
||
<div id="tab-media" class="tab-content">
|
||
<div class="box" style="border-radius:0 var(--radius) var(--radius) var(--radius)">
|
||
|
||
<h2 class="title is-5 mb-3">Medium hochladen</h2>
|
||
<form id="upload-form" method="POST" action="/tenant/{{.Tenant.Slug}}/upload" enctype="multipart/form-data">
|
||
<div class="field">
|
||
<label class="label is-small">Typ</label>
|
||
<div class="select is-small">
|
||
<select name="type" id="upload-type" onchange="toggleTypeFields()">
|
||
<option value="image">🖼 Bild</option>
|
||
<option value="video">🎬 Video</option>
|
||
<option value="pdf">📄 PDF</option>
|
||
<option value="web">🌐 Website (URL)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label class="label is-small">Titel <span class="has-text-grey">(optional)</span></label>
|
||
<input class="input is-small" type="text" name="title" placeholder="Aus Dateinamen abgeleitet">
|
||
</div>
|
||
<div id="file-field" class="field">
|
||
<label class="label is-small">Datei</label>
|
||
<div class="upload-zone" onclick="document.getElementById('upload-file').click()" ondragover="event.preventDefault();this.classList.add('dragover')" ondragleave="this.classList.remove('dragover')" ondrop="handleDrop(event)">
|
||
<p style="font-size:1.5rem">📂</p>
|
||
<p>Klicken oder Datei hierher ziehen</p>
|
||
<p id="drop-fn" style="display:none;color:#374151;font-weight:600"></p>
|
||
</div>
|
||
<input type="file" id="upload-file" name="file" accept="image/*,video/*,application/pdf" style="display:none" onchange="var p=document.getElementById('drop-fn');if(this.files[0]){p.textContent=this.files[0].name;p.style.display=''}">
|
||
</div>
|
||
<div id="url-field" class="field" style="display:none">
|
||
<label class="label is-small">URL</label>
|
||
<input class="input is-small" type="url" name="url" placeholder="https://...">
|
||
</div>
|
||
<div id="upload-progress-wrap" class="field" style="display:none">
|
||
<progress id="upload-progress" class="progress is-primary is-small" value="0" max="100">0%</progress>
|
||
</div>
|
||
<div id="upload-error" class="notification is-danger is-light is-small mt-2" style="display:none"></div>
|
||
<div class="field"><button class="button is-primary is-small" type="button" id="upload-btn" onclick="startUpload()">Hochladen</button></div>
|
||
</form>
|
||
|
||
<hr>
|
||
<h2 class="title is-5 mb-3">Vorhandene Medien</h2>
|
||
{{if .Assets}}
|
||
<div style="overflow-x:auto">
|
||
<table class="table is-fullwidth is-hoverable">
|
||
<thead><tr style="font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;color:#6b7280">
|
||
<th>Typ</th><th>Titel</th><th>Größe</th><th></th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{{range .Assets}}
|
||
<tr>
|
||
<td>{{typeIcon .Type}}</td>
|
||
<td>{{.Title}}</td>
|
||
<td class="has-text-grey" style="font-size:.8rem">{{if .SizeBytes}}{{humanSize .SizeBytes}}{{else}}–{{end}}</td>
|
||
<td>
|
||
<form method="POST" action="/tenant/{{$.Tenant.Slug}}/media/{{.ID}}/delete"
|
||
onsubmit="return confirm('Medium löschen?')">
|
||
<button class="button is-small is-danger is-outlined" type="submit">Löschen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{{else}}
|
||
<p class="has-text-grey">Noch keine Medien hochgeladen.</p>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<script>
|
||
// ─── Tab switching ─────────────────────────────────────────────
|
||
(function() {
|
||
var tabs = document.querySelectorAll('#dash-tabs li[data-tab]');
|
||
tabs.forEach(function(tab) {
|
||
tab.addEventListener('click', function() {
|
||
tabs.forEach(function(t) { t.classList.remove('is-active'); });
|
||
tab.classList.add('is-active');
|
||
document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('is-active'); });
|
||
document.getElementById('tab-' + tab.dataset.tab).classList.add('is-active');
|
||
history.replaceState(null, '', '?tab=' + tab.dataset.tab);
|
||
});
|
||
});
|
||
var tp = new URLSearchParams(window.location.search).get('tab');
|
||
if (tp) { var t = document.querySelector('#dash-tabs li[data-tab="' + tp + '"]'); if (t) t.click(); }
|
||
})();
|
||
|
||
// ─── Upload type toggle ────────────────────────────────────────
|
||
function toggleTypeFields() {
|
||
var t = document.getElementById('upload-type').value;
|
||
document.getElementById('file-field').style.display = t === 'web' ? 'none' : '';
|
||
document.getElementById('url-field').style.display = t === 'web' ? '' : 'none';
|
||
}
|
||
|
||
function handleDrop(e) {
|
||
e.preventDefault(); e.currentTarget.classList.remove('dragover');
|
||
var inp = document.getElementById('upload-file');
|
||
if (inp && e.dataTransfer.files.length) {
|
||
inp.files = e.dataTransfer.files;
|
||
var p = document.getElementById('drop-fn');
|
||
p.textContent = e.dataTransfer.files[0].name; p.style.display = '';
|
||
}
|
||
}
|
||
|
||
function startUpload() {
|
||
var t = document.getElementById('upload-type').value;
|
||
if (t === 'web') { document.getElementById('upload-form').submit(); return; }
|
||
var form = document.getElementById('upload-form');
|
||
var inp = document.getElementById('upload-file');
|
||
var btn = document.getElementById('upload-btn');
|
||
var pg = document.getElementById('upload-progress-wrap');
|
||
var bar = document.getElementById('upload-progress');
|
||
var err = document.getElementById('upload-error');
|
||
err.style.display='none';
|
||
if (!inp.files || !inp.files.length) { err.textContent='Bitte eine Datei auswählen.';err.style.display='';return; }
|
||
var fd = new FormData(form);
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.upload.onprogress = function(e) { if (e.lengthComputable) bar.value=Math.round(e.loaded/e.total*100); };
|
||
xhr.onloadstart = function() { btn.style.display='none'; pg.style.display=''; bar.value=0; };
|
||
xhr.onload = function() {
|
||
if (xhr.status>=200&&xhr.status<400) { window.location.href=window.location.pathname+'?tab=media&flash=uploaded'; }
|
||
else { pg.style.display='none';btn.style.display='';err.textContent='Upload fehlgeschlagen: '+xhr.responseText;err.style.display=''; }
|
||
};
|
||
xhr.onerror = function() { pg.style.display='none';btn.style.display='';err.textContent='Netzwerkfehler.';err.style.display=''; };
|
||
xhr.open('POST', form.action); xhr.send(fd);
|
||
}
|
||
|
||
// ─── Status polling ────────────────────────────────────────────
|
||
(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;
|
||
data.screens.forEach(function(s){
|
||
var el = document.getElementById('status-' + s.screen_id);
|
||
if (!el) return;
|
||
var st = s.derived_state || 'unknown';
|
||
el.className = 'status-dot ' + (st==='online'?'online':st==='degraded'?'stale':'offline');
|
||
el.title = st;
|
||
});
|
||
}).catch(function(){});
|
||
}
|
||
update(); setInterval(update, 30000);
|
||
})();
|
||
|
||
// ─── CSRF injection ────────────────────────────────────────────
|
||
function getCsrf() { var m=document.cookie.match('(?:^|; )morz_csrf=([^;]*)');return m?decodeURIComponent(m[1]):''; }
|
||
function injectCSRF() {
|
||
var t=getCsrf();if(!t)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=t;f.appendChild(i);}
|
||
});
|
||
}
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',injectCSRF);else injectCSRF();
|
||
|
||
// ─── Toast ─────────────────────────────────────────────────────
|
||
function showToast(msg,type){
|
||
var t=document.createElement('div');t.className='morz-toast '+(type||'is-success');
|
||
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(){
|
||
var flash = new URLSearchParams(window.location.search).get('flash');
|
||
if (flash==='uploaded') showToast('✓ Medium erfolgreich hochgeladen.');
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>`
|