morz-infoboard/server/backend/internal/httpapi/screenshot.go
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

59 lines
1.4 KiB
Go

package httpapi
import (
"io"
"net/http"
)
const maxScreenshotSize = 3 << 20 // 3 MB
func handlePlayerScreenshot(store *ScreenshotStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxScreenshotSize)
if err := r.ParseMultipartForm(maxScreenshotSize); err != nil {
http.Error(w, "bad multipart form", http.StatusBadRequest)
return
}
screenID := r.FormValue("screen_id")
if screenID == "" {
http.Error(w, "screen_id required", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("screenshot")
if err != nil {
http.Error(w, "screenshot file required", http.StatusBadRequest)
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
http.Error(w, "read error", http.StatusInternalServerError)
return
}
mimeType := header.Header.Get("Content-Type")
if mimeType == "" {
mimeType = "image/png"
}
store.Save(screenID, data, mimeType)
w.WriteHeader(http.StatusOK)
}
}
func handleGetScreenshot(store *ScreenshotStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
screenID := r.PathValue("screenId")
data, mimeType, ok := store.Get(screenID)
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", mimeType)
w.Header().Set("Cache-Control", "no-store")
w.Write(data) //nolint:errcheck
}
}