Admin-UI: Bildschirm einrichten mit Ansible-Anleitung (Variante A)
- POST /admin/screens/provision: legt Screen in DB an (Upsert) und zeigt
eine 5-Schritt-Seite mit kopierbaren Code-Blöcken:
1. inventory.yml Eintrag
2. host_vars/{slug}/vars.yml Inhalt
3. ssh-copy-id Befehl
4. ansible-playbook Befehl (mit Vault-Passwort-Hinweis)
5. Link zur Playlist-Verwaltung
- Admin-Formular: IP-Adresse + SSH-User Felder ergänzt
- Altes "nur anlegen"-Formular als aufklappbaren Details-Block versteckt
- Clipboard-Copy-Buttons für jeden Code-Block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e03948f25d
commit
12c10f0337
3 changed files with 256 additions and 16 deletions
|
|
@ -1,5 +1,133 @@
|
|||
package manage
|
||||
|
||||
const provisionTmpl = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Einrichten – {{.Screen.Name}}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
|
||||
<style>
|
||||
body { background: #f5f5f5; }
|
||||
pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 4px;
|
||||
overflow-x: auto; font-size: 0.9em; line-height: 1.5; }
|
||||
.step-number { background: #3273dc; color: #fff; border-radius: 50%;
|
||||
width: 2rem; height: 2rem; display: inline-flex;
|
||||
align-items: center; justify-content: center;
|
||||
font-weight: bold; margin-right: 0.5rem; flex-shrink: 0; }
|
||||
.step { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: 2rem; }
|
||||
.step-body { flex: 1; }
|
||||
.copy-btn { cursor: pointer; font-size: 0.75em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-dark">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/admin">← Admin</a>
|
||||
<span class="navbar-item"><strong>Bildschirm einrichten: {{.Screen.Name}}</strong></span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="section">
|
||||
<div class="container" style="max-width:860px">
|
||||
|
||||
<div class="notification is-success is-light">
|
||||
<strong>✓ Screen «{{.Screen.Name}}» ({{.Screen.Slug}}) wurde im Backend angelegt.</strong><br>
|
||||
Führe die folgenden Schritte auf deinem Ansible-Host aus, um den Bildschirm zu provisionieren.
|
||||
</div>
|
||||
|
||||
<!-- Schritt 1 -->
|
||||
<div class="box">
|
||||
<div class="step">
|
||||
<span class="step-number">1</span>
|
||||
<div class="step-body">
|
||||
<p class="title is-6">Host zur Ansible-Inventardatei hinzufügen</p>
|
||||
<p class="mb-3">Öffne <code>ansible/inventory.yml</code> und füge den Host unter <code>signage_players → hosts</code> ein:</p>
|
||||
<pre id="inv"> {{.Screen.Slug}}:</pre>
|
||||
<button class="button is-small is-light copy-btn mt-2" onclick="copy('inv')">📋 Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 2 -->
|
||||
<div class="box">
|
||||
<div class="step">
|
||||
<span class="step-number">2</span>
|
||||
<div class="step-body">
|
||||
<p class="title is-6">Host-Variablen anlegen</p>
|
||||
<p class="mb-3">Erstelle die Datei <code>ansible/host_vars/{{.Screen.Slug}}/vars.yml</code> mit folgendem Inhalt:</p>
|
||||
<pre id="hostvars">---
|
||||
ansible_host: {{.IP}}
|
||||
ansible_user: {{.SSHUser}}
|
||||
screen_id: {{.Screen.Slug}}
|
||||
screen_name: "{{.Screen.Name}}"
|
||||
screen_orientation: {{.Orientation}}</pre>
|
||||
<button class="button is-small is-light copy-btn mt-2" onclick="copy('hostvars')">📋 Kopieren</button>
|
||||
<p class="help mt-2">Tipp: <code>mkdir -p ansible/host_vars/{{.Screen.Slug}}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 3 -->
|
||||
<div class="box">
|
||||
<div class="step">
|
||||
<span class="step-number">3</span>
|
||||
<div class="step-body">
|
||||
<p class="title is-6">SSH-Zugang sicherstellen</p>
|
||||
<p>Stelle sicher, dass dein SSH-Key auf dem Zielgerät hinterlegt ist:</p>
|
||||
<pre id="sshcopy">ssh-copy-id {{.SSHUser}}@{{.IP}}</pre>
|
||||
<button class="button is-small is-light copy-btn mt-2" onclick="copy('sshcopy')">📋 Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 4 -->
|
||||
<div class="box">
|
||||
<div class="step">
|
||||
<span class="step-number">4</span>
|
||||
<div class="step-body">
|
||||
<p class="title is-6">Ansible-Playbook ausführen</p>
|
||||
<p class="mb-3">Führe das Playbook vom Projektverzeichnis aus aus. Das installiert den Agent, konfiguriert Chromium und startet den Kiosk-Modus:</p>
|
||||
<pre id="playbookcmd">cd /path/to/morz-infoboard
|
||||
ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit {{.Screen.Slug}}</pre>
|
||||
<button class="button is-small is-light copy-btn mt-2" onclick="copy('playbookcmd')">📋 Kopieren</button>
|
||||
<p class="help mt-2">
|
||||
Falls du einen Vault-Pass verwendest:
|
||||
<code>--vault-password-file ansible/.vault_pass</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 5 -->
|
||||
<div class="box">
|
||||
<div class="step">
|
||||
<span class="step-number">5</span>
|
||||
<div class="step-body">
|
||||
<p class="title is-6">Fertig — Playlist befüllen</p>
|
||||
<p>Nach erfolgreichem Ansible-Lauf meldet sich der Bildschirm automatisch im Backend an und lädt seine Playlist. Jetzt kannst du Inhalte zuweisen:</p>
|
||||
<a class="button is-primary mt-3" href="/manage/{{.Screen.Slug}}">Playlist für «{{.Screen.Name}}» verwalten →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function copy(id) {
|
||||
var el = document.getElementById(id);
|
||||
navigator.clipboard.writeText(el.innerText).then(function() {
|
||||
var btn = el.nextElementSibling;
|
||||
var orig = btn.textContent;
|
||||
btn.textContent = '✓ Kopiert!';
|
||||
setTimeout(function() { btn.textContent = orig; }, 1500);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
const adminTmpl = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
|
|
@ -57,8 +185,71 @@ const adminTmpl = `<!DOCTYPE html>
|
|||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Neuer Bildschirm</h2>
|
||||
<form method="POST" action="/admin/screens">
|
||||
<h2 class="title is-5">Neuen Bildschirm einrichten</h2>
|
||||
<p class="mb-4 has-text-grey">
|
||||
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">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Slug / Hostname</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="slug" placeholder="z.B. info12" required
|
||||
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 class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Anzeigename</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="name" placeholder="z.B. Kantine EG" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<div class="field">
|
||||
<label class="label">IP-Adresse</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="ip" placeholder="10.0.0.X" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<div class="field">
|
||||
<label class="label">SSH-User</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="ssh_user" placeholder="morz" value="morz">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<div class="field">
|
||||
<label class="label">Format</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="orientation">
|
||||
<option value="landscape">Querformat</option>
|
||||
<option value="portrait">Hochformat</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="button is-primary" type="submit">Anlegen & Anleitung generieren →</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Bestehenden Screen manuell anlegen</h2>
|
||||
<details>
|
||||
<summary class="has-text-grey" style="cursor:pointer">Nur DB-Eintrag, kein Deployment (aufklappen)</summary>
|
||||
<form method="POST" action="/admin/screens" class="mt-4">
|
||||
<div class="columns is-vcentered">
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
|
|
@ -67,7 +258,6 @@ const adminTmpl = `<!DOCTYPE html>
|
|||
<input class="input" type="text" name="slug" placeholder="z.B. flur-eg" required
|
||||
pattern="[a-z0-9-]+" title="Nur Kleinbuchstaben, Zahlen und Bindestriche">
|
||||
</div>
|
||||
<p class="help">URL-sichere Kennung (eindeutig)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
|
|
@ -94,13 +284,12 @@ const adminTmpl = `<!DOCTYPE html>
|
|||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label"> </label>
|
||||
<div class="control">
|
||||
<button class="button is-primary is-fullwidth" type="submit">Erstellen</button>
|
||||
</div>
|
||||
<button class="button is-outlined is-fullwidth" type="submit">Nur anlegen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -165,6 +165,56 @@ func HandleCreateScreenUI(tenants *store.TenantStore, screens *store.ScreenStore
|
|||
}
|
||||
}
|
||||
|
||||
// HandleProvisionUI creates a screen in DB and shows the Ansible setup instructions.
|
||||
func HandleProvisionUI(tenants *store.TenantStore, screens *store.ScreenStore) http.HandlerFunc {
|
||||
t := template.Must(template.New("provision").Funcs(tmplFuncs).Parse(provisionTmpl))
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
slug := strings.TrimSpace(r.FormValue("slug"))
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
ip := strings.TrimSpace(r.FormValue("ip"))
|
||||
sshUser := strings.TrimSpace(r.FormValue("ssh_user"))
|
||||
orientation := r.FormValue("orientation")
|
||||
|
||||
if slug == "" || ip == "" {
|
||||
http.Error(w, "slug und IP-Adresse erforderlich", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
name = slug
|
||||
}
|
||||
if sshUser == "" {
|
||||
sshUser = "morz"
|
||||
}
|
||||
if orientation == "" {
|
||||
orientation = "landscape"
|
||||
}
|
||||
|
||||
tenant, err := tenants.Get(r.Context(), "morz")
|
||||
if err != nil {
|
||||
http.Error(w, "standard-tenant nicht gefunden", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
screen, err := screens.Upsert(r.Context(), tenant.ID, slug, name, orientation)
|
||||
if err != nil {
|
||||
http.Error(w, "DB-Fehler: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
t.Execute(w, map[string]any{ //nolint:errcheck
|
||||
"Screen": screen,
|
||||
"IP": ip,
|
||||
"SSHUser": sshUser,
|
||||
"Orientation": orientation,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HandleDeleteScreenUI handles DELETE for a screen, then redirects.
|
||||
func HandleDeleteScreenUI(screens *store.ScreenStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ func registerManageRoutes(mux *http.ServeMux, d RouterDeps) {
|
|||
|
||||
// ── Admin UI ──────────────────────────────────────────────────────────
|
||||
mux.HandleFunc("GET /admin", manage.HandleAdminUI(d.TenantStore, d.ScreenStore))
|
||||
mux.HandleFunc("POST /admin/screens/provision", manage.HandleProvisionUI(d.TenantStore, d.ScreenStore))
|
||||
mux.HandleFunc("POST /admin/screens", manage.HandleCreateScreenUI(d.TenantStore, d.ScreenStore))
|
||||
mux.HandleFunc("POST /admin/screens/{screenId}/delete", manage.HandleDeleteScreenUI(d.ScreenStore))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue