requestStartTour('tools')}
+ const helpButtonNode = (
+
+
- );
- }
-
- // If admin, show menu with both options
- return (
-
-
) : null;
diff --git a/frontend/src/core/components/shared/quickAccessBar/useToursTooltip.ts b/frontend/src/core/components/shared/quickAccessBar/useToursTooltip.ts
new file mode 100644
index 000000000..bed983fdd
--- /dev/null
+++ b/frontend/src/core/components/shared/quickAccessBar/useToursTooltip.ts
@@ -0,0 +1,81 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { TOUR_STATE_EVENT, type TourStatePayload } from '@app/constants/events';
+import { isOnboardingCompleted, hasShownToursTooltip, markToursTooltipShown } from '@app/components/onboarding/orchestrator/onboardingStorage';
+
+export interface ToursTooltipState {
+ tooltipOpen: boolean | undefined;
+ manualCloseOnly: boolean;
+ showCloseButton: boolean;
+ toursMenuOpen: boolean;
+ setToursMenuOpen: (open: boolean) => void;
+ handleTooltipOpenChange: (next: boolean) => void;
+}
+
+/**
+ * Encapsulates all the logic for the tours tooltip:
+ * - Shows automatically after onboarding/tour completes (once per user)
+ * - Hides while the tours menu is open
+ * - After dismissal, reverts to hover-only tooltip
+ */
+export function useToursTooltip(): ToursTooltipState {
+ const [showToursTooltip, setShowToursTooltip] = useState(false);
+ const [toursMenuOpen, setToursMenuOpen] = useState(false);
+ const tourWasOpenRef = useRef(false);
+
+ // Auto-show when a tour ends (fires once per user)
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+
+ const handleTourStateChange = (event: Event) => {
+ const { detail } = event as CustomEvent
;
+ const wasOpen = tourWasOpenRef.current;
+ tourWasOpenRef.current = detail.isOpen;
+
+ if (wasOpen && !detail.isOpen && !hasShownToursTooltip()) {
+ setShowToursTooltip(true);
+ }
+ };
+
+ window.addEventListener(TOUR_STATE_EVENT, handleTourStateChange);
+ return () => window.removeEventListener(TOUR_STATE_EVENT, handleTourStateChange);
+ }, []);
+
+ // Show once after onboarding is complete
+ useEffect(() => {
+ if (isOnboardingCompleted() && !hasShownToursTooltip()) {
+ setShowToursTooltip(true);
+ }
+ }, []);
+
+ const handleDismissToursTooltip = useCallback(() => {
+ markToursTooltipShown();
+ setShowToursTooltip(false);
+ }, []);
+
+ const hasBeenDismissed = hasShownToursTooltip();
+
+ const handleTooltipOpenChange = useCallback(
+ (next: boolean) => {
+ if (!next) {
+ if (!hasBeenDismissed) {
+ handleDismissToursTooltip();
+ }
+ } else if (!hasBeenDismissed && !toursMenuOpen) {
+ setShowToursTooltip(true);
+ }
+ },
+ [hasBeenDismissed, toursMenuOpen, handleDismissToursTooltip]
+ );
+
+ const tooltipOpen = toursMenuOpen ? false : hasBeenDismissed ? undefined : showToursTooltip;
+
+ return {
+ tooltipOpen,
+ manualCloseOnly: !hasBeenDismissed,
+ showCloseButton: !hasBeenDismissed,
+ toursMenuOpen,
+ setToursMenuOpen,
+ handleTooltipOpenChange,
+ };
+}
+
diff --git a/frontend/src/core/components/shared/tooltip/Tooltip.module.css b/frontend/src/core/components/shared/tooltip/Tooltip.module.css
index 62c4bf696..9d3bdb80a 100644
--- a/frontend/src/core/components/shared/tooltip/Tooltip.module.css
+++ b/frontend/src/core/components/shared/tooltip/Tooltip.module.css
@@ -39,7 +39,7 @@
background: var(--bg-raised);
padding: 0.25rem;
border-radius: 0.25rem;
- border: 0.0625rem solid var(--primary-color, #3b82f6);
+ border: 0.0625rem solid var(--border-default);
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
z-index: 1;
@@ -60,6 +60,13 @@
border-color: #ef4444 !important;
}
+.tooltip-pin-button:focus,
+.tooltip-pin-button:focus-visible {
+ outline: none;
+ border-color: var(--border-default) !important;
+ background-color: var(--bg-raised) !important;
+}
+
/* Tooltip Header */
.tooltip-header {
display: flex;
@@ -91,7 +98,7 @@
/* Tooltip Body */
.tooltip-body {
- padding: 1rem !important;
+ padding: 1rem;
color: var(--text-primary) !important;
font-size: 0.875rem !important;
line-height: 1.6 !important;
diff --git a/frontend/src/core/components/shared/tooltip/TooltipContent.tsx b/frontend/src/core/components/shared/tooltip/TooltipContent.tsx
index 8bd85966a..42ee19af3 100644
--- a/frontend/src/core/components/shared/tooltip/TooltipContent.tsx
+++ b/frontend/src/core/components/shared/tooltip/TooltipContent.tsx
@@ -5,18 +5,20 @@ import { TooltipTip } from '@app/types/tips';
interface TooltipContentProps {
content?: React.ReactNode;
tips?: TooltipTip[];
+ extraRightPadding?: number;
}
export const TooltipContent: React.FC = ({
content,
tips,
+ extraRightPadding = 0,
}) => {
return (
const loadSampleFile = useCallback(async () => {
try {
+ // Hide the modal immediately so the tour targets are visible while we load
+ closeFilesModal();
const response = await fetch(`${BASE_PATH}/samples/Sample.pdf`);
const blob = await response.blob();
const file = new File([blob], 'Sample.pdf', { type: 'application/pdf' });
diff --git a/frontend/src/core/hooks/useServerExperience.ts b/frontend/src/core/hooks/useServerExperience.ts
index 28f62c1c5..7b78a58d4 100644
--- a/frontend/src/core/hooks/useServerExperience.ts
+++ b/frontend/src/core/hooks/useServerExperience.ts
@@ -64,7 +64,10 @@ export function useServerExperience(): ServerExperienceValue {
const loginEnabled = config?.enableLogin !== false;
const configIsAdmin = Boolean(config?.isAdmin);
- const effectiveIsAdmin = configIsAdmin || (!loginEnabled && selfReportedAdmin);
+ // For no-login servers, treat everyone as a regular user (no effective admin)
+ // Commented out the previous self-reported admin path to avoid elevating users.
+ // const effectiveIsAdmin = configIsAdmin || (!loginEnabled && selfReportedAdmin);
+ const effectiveIsAdmin = loginEnabled ? configIsAdmin : false;
const hasPaidLicense = config?.license === 'SERVER' || config?.license === 'PRO' || config?.license === 'ENTERPRISE';
const setSelfReportedAdmin = useCallback((value: boolean) => {
diff --git a/frontend/src/core/services/auditService.ts b/frontend/src/core/services/auditService.ts
index ac2da176b..30951b67b 100644
--- a/frontend/src/core/services/auditService.ts
+++ b/frontend/src/core/services/auditService.ts
@@ -49,7 +49,9 @@ const auditService = {
* Get audit system status
*/
async getSystemStatus(): Promise
{
- const response = await apiClient.get('/api/v1/proprietary/ui-data/audit-dashboard');
+ const response = await apiClient.get('/api/v1/proprietary/ui-data/audit-dashboard', {
+ suppressErrorToast: true,
+ });
const data = response.data;
// Map V1 response to expected format
diff --git a/frontend/src/proprietary/components/shared/UpgradeBanner.tsx b/frontend/src/proprietary/components/shared/UpgradeBanner.tsx
index 966a82f2e..51441f228 100644
--- a/frontend/src/proprietary/components/shared/UpgradeBanner.tsx
+++ b/frontend/src/proprietary/components/shared/UpgradeBanner.tsx
@@ -13,7 +13,7 @@ import {
UPGRADE_BANNER_ALERT_EVENT,
} from '@core/constants/events';
import { useServerExperience } from '@app/hooks/useServerExperience';
-import { hasSeenStep } from '@core/components/onboarding/orchestrator/onboardingStorage';
+import { isOnboardingCompleted } from '@core/components/onboarding/orchestrator/onboardingStorage';
const FRIENDLY_LAST_SEEN_KEY = 'upgradeBannerFriendlyLastShownAt';
const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
@@ -26,6 +26,7 @@ const UpgradeBanner: React.FC = () => {
const onAuthRoute = isAuthRoute(location.pathname);
const { openCheckout } = useCheckout();
const {
+ loginEnabled,
totalUsers,
userCountResolved,
userCountLoading,
@@ -34,9 +35,11 @@ const UpgradeBanner: React.FC = () => {
licenseLoading,
freeTierLimit,
overFreeTierLimit,
+ weeklyActiveUsers,
scenarioKey,
} = useServerExperience();
- const onboardingComplete = hasSeenStep('welcome');
+ const onboardingComplete = isOnboardingCompleted();
+ console.log('onboardingComplete', onboardingComplete);
const [friendlyVisible, setFriendlyVisible] = useState(() => {
if (typeof window === 'undefined') return false;
const lastShownRaw = window.localStorage.getItem(FRIENDLY_LAST_SEEN_KEY);
@@ -296,8 +299,18 @@ const UpgradeBanner: React.FC = () => {
);
};
+ const suppressForNoLogin =
+ !loginEnabled ||
+ (!loginEnabled && (weeklyActiveUsers ?? Number.POSITIVE_INFINITY) > 5);
+
// Don't show on auth routes or if neither banner type should show
- if (onAuthRoute || (!friendlyVisible && !shouldEvaluateUrgent)) {
+ // Also suppress entirely for no-login servers (treat them as regular users only)
+ // and, per request, never surface upgrade messaging there when WAU > 5.
+ if (
+ onAuthRoute ||
+ suppressForNoLogin ||
+ (!friendlyVisible && !shouldEvaluateUrgent)
+ ) {
return null;
}
diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx
index c7142b3d2..1f71b090d 100644
--- a/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx
+++ b/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx
@@ -23,8 +23,14 @@ const AdminAuditSection: React.FC = () => {
setError(null);
const status = await auditService.getSystemStatus();
setSystemStatus(status);
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to load audit system status');
+ } catch (err: any) {
+ // Check if this is a permission/license error (403/404)
+ const status = err?.response?.status;
+ if (status === 403 || status === 404) {
+ setError('enterprise-license-required');
+ } else {
+ setError(err instanceof Error ? err.message : 'Failed to load audit system status');
+ }
} finally {
setLoading(false);
}
@@ -56,6 +62,16 @@ const AdminAuditSection: React.FC = () => {
}
if (error) {
+ if (error === 'enterprise-license-required') {
+ return (
+
+ {t(
+ 'audit.enterpriseRequiredMessage',
+ 'The audit logging system is an enterprise feature. Please upgrade to an enterprise license to access audit logs and analytics.'
+ )}
+
+ );
+ }
return (
{error}
diff --git a/frontend/src/proprietary/testing/serverExperienceSimulations.ts b/frontend/src/proprietary/testing/serverExperienceSimulations.ts
index 0aa745d54..ce6b60ab0 100644
--- a/frontend/src/proprietary/testing/serverExperienceSimulations.ts
+++ b/frontend/src/proprietary/testing/serverExperienceSimulations.ts
@@ -51,6 +51,7 @@ const BASE_NO_LOGIN_CONFIG: AppConfig = {
appVersion: '2.0.0',
serverCertificateEnabled: false,
enableAlphaFunctionality: false,
+ enableDesktopInstallSlide: true,
serverPort: 8080,
premiumEnabled: false,
runningProOrHigher: false,