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 */}