Logo-Upload sichtbar machen; empfohlene Abmessungen angeben

Fehler: Ein hochgeladenes Logo blieb unsichtbar, auch nach dem Neuladen.
Die Oberflaeche prueft per Anfrage, ob ein Bild hinterlegt ist, und nutzte dafuer
HEAD. FastAPI registriert fuer eine GET-Route aber - anders als Starlette - kein
HEAD; die Anfrage lief in ein 405, und jedes hinterlegte Bild galt als "nicht
vorhanden". Der Upload selbst war die ganze Zeit in Ordnung.

- Neuer Endpunkt GET /branding liefert nur den Status ({"logo": true, ...}),
  ohne Bilddaten zu uebertragen. Die Oberflaeche fragt jetzt diesen ab.
- Die Bild-Route beantwortet zusaetzlich HEAD, damit sie sich erwartungsgemaess
  verhaelt.

Ausserdem:
- Die empfohlenen Abmessungen stehen jetzt sichtbar ueber der Vorschau
  (Logo etwa 400x72 px, Favicon 64x64 px, jeweils hoechstens 512 KB).
- Nach dem Hochladen wird die tatsaechliche Bildgroesse angezeigt, damit man
  sie mit der Empfehlung vergleichen kann.

Geprueft: "npm run build" laeuft durch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 08:55:17 +02:00
parent 903392e953
commit 4448718d8d
4 changed files with 54 additions and 12 deletions

View File

@@ -40,7 +40,19 @@ def _check_kind(kind: str) -> str:
return kind return kind
@router.get("/{kind}") @router.get("")
def branding_status(db: Session = Depends(get_db)) -> dict[str, bool]:
"""Welche eigenen Bilder hinterlegt sind ohne sie zu übertragen.
Vorher fragte die Oberfläche das per HEAD auf das Bild ab. FastAPI
registriert für eine GET-Route aber kein HEAD (anders als Starlette), die
Anfrage lief also in ein 405 und jedes hinterlegte Logo galt als "nicht da".
"""
vorhanden = {asset.kind for asset in db.query(BrandingAsset).all()}
return {kind: kind in vorhanden for kind in KINDS}
@router.api_route("/{kind}", methods=["GET", "HEAD"])
def get_branding(kind: str, db: Session = Depends(get_db)) -> Response: def get_branding(kind: str, db: Session = Depends(get_db)) -> Response:
"""Liefert das hinterlegte Bild oder 404, wenn keines gesetzt ist. """Liefert das hinterlegte Bild oder 404, wenn keines gesetzt ist.

View File

@@ -8,14 +8,28 @@ export function brandingUrl(kind, version) {
return `/api/branding/${kind}${version ? `?v=${version}` : ""}`; return `/api/branding/${kind}${version ? `?v=${version}` : ""}`;
} }
/**
* Fragt beim Server ab, welche eigenen Bilder hinterlegt sind.
*
* Frueher lief das als HEAD-Anfrage auf das Bild selbst. FastAPI beantwortet
* eine GET-Route aber nicht automatisch auch mit HEAD die Anfrage endete in
* einem 405, und ein hochgeladenes Logo wurde nie angezeigt. Jetzt liefert der
* Server einen kleinen Status, ganz ohne Bilddaten.
*/
export async function brandingStatus() {
try {
const resp = await fetch("/api/branding");
if (!resp.ok) return {};
return await resp.json();
} catch {
return {}; // Server nicht erreichbar: eingebautes Zeichen genügt.
}
}
/** Prüft, ob ein eigenes Bild hinterlegt ist (ohne es zu laden). */ /** Prüft, ob ein eigenes Bild hinterlegt ist (ohne es zu laden). */
export async function hasBranding(kind) { export async function hasBranding(kind) {
try { const status = await brandingStatus();
const resp = await fetch(brandingUrl(kind), { method: "HEAD" }); return Boolean(status[kind]);
return resp.ok;
} catch {
return false; // Server nicht erreichbar: eingebautes Zeichen genügt.
}
} }
/** /**

View File

@@ -7,11 +7,13 @@ import Icon from "./Icon";
* Ein Bild der Installation setzen oder wieder entfernen (Logo bzw. Favicon). * Ein Bild der Installation setzen oder wieder entfernen (Logo bzw. Favicon).
* Ohne eigenes Bild bleibt das eingebaute Zeichen der Oberfläche stehen. * Ohne eigenes Bild bleibt das eingebaute Zeichen der Oberfläche stehen.
*/ */
export default function BrandingUpload({ kind, title, hint, onChanged }) { export default function BrandingUpload({ kind, title, hint, empfehlung, onChanged }) {
const [vorhanden, setVorhanden] = useState(false); const [vorhanden, setVorhanden] = useState(false);
const [version, setVersion] = useState(0); const [version, setVersion] = useState(0);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [fehler, setFehler] = useState(null); const [fehler, setFehler] = useState(null);
// Tatsaechliche Bildgroesse - damit man sieht, ob sie zur Empfehlung passt.
const [groesse, setGroesse] = useState(null);
const dateiFeld = useRef(null); const dateiFeld = useRef(null);
useEffect(() => { useEffect(() => {
@@ -55,13 +57,23 @@ export default function BrandingUpload({ kind, title, hint, onChanged }) {
return ( return (
<div style={{ marginBottom: "var(--sp-4)" }}> <div style={{ marginBottom: "var(--sp-4)" }}>
<label style={{ marginBottom: "var(--sp-2)" }}>{title}</label> <label style={{ marginBottom: "var(--sp-2)" }}>{title}</label>
{empfehlung && (
<p className="muted small mt-0"><strong>Empfohlen:</strong> {empfehlung}</p>
)}
<div className="brand-preview"> <div className="brand-preview">
{vorhanden ? ( {vorhanden ? (
<img src={brandingUrl(kind, version)} alt={title} /> <img src={brandingUrl(kind, version)} alt={title}
onLoad={(e) => setGroesse({
w: e.currentTarget.naturalWidth,
h: e.currentTarget.naturalHeight,
})} />
) : ( ) : (
<span className="muted small">Kein eigenes Bild Standard wird verwendet</span> <span className="muted small">Kein eigenes Bild Standard wird verwendet</span>
)} )}
</div> </div>
{vorhanden && groesse?.w > 0 && (
<p className="muted small mt-0">Hochgeladen: {groesse.w} × {groesse.h} px</p>
)}
<p className="muted small mt-0">{hint}</p> <p className="muted small mt-0">{hint}</p>
{fehler && <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>} {fehler && <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>}
<div className="field-inline"> <div className="field-inline">

View File

@@ -86,13 +86,17 @@ export default function Settings() {
<BrandingUpload <BrandingUpload
kind="logo" kind="logo"
title="Eigenes Logo" title="Eigenes Logo"
hint="Ersetzt Zeichen und Schriftzug oben in der Seitenleiste. Die Höhe wird empfehlung="etwa 400 × 72 px, PNG oder SVG mit durchsichtigem Hintergrund
begrenzt, damit das Layout erhalten bleibt. Ohne Angabe bleibt das (höchstens 512 KB)"
eingebaute Zeichen stehen." hint="Ersetzt Zeichen und Schriftzug oben in der Seitenleiste. Dargestellt wird
es mit 36 px Höhe und höchstens 200 px Breite die doppelte Größe sorgt
für ein scharfes Bild auf hochauflösenden Bildschirmen. Ohne Angabe bleibt
das eingebaute Zeichen stehen."
/> />
<BrandingUpload <BrandingUpload
kind="favicon" kind="favicon"
title="Eigenes Favicon" title="Eigenes Favicon"
empfehlung="quadratisch, 64 × 64 px, PNG, SVG oder ICO (höchstens 512 KB)"
hint="Das Symbol im Browser-Tab. Ohne Angabe wird das Standardzeichen verwendet. hint="Das Symbol im Browser-Tab. Ohne Angabe wird das Standardzeichen verwendet.
Änderungen werden nach einem Neuladen der Seite sichtbar." Änderungen werden nach einem Neuladen der Seite sichtbar."
onChanged={applyFavicon} onChanged={applyFavicon}