Phase 3c:
- register.go: hardcoded "morz" durch cfg.DefaultTenantSlug ersetzt
Phase 4:
- neues Package httpapi/tenant: HandleTenantDashboard, HandleTenantUpload, HandleTenantDeleteMedia
- tenantDashTmpl: Navbar, zwei Tabs (Monitore/Mediathek), Status-Polling, Upload-Fortschritt
- router.go: /tenant/{tenantSlug}/... Routen hinter RequireAuth+RequireTenantAccess
- manage/templates.go: Abmelden-Button in Admin-UI und Manage-UI Navbar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
299 lines
10 KiB
Go
299 lines
10 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>Mein Dashboard – morz infoboard</title>
|
||
<link rel="stylesheet" href="/static/bulma.min.css">
|
||
<style>
|
||
body { background: #f5f5f5; min-height: 100vh; }
|
||
.navbar { margin-bottom: 0; }
|
||
.tab-content { display: none; }
|
||
.tab-content.is-active { display: block; }
|
||
.progress-bar-wrap { display: none; margin-top: .5rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
|
||
<div class="navbar-brand">
|
||
<span class="navbar-item"><strong>📺 Infoboard</strong></span>
|
||
</div>
|
||
<div class="navbar-menu">
|
||
<div class="navbar-end">
|
||
<div class="navbar-item">
|
||
<form method="POST" action="/logout">
|
||
<button class="button is-light is-small" 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">
|
||
<button class="delete" onclick="this.parentElement.remove()"></button>
|
||
{{.Flash}}
|
||
</div>
|
||
{{end}}
|
||
|
||
<div class="tabs is-boxed mb-0" id="dash-tabs">
|
||
<ul>
|
||
<li class="is-active" data-tab="monitors">
|
||
<a><span>Meine Monitore</span></a>
|
||
</li>
|
||
<li data-tab="media">
|
||
<a><span>Mediathek</span></a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Tab A: Meine Monitore -->
|
||
<div id="tab-monitors" class="tab-content is-active">
|
||
<div class="box" style="border-top-left-radius:0">
|
||
{{if .Screens}}
|
||
<div class="columns is-multiline">
|
||
{{range .Screens}}
|
||
<div class="column is-4">
|
||
<div class="card">
|
||
<div class="card-content">
|
||
<p class="title is-5">
|
||
{{if eq .Orientation "portrait"}}📱{{else}}🖥{{end}}
|
||
{{.Name}}
|
||
</p>
|
||
<p class="subtitle is-6 has-text-grey">
|
||
{{if eq .Orientation "portrait"}}Hochformat{{else}}Querformat{{end}}
|
||
</p>
|
||
<div id="status-{{.Slug}}" class="mb-3">
|
||
<span class="tag is-warning">Unbekannt</span>
|
||
</div>
|
||
</div>
|
||
<footer class="card-footer">
|
||
<a class="card-footer-item"
|
||
href="/manage/{{.Slug}}?from=tenant">
|
||
Playlist bearbeiten
|
||
</a>
|
||
</footer>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{else}}
|
||
<p class="has-text-grey">Noch keine Monitore zugewiesen.</p>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab B: Mediathek -->
|
||
<div id="tab-media" class="tab-content">
|
||
<div class="box" style="border-top-left-radius:0">
|
||
|
||
<h2 class="title is-5">Medium hochladen</h2>
|
||
|
||
<form id="upload-form" method="POST"
|
||
action="/tenant/{{.Tenant.Slug}}/upload"
|
||
enctype="multipart/form-data">
|
||
|
||
<div class="field">
|
||
<label class="label">Typ</label>
|
||
<div class="control">
|
||
<div class="select">
|
||
<select name="type" id="upload-type" onchange="toggleUploadFields()">
|
||
<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>
|
||
|
||
<div class="field">
|
||
<label class="label">Titel (optional)</label>
|
||
<div class="control">
|
||
<input class="input" type="text" name="title" placeholder="Wird aus Dateinamen abgeleitet">
|
||
</div>
|
||
</div>
|
||
|
||
<div id="file-field" class="field">
|
||
<label class="label">Datei</label>
|
||
<div class="control">
|
||
<input class="input" type="file" name="file" id="upload-file"
|
||
accept="image/*,video/*,application/pdf">
|
||
</div>
|
||
<div class="progress-bar-wrap" id="upload-progress-wrap">
|
||
<progress class="progress is-info" id="upload-progress" value="0" max="100"></progress>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="url-field" class="field" style="display:none">
|
||
<label class="label">URL</label>
|
||
<div class="control">
|
||
<input class="input" type="url" name="url" placeholder="https://...">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="control">
|
||
<button class="button is-primary" type="submit" id="upload-btn">Hochladen</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<hr>
|
||
|
||
<h2 class="title is-5">Vorhandene Medien</h2>
|
||
|
||
{{if .Assets}}
|
||
<div style="overflow-x:auto">
|
||
<table class="table is-fullwidth is-hoverable is-striped">
|
||
<thead>
|
||
<tr>
|
||
<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 is-size-7">
|
||
{{if .SizeBytes}}{{humanSize .SizeBytes}}{{else}}–{{end}}
|
||
</td>
|
||
<td>
|
||
<form method="POST"
|
||
action="/tenant/{{$.Tenant.Slug}}/media/{{.ID}}/delete"
|
||
onsubmit="return confirm('Wirklich 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><!-- /tab-media -->
|
||
|
||
</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');
|
||
// Sync URL ohne Reload
|
||
var url = new URL(window.location.href);
|
||
url.searchParams.set('tab', tab.dataset.tab);
|
||
history.replaceState(null, '', url.toString());
|
||
});
|
||
});
|
||
|
||
// Beim Laden ggf. Tab aus URL herstellen
|
||
var tabParam = new URLSearchParams(window.location.search).get('tab');
|
||
if (tabParam) {
|
||
var target = document.querySelector('#dash-tabs li[data-tab="' + tabParam + '"]');
|
||
if (target) target.click();
|
||
}
|
||
})();
|
||
|
||
// ── Upload-Formular Typ-Felder ────────────────────────────────────────────────
|
||
function toggleUploadFields() {
|
||
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';
|
||
}
|
||
|
||
// ── Upload-Fortschrittsbalken ─────────────────────────────────────────────────
|
||
(function() {
|
||
var form = document.getElementById('upload-form');
|
||
if (!form) return;
|
||
form.addEventListener('submit', function(e) {
|
||
var t = document.getElementById('upload-type').value;
|
||
if (t === 'web') return; // kein XHR für URL-Typ
|
||
var file = document.getElementById('upload-file').files[0];
|
||
if (!file) return;
|
||
e.preventDefault();
|
||
|
||
var wrap = document.getElementById('upload-progress-wrap');
|
||
var bar = document.getElementById('upload-progress');
|
||
var btn = document.getElementById('upload-btn');
|
||
wrap.style.display = 'block';
|
||
btn.disabled = true;
|
||
btn.textContent = 'Lädt hoch…';
|
||
|
||
var fd = new FormData(form);
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open('POST', form.action);
|
||
xhr.upload.addEventListener('progress', function(ev) {
|
||
if (ev.lengthComputable) {
|
||
bar.value = Math.round(ev.loaded / ev.total * 100);
|
||
}
|
||
});
|
||
xhr.addEventListener('load', function() {
|
||
if (xhr.status >= 200 && xhr.status < 400) {
|
||
window.location.href = window.location.pathname + '?tab=media&flash=uploaded';
|
||
} else {
|
||
alert('Upload fehlgeschlagen: ' + xhr.responseText);
|
||
btn.disabled = false;
|
||
btn.textContent = 'Hochladen';
|
||
wrap.style.display = 'none';
|
||
}
|
||
});
|
||
xhr.addEventListener('error', function() {
|
||
alert('Netzwerkfehler beim Upload.');
|
||
btn.disabled = false;
|
||
btn.textContent = 'Hochladen';
|
||
wrap.style.display = 'none';
|
||
});
|
||
xhr.send(fd);
|
||
});
|
||
})();
|
||
|
||
// ── Status-Polling alle 30 s ──────────────────────────────────────────────────
|
||
(function() {
|
||
function pollStatus() {
|
||
fetch('/api/v1/screens/status')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(list) {
|
||
if (!Array.isArray(list)) return;
|
||
list.forEach(function(s) {
|
||
var el = document.getElementById('status-' + s.screen_id);
|
||
if (!el) return;
|
||
var ok = s.status === 'ok' || s.status === 'online';
|
||
var cls = ok ? 'is-success' : 'is-danger';
|
||
var text = ok ? 'Online' : 'Offline';
|
||
el.innerHTML = '<span class="tag ' + cls + '">' + text + '</span>';
|
||
});
|
||
})
|
||
.catch(function() { /* ignore */ });
|
||
}
|
||
pollStatus();
|
||
setInterval(pollStatus, 30000);
|
||
})();
|
||
</script>
|
||
|
||
</body>
|
||
</html>`
|