feat(quick-260921-pi9): Bilderrahmen-Widget - Diashow mit Grossansicht, Bildverwaltung in den Einstellungen

- picture-frame-config.ts: Eintragstyp als Vereinigung (upload | url) in EINER
  geordneten Liste, resolvePictureFrameConfig laesst alles ausser https weg
  (T-PI9-07), Intervall 0 oder 5..3600 s, pickNextIndex (Zufall nie dasselbe)
- dashboard-images-api.ts: Upload als FormData-Feld image ohne eigenen
  Content-Type, 413 -> deutsche Meldung, Proxy-Pfad fuer <img src>
- PictureFrameWidget: Leerzustand, <img referrerPolicy=no-referrer> (Browser
  laedt Fremdbilder, Server nie), object-contain/cover, Unterschrift-Streifen,
  Wechsel per Timer mit Raeumung, kaputte Bilder verlassen den Umlauf,
  Grossansicht nur ausserhalb des Bearbeitungsmodus mit Fokus-Rueckgabe und
  Pause des Wechsels; im Bearbeitungsmodus kein Knopf (Karte bleibt Griff)
- PictureFrameConfigForm im WidgetSettingsPanel: Ausschnitt, Intervall,
  Reihenfolge, Liste mit Vorschau/Unterschrift/Pfeilen/Entfernen (Upload wird
  auch serverseitig geloescht), Datei hochladen, https-Adresse hinzufuegen
- Registry (4x4 min, 8x8 Vorgabe), Katalog, Seite, Uebersetzungen de/en
  (widgets.pictureFrame, 28 Schluessel), bestehende Tests auf acht Typen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-21 18:56:02 +02:00
parent 737974b653
commit c080580459
18 changed files with 1688 additions and 4 deletions
+76
View File
@@ -0,0 +1,76 @@
/**
* dashboard-images-api — Aufrufe des Bilderrahmen-Widgets gegen
* /dashboard/images (quick-260921-pi9). Muster favorites-api.ts: Cookie
* immer mit (`credentials: 'include'`).
*
* `uploadDashboardImage` schickt `multipart/form-data` mit genau dem Feld
* `image` und OHNE eigenen Content-Type-Header — die Multipart-Grenze setzt
* der Browser (Muster bug-report-api.ts). Die deutsche Meldung fuer „zu
* gross“ entsteht HIER aus dem Status 413 (multer meldet englisch), die
* Meldungen fuer 400 kommen von der API (bereits deutsch).
*
* `dashboardImageSrc` liefert den Proxy-Pfad fuer `<img src>`: der
* Next-Rewrite `/api-proxy/:path*` (next.config.ts) reicht Cookies mit,
* die Bytes kommen aus Tesseras eigenem Origin (Muster FavoriteIcon).
*/
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
export interface DashboardImageMeta {
id: string;
originalName: string;
mimeType: string;
size: number;
createdAt: string;
}
export async function fetchDashboardImages(): Promise<DashboardImageMeta[]> {
const res = await fetch(`${API_URL}/dashboard/images`, { credentials: 'include' });
if (!res.ok) throw new Error('Failed to fetch dashboard images');
return res.json();
}
export async function uploadDashboardImage(file: File): Promise<DashboardImageMeta> {
const body = new FormData();
body.append('image', file, file.name);
const res = await fetch(`${API_URL}/dashboard/images`, {
method: 'POST',
credentials: 'include',
body,
});
if (res.ok) return res.json();
if (res.status === 413) {
throw new Error('Die Datei ist zu groß – erlaubt sind höchstens 5 MB.');
}
if (res.status === 400) {
const message = await readMessage(res);
if (message) throw new Error(message);
}
throw new Error('Das Bild konnte nicht hochgeladen werden.');
}
export async function deleteDashboardImage(id: string): Promise<void> {
const res = await fetch(`${API_URL}/dashboard/images/${encodeURIComponent(id)}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to delete dashboard image');
}
export function dashboardImageSrc(id: string): string {
return `/api-proxy/dashboard/images/${encodeURIComponent(id)}`;
}
async function readMessage(res: Response): Promise<string | null> {
try {
const data: unknown = await res.json();
if (typeof data === 'object' && data !== null && 'message' in data) {
const message = (data as { message: unknown }).message;
if (typeof message === 'string' && message !== '') return message;
}
} catch {
// kein JSON — allgemeine Meldung
}
return null;
}