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:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user