morz-infoboard/docs/SERVER-KONZEPT.md
Jesko Anschütz b73da77835 feat(screens): Screen-Übersicht mit On-Demand-Screenshots für Multi-Screen-User
- GET /manage: neue Übersichtsseite mit Bulma-Karten für screen_user mit ≥2 Screens
- handleScreenUserRedirect leitet bei ≥2 Screens auf /manage statt auf ersten Screen
- On-Demand-Screenshot-Flow via MQTT:
  - Backend publiziert signage/screen/{slug}/screenshot-request beim Seitenaufruf
  - Player-Agent empfängt Topic, ruft TakeAndSendOnce() auf
  - Player POST /api/v1/player/screenshot → Backend speichert in ScreenshotStore (RAM)
  - GET /api/v1/screens/{screenId}/screenshot liefert gespeichertes Bild (authOnly)
- ScreenshotStore: In-Memory, thread-safe, kein Persistenz-Overhead
- JS-Retry nach 4s in Templates (Screenshot braucht 1-3s für MQTT-Roundtrip)
- manageTmpl zeigt Screenshot-Thumbnail beim Einzelscreen-Aufruf
- Doku: neue Endpoints, MQTT-Topics, Screenshot-Flow in SERVER-KONZEPT.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 14:27:10 +01:00

438 lines
13 KiB
Markdown
Raw Permalink 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.

# Info-Board Neu - Server-Konzept
## Ziel
Der Server ist die zentrale Steuer- und Verwaltungsinstanz der Plattform.
Er soll:
- Benutzer, Firmen und Screens verwalten
- Medien und Playlists bereitstellen
- globale Templates und Kampagnen steuern
- Provisionierung neuer Screens ausloesen
- Status, Screenshots und Heartbeats sammeln
- MQTT und HTTPS sauber trennen
## Grundprinzip
Der Server ist die Quelle der fachlichen Wahrheit.
Der Player bleibt trotzdem lauffaehig, wenn der Server temporaer nicht erreichbar ist.
Das bedeutet:
- Server verwaltet Konfiguration und Inhalte zentral
- Player fuehrt lokal und robust aus
- Server sendet Steuerimpulse, Player synchronisiert aktiv nach
## Hauptkomponenten
### Reverse Proxy
Aufgaben:
- TLS-Terminierung
- Routing fuer API und Web-UIs
- optionale Auth-/Header-Regeln
### Backend-API
Bevorzugte Sprache:
- `Go`
Aufgaben:
- Authentifizierung und Autorisierung
- CRUD fuer Tenants, Users, Screens, Medien, Playlists
- Verwaltung globaler Templates und Kampagnen
- Player-Sync-Endpunkte
- Speicherung von Status und Screenshots
- Start von Provisionierungsjobs
### Admin-UI
Aufgaben:
- Gesamtübersicht aller Screens
- Vorschau und Status
- Template-/Kampagnenverwaltung
- Kommandos und Provisionierung
### Tenant-UI
Aufgaben:
- Uploads und Medienverwaltung pro Firma
- Pflege der monitorbezogenen Playlist
- Vorschau des eigenen Screens
### Datenbank
Empfehlung:
- PostgreSQL
Aufgaben:
- Speicherung fachlicher Daten
- Status, Jobs, Revisionen, Zuordnungen
### MQTT-Broker
Empfehlung:
- Mosquitto
Aufgaben:
- Heartbeats
- Statusmeldungen
- Events
- Kommandos und ACKs
### MQTT-Topics (implementiert)
| Topic | Publisher | Subscriber | Beschreibung |
|----------------------------------------------|------------|---------------|---------------------------------------------------|
| `signage/screen/{slug}/playlist-changed` | Backend | Player-Agent | Benachrichtigung bei Playlist-Aenderung; Backend debounced 2 s |
| `signage/screen/{slug}/screenshot-request` | Backend | Player-Agent | Fordert sofortigen On-Demand-Screenshot an |
Der Backend-`Notifier` (`internal/mqttnotifier/notifier.go`) veroeffentlicht beide Topics.
Der Player-`Subscriber` (`player/agent/internal/mqttsubscriber/subscriber.go`) abonniert beide Topics fuer den eigenen Screen-Slug. Auf ein `screenshot-request`-Signal ruft der Agent `Screenshotter.TakeAndSendOnce(ctx)` auf und laedt das Bild direkt per `POST /api/v1/player/screenshot` hoch.
### Dateispeicher
Aufgaben:
- Uploads
- Screenshots
- ggf. serverseitig vorbereitete Medien
V1:
- Dateisystem ausreichend
## Fachliche Bereiche
## 1. Mandanten und Benutzer
Der Server trennt:
- globale Admins
- tenantgebundene Nutzer
Regel:
- Firmen sehen nur ihren Bereich
- Admins sehen alles
## 2. Screen-Verwaltung
Der Server kennt jeden Screen mit:
- ID
- Name
- Klasse
- Orientierung
- Rotation
- Tenant-Zuordnung
- technischem Registrierungsstatus
## 3. Medienverwaltung
Der Server verwaltet:
- Uploads
- externe Medienreferenzen
- Metadaten
- tenant- oder screenspezifische Zuordnung
## 4. Playlist-Verwaltung
Der Server verwaltet tenantbezogene Inhalte pro Screen.
Wichtige Felder:
- Reihenfolge
- Dauer
- `valid_from`
- `valid_until`
- Fehlerstrategie
- Cache-Politik
## 5. Template- und Kampagnenverwaltung
Der Server stellt den globalen Orchestrierungsbereich bereit.
Funktionen:
- Templates erstellen
- Szenen pflegen
- Zielgruppen waehlen
- Kampagnen aktivieren/deaktivieren
- Zeitfenster setzen
- Uebersteuerung sichtbar machen
## 6. Provisionierung
Der Server startet Provisionierungsjobs fuer neue Screens.
Aufgaben:
- Eingaben aus Admin-UI entgegennehmen
- Job anlegen
- Secret-Handling absichern
- Worker oder Jobrunner starten
- Fortschritt speichern
- Fehler sauber rueckmelden
## API-Bereiche
Die API soll mindestens diese Domänen haben:
- `auth`
- `tenants`
- `users`
- `screens`
- `media`
- `playlists`
- `templates`
- `campaigns`
- `player`
- `commands`
- `provisioning`
## Revisionsmodell
Der Server arbeitet mit Revisionen, damit Player effizient synchronisieren koennen.
Mindestens:
- `config_revision`
- `playlist_revision`
- `media_revision`
- `campaign_revision`
## Status- und Vorschaukonzept
Der Server speichert:
- letzten bekannten Heartbeat
- letzten Status
- letzten Screenshot (In-Memory, nicht persistiert)
- aktuelle Inhaltsquelle pro Screen
### Screenshot-Flow
1. Der Player-Agent sendet periodisch (Intervall: `MORZ_INFOBOARD_SCREENSHOT_EVERY`) einen Screenshot per `POST /api/v1/player/screenshot` (Multipart, kein Auth).
2. Alternativ kann das Backend per MQTT-Topic `signage/screen/{slug}/screenshot-request` einen On-Demand-Screenshot anfordern (`Notifier.RequestScreenshot(slug)`). Der Player-Agent empfaengt das Signal und ruft `Screenshotter.TakeAndSendOnce(ctx)` auf.
3. Das Backend speichert den Screenshot im `ScreenshotStore` (In-Memory, keyed by `screen_id`). Pro Screen wird nur der jeweils neueste Screenshot gehalten.
4. Eingeloggte Benutzer koennen den Screenshot unter `GET /api/v1/screens/{screenId}/screenshot` abrufen (`authOnly`). Der Response-Header enthaelt den vom Player gemeldeten MIME-Typ sowie `Cache-Control: no-store`.
Die Admin-UI soll damit erkennen:
- online/offline
- normaler tenantbezogener Betrieb
- globale Kampagnen-Uebersteuerung
- Fallback-Betrieb
- Fehlerzustand
## Provisionierungsjobrunner
Die Provisionierung soll nicht direkt in Web-Requests laufen.
Stattdessen:
- API legt Job an
- dedizierter Worker/Jobrunnner arbeitet ihn ab
- Fortschritt wird in DB gespeichert
Zusaetzlich fuer v1 festzulegen:
- ACK-Timeout-Handling fuer `device_commands` ueber Worker
- Secret-Handling fuer Provisionierungs-Bootstrap ueber kurzlebige Secret-Referenzen
- physische Netzposition des Workers fuer SSH-Erreichbarkeit als Betriebsparameter
## Docker-Compose-Zielbild
Sinnvolle Komponenten in `compose/`:
- `reverse-proxy`
- `backend`
- `postgres`
- `mosquitto`
- optional `worker`
## Authentifizierung
Der Server verwendet einen Session-basierten Login-Flow mit `bcrypt`-Passwort-Hashing.
### Login-Flow
1. `GET /login` rendert das Login-Formular (Bulma-Card zentriert).
2. `POST /login` prueft Username und Passwort:
- `AuthStore.GetUserByUsername` laedt den User inkl. Tenant-Slug.
- `bcrypt.CompareHashAndPassword` prueft das Passwort (Cost-Faktor 12).
- Bei Erfolg legt `AuthStore.CreateSession` eine Session an (TTL 24 Stunden).
- Das Session-Token wird als `morz_session`-Cookie gesetzt (`HttpOnly=true`, `Secure=true`).
- Im `DevMode` (`MORZ_INFOBOARD_DEV_MODE=true`) wird `Secure=false` gesetzt fuer lokalen HTTP-Betrieb.
- Weiterleitung je nach Rolle: `admin``/admin`, `tenant``/tenant/{slug}/dashboard`.
3. `POST /logout` loescht die Session in der DB und entfernt den Cookie.
### Cookie-Lebensdauer
- Standard-TTL: 24 Stunden
- Der Cookie verfaellt automatisch; die DB wird stuendlich durch `CleanExpiredSessions` bereinigt.
### Admin-User-Bootstrap
Beim Server-Start wird `EnsureAdminUser` aufgerufen, wenn `MORZ_INFOBOARD_ADMIN_PASSWORD` gesetzt ist.
Der Admin-User wird dem Tenant mit Slug `MORZ_INFOBOARD_DEFAULT_TENANT` (Standard: `morz`) zugeordnet.
Ist der User bereits vorhanden, passiert nichts. Fehler sind nicht fatal — der Server startet trotzdem.
---
## Middleware-Kette
Alle geschuetzten Routen werden durch Middleware-Funktionen in `internal/httpapi/middleware.go` abgesichert.
```
Eingehende Anfrage
RequireAuth Liest morz_session-Cookie, prueft Session via DB,
speichert *store.User im Request-Context.
→ Fehler: Redirect zu /login?next=<Pfad>
├─► RequireAdmin Prueft user.Role == "admin"
│ → Fehler: 403 Forbidden
├─► RequireTenant Prueft user.TenantSlug == {tenantSlug} aus dem URL-Pfad.
│ Access Admins duerfen immer durch.
│ → Fehler: 403 Forbidden
└─► RequireScreen Enforces per-screen access control.
Access Admins duerfen auf alle Screens zugreifen.
Screen-User brauchen expliziten Eintrag in `user_screen_permissions`.
Tenant-User duerfen auf alle Screens ihres Tenants zugreifen.
→ Fehler: 404 Not Found (Screen) oder 403 Forbidden (kein Zugriff)
```
### Route-Gruppen im Router
| Gruppe | Middleware | Beispielrouten |
|----------------|----------------------------------------------------|---------------------------------------------|
| Oeffentlich | keine | `/healthz`, `/login`, `/api/v1/screens/register` |
| Auth-only | RequireAuth | `/api/v1/items/{itemId}`, `/api/v1/media/{id}` |
| Admin-only | RequireAuth + RequireAdmin | `/admin`, `/admin/screens/...` |
| Tenant-scoped | RequireAuth + RequireTenantAccess | `/tenant/{tenantSlug}/...`, `/api/v1/tenants/{tenantSlug}/...` |
| Screen-scoped | RequireAuth + RequireScreenAccess | `/manage/{screenSlug}/...`, `/api/v1/screens/{screenId}/playlist` |
Der Hilfsfunktion `chain(middlewares...)` in `router.go` wrappet Handler von aussen nach innen.
---
## RequireScreenAccess Middleware
Die Middleware `RequireScreenAccess` erzwingt Zugriffskontrolle auf Screen-Ressourcen und wird ausschliesslich fuer Routen verwendet, deren Handler screen-spezifische Operationen durchfuehren.
**Verhalten:**
- **Admin-User**: duerfen auf alle Screens zugreifen (Bypass).
- **Screen-User**: duerfen nur auf Screens zugreifen, fuer die sie einen expliziten Eintrag in `user_screen_permissions` haben.
- **Tenant-User**: duerfen auf alle Screens ihres Tenants zugreifen (noch nicht vollstaendig implementiert).
**Implementierung:**
Die Middleware extrahiert den `screenSlug` aus dem URL-Pfad-Parameter, schlaegt den Screen in der Datenbank auf und prueft via `ScreenStore.HasUserScreenAccess()`, ob der Nutzer Zugriff hat. Admins umgehen diese Pruefung.
**Fehlerbehandlung:**
- Screen nicht gefunden: `404 Not Found`
- Kein Zugriff auf Screen: `403 Forbidden`
**Verwendung in Router:**
Die `authScreen`-Middleware-Kombination wird fuer folgende Routes verwendet:
- `GET /manage/{screenSlug}` — Playlist-Management-UI
- `POST /manage/{screenSlug}/upload` — Medium hochladen
- `POST /manage/{screenSlug}/items` — Item hinzufuegen
- `POST /manage/{screenSlug}/items/{itemId}` — Item aktualisieren
- `POST /manage/{screenSlug}/items/{itemId}/delete` — Item loeschen
- `POST /manage/{screenSlug}/reorder` — Items reordnen
- `POST /manage/{screenSlug}/media/{mediaId}/delete` — Medium loeschen
---
## Tenant-Dashboard
Das Tenant-Self-Service-Dashboard ist unter `/tenant/{tenantSlug}/dashboard` erreichbar.
### URL-Schema
| Methode | Pfad | Beschreibung |
|---------|---------------------------------------------|---------------------------|
| GET | `/tenant/{tenantSlug}/dashboard` | Dashboard rendern |
| POST | `/tenant/{tenantSlug}/upload` | Medium hochladen |
| POST | `/tenant/{tenantSlug}/media/{mediaId}/delete` | Medium loeschen |
### Tabs
- **Tab A Meine Monitore:** Zeigt Screen-Karten mit Live-Status. Der Status wird per JavaScript
aus `GET /api/v1/screens/status` geladen und alle 30 Sekunden aktualisiert.
Status-Badge: `is-success` (online), `is-danger` (offline), `is-warning` (unbekannt).
- **Tab B Mediathek:** Upload-Formular (Bild, Video, PDF oder Web-URL) und Dateiliste
mit Loeschen-Button. Nach Upload oder Loeschen Redirect mit `?tab=media&flash=uploaded/deleted`.
### Eigentuemer-Pruefung beim Loeschen
`HandleTenantDeleteMedia` prueft, dass `asset.TenantID == tenant.ID`, bevor es loescht.
Damit ist sichergestellt, dass ein Tenant keine Assets anderer Tenants loeschen kann,
selbst wenn er die `mediaId` erraten wuerde.
---
## Sicherheitsgrundsaetze
- Root-Bootstrap-Geheimnisse nur kurzlebig oder referenziert speichern
- API- und MQTT-Zugaenge getrennt behandeln
- alle Admin-Kommandos auditieren
- Tenant-Trennung strikt serverseitig erzwingen
## API-Fehlermodell
Vor Implementierungsbeginn gilt ein einheitlicher Fehlerumschlag.
Empfehlung:
```json
{
"error": {
"code": "screen_not_found",
"message": "Screen existiert nicht",
"details": null
}
}
```
## Zielstruktur im Repo
Empfohlene Unterstruktur fuer den Server:
- `server/backend/`
- `server/admin-ui/`
- `server/tenant-ui/`
- `server/worker/`
- `compose/`
## Testfaelle fuer den Server
- Tenant sieht nur eigene Daten
- Admin sieht alle Daten
- Kampagne ueberschreibt tenantbezogenen Content korrekt
- Screen-Provisionierung legt Job sauber an
- Player-Sync ueber Revisionen funktioniert
- MQTT-Kommandos werden protokolliert
- Screenshot-Upload erscheint im Admin-Dashboard