From f51b9cf8df2dfba07590b35cc63def689df98c4a Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Mon, 8 Jun 2026 17:38:01 +0200 Subject: [PATCH] fix(admin): graceful logo-img fallback + show sidebar widgets during perm hydration (#523 follow-up 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues Rekoo-PS hit immediately after upgrading to v3.60.3-beta.0: 1. **Broken logo URL rendered the browser's broken-image icon + alt text.** Their `` had no `onError` handler, so a 404 / slow logo URL produced the default broken-image rendering — which uses the `alt` attribute (`companyName`) as text. Visually it looked like the wordmark span had unexpectedly re-appeared on phone, even though the actual `` was correctly hidden by the existing `wordmarkVisibilityClass` logic. Fix: - `useState` tracks `logoLoadError` (first failure) and `fallbackLoadError` (second failure). On a configured-URL miss the `` swaps to the bundled `/picpeak-kamera-transparent.png`; on a second miss the `` is removed from the DOM entirely. - `useEffect([resolvedLogoUrl])` resets both flags when the URL changes, so a dark-mode toggle that flips `lightLogo ↔ darkLogo` gets a fresh attempt instead of being permanently sad. - `wordmarkVisibilityClass` now derives from `logoEffectivelyVisible` (showLogo && !fallbackLoadError) — when both the configured URL AND the bundled fallback have failed, the wordmark un-hides on ` fallback chain also covers the broader "logo hosted on a flaky CDN" case for self-hosters, not just the one-time post-upgrade asset-cache hiccup. Pure resilience polish — no behaviour change when everything works. --- frontend/src/components/admin/AdminHeader.tsx | 44 +++++++++++++++++-- .../src/components/admin/AdminSidebar.tsx | 16 +++++-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/admin/AdminHeader.tsx b/frontend/src/components/admin/AdminHeader.tsx index 2f5e7446..bf2402b5 100644 --- a/frontend/src/components/admin/AdminHeader.tsx +++ b/frontend/src/components/admin/AdminHeader.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Menu, User, LogOut, Settings, Bell, Lock, CheckCircle, Trash2, Sun, Moon, Globe, ChevronDown } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -51,11 +51,31 @@ export const AdminHeader: React.FC = ({ onMenuClick }) => { ? (logoUrl.startsWith('http') ? logoUrl : buildResourceUrl(logoUrl)) : '/picpeak-kamera-transparent.png'; + // #523 follow-up 2: graceful fallback when the configured logo URL + // 404s or stalls. Without an error handler, the failure draws + // the browser's default broken-image-icon + alt text rendering — see + // Rekoo-PS's 3.60.3-beta.0 screenshot where "Arkan Studio" appeared + // as the alt text of a broken icon, not the real wordmark span. The + // chain: + // 1. configured URL fails → try the bundled picpeak fallback + // 2. bundled fallback fails → hide the image entirely, let the + // wordmark carry the brand + // Reset on URL change so a dark-mode toggle (which can flip lightLogo + // ↔ darkLogo) retries the new URL instead of being permanently sad. + const [logoLoadError, setLogoLoadError] = useState(false); + const [fallbackLoadError, setFallbackLoadError] = useState(false); + useEffect(() => { + setLogoLoadError(false); + setFallbackLoadError(false); + }, [resolvedLogoUrl]); + const logoImgSrc = logoLoadError ? '/picpeak-kamera-transparent.png' : resolvedLogoUrl; + // Renders the logo + wordmark block per the current logo_display_mode. // Re-used in left / center / right slots below so all three positions // produce visually identical brand chrome. const showLogo = !logoInSidebar && (logoDisplayMode === 'logo_only' || logoDisplayMode === 'logo_and_text'); const showText = logoDisplayMode === 'text_only' || logoDisplayMode === 'logo_and_text'; + const logoEffectivelyVisible = showLogo && !fallbackLoadError; // On = ({ onMenuClick }) => { // cluster it overlaps the LanguageSelector button (#523 follow-up, // Rekoo-PS's "Arkan Studio" screenshot in v3.59.0-beta.0). text_only // mode keeps the wordmark on every width — nothing else would render. - const wordmarkVisibilityClass = showLogo ? 'hidden sm:inline' : 'inline'; + // + // #523 follow-up 2: when both the configured URL AND the bundled + // fallback have failed (fallbackLoadError → logoEffectivelyVisible + // false), unhide the wordmark on { // Skeleton placeholder while `usePublicSettings()` is in flight (#523 // follow-up — Rekoo-PS's "logo took some time to load" screenshot in @@ -88,8 +113,19 @@ export const AdminHeader: React.FC = ({ onMenuClick }) => { // action buttons on narrow mobile widths (#523 regression). return (
- {showLogo && ( - {companyName} + {logoEffectivelyVisible && ( + {companyName} { + // First failure: configured URL → try the bundled fallback. + // Second failure: bundled fallback → hide entirely, let + // the wordmark carry the brand (#523 follow-up 2). + if (!logoLoadError) setLogoLoadError(true); + else setFallbackLoadError(true); + }} + /> )} {showText && ( {companyName} diff --git a/frontend/src/components/admin/AdminSidebar.tsx b/frontend/src/components/admin/AdminSidebar.tsx index bdc5e552..79aefddf 100644 --- a/frontend/src/components/admin/AdminSidebar.tsx +++ b/frontend/src/components/admin/AdminSidebar.tsx @@ -101,7 +101,7 @@ const navigation: NavItem[] = [ export const AdminSidebar: React.FC = ({ isOpen, onClose, collapsed = false, onToggleCollapse }) => { const location = useLocation(); const { t } = useTranslation(); - const { hasPermission } = usePermissions(); + const { hasPermission, isLoading: permissionsLoading } = usePermissions(); const { flags } = useFeatureFlags(); // Branding lookup for the "logo_position = sidepanel" mode — when // chosen, the logo replaces the "PicPeak Admin" text in the brand @@ -283,8 +283,18 @@ export const AdminSidebar: React.FC = ({ isOpen, onClose, col {/* Bottom section - sticky to bottom (only for users with settings.view permission). Hidden on desktop when collapsed since these widgets don't fit in the icon rail; - mobile keeps them visible because mobile width is always w-64. */} - {hasPermission('settings.view') && ( + mobile keeps them visible because mobile width is always w-64. + + #523 follow-up 2: render OPTIMISTICALLY while permissions are + still hydrating from the auth context (Rekoo-PS's 3.60.3-beta.0 + screenshot showed the whole bottom block missing on first paint + right after a deploy — `hasPermission` returns false during the + ~hundreds-of-ms hydration window, the widgets vanish entirely, + then re-appear). Only HIDE the block when we definitively know + the user lacks the permission. VersionInfo + StorageInfo each + have their own loading states so admins see "—" / a spinner + instead of nothing during the actual data fetch. */} + {(permissionsLoading || hasPermission('settings.view')) && (
{/* Version Info */}