morz-infoboard/server/backend/internal/httpapi/tenant/templates.go
Jesko Anschütz fb8d598e9e Tenant-Feature Phase 3c + Phase 4: Register-Fix + Tenant-Dashboard UI
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>
2026-03-23 18:08:32 +01:00

299 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>`