- 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>
438 lines
13 KiB
Markdown
438 lines
13 KiB
Markdown
# 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
|