diff --git a/server/backend/internal/httpapi/manage/templates.go b/server/backend/internal/httpapi/manage/templates.go index 24566c5..d671750 100644 --- a/server/backend/internal/httpapi/manage/templates.go +++ b/server/backend/internal/httpapi/manage/templates.go @@ -1,5 +1,133 @@ package manage +const provisionTmpl = ` + + + + + Einrichten – {{.Screen.Name}} + + + + + + +
+
+ +
+ ✓ Screen «{{.Screen.Name}}» ({{.Screen.Slug}}) wurde im Backend angelegt.
+ Führe die folgenden Schritte auf deinem Ansible-Host aus, um den Bildschirm zu provisionieren. +
+ + +
+
+ 1 +
+

Host zur Ansible-Inventardatei hinzufügen

+

Öffne ansible/inventory.yml und füge den Host unter signage_players → hosts ein:

+
        {{.Screen.Slug}}:
+ +
+
+
+ + +
+
+ 2 +
+

Host-Variablen anlegen

+

Erstelle die Datei ansible/host_vars/{{.Screen.Slug}}/vars.yml mit folgendem Inhalt:

+
---
+ansible_host: {{.IP}}
+ansible_user: {{.SSHUser}}
+screen_id: {{.Screen.Slug}}
+screen_name: "{{.Screen.Name}}"
+screen_orientation: {{.Orientation}}
+ +

Tipp: mkdir -p ansible/host_vars/{{.Screen.Slug}}

+
+
+
+ + +
+
+ 3 +
+

SSH-Zugang sicherstellen

+

Stelle sicher, dass dein SSH-Key auf dem Zielgerät hinterlegt ist:

+
ssh-copy-id {{.SSHUser}}@{{.IP}}
+ +
+
+
+ + +
+
+ 4 +
+

Ansible-Playbook ausführen

+

Führe das Playbook vom Projektverzeichnis aus aus. Das installiert den Agent, konfiguriert Chromium und startet den Kiosk-Modus:

+
cd /path/to/morz-infoboard
+ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit {{.Screen.Slug}}
+ +

+ Falls du einen Vault-Pass verwendest: + --vault-password-file ansible/.vault_pass +

+
+
+
+ + +
+
+ 5 +
+

Fertig — Playlist befüllen

+

Nach erfolgreichem Ansible-Lauf meldet sich der Bildschirm automatisch im Backend an und lädt seine Playlist. Jetzt kannst du Inhalte zuweisen:

+ Playlist für «{{.Screen.Name}}» verwalten → +
+
+
+ +
+
+ + + +` + const adminTmpl = ` @@ -57,24 +185,45 @@ const adminTmpl = `
-

Neuer Bildschirm

-
-
+

Neuen Bildschirm einrichten

+

+ Fülle die Angaben aus. Der Bildschirm wird im Backend angelegt und du erhältst + eine Schritt-für-Schritt-Anleitung mit allen nötigen Befehlen + für das Ansible-Deployment. +

+ +
- +
-
-

URL-sichere Kennung (eindeutig)

+

Eindeutig, URL-sicher — wird als screen_id verwendet

-
+
- +
- + +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+
@@ -91,16 +240,56 @@ const adminTmpl = `
-
-
- -
- +
+ + +
+ +
+

Bestehenden Screen manuell anlegen

+
+ Nur DB-Eintrag, kein Deployment (aufklappen) +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
-
- + +
diff --git a/server/backend/internal/httpapi/manage/ui.go b/server/backend/internal/httpapi/manage/ui.go index 6c52afe..1e933d0 100644 --- a/server/backend/internal/httpapi/manage/ui.go +++ b/server/backend/internal/httpapi/manage/ui.go @@ -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) { diff --git a/server/backend/internal/httpapi/router.go b/server/backend/internal/httpapi/router.go index d5ebb86..609d3f9 100644 --- a/server/backend/internal/httpapi/router.go +++ b/server/backend/internal/httpapi/router.go @@ -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))