From 207f4e091d801b3d5b79e288da910b99e3ae2eb3 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 11 Dec 2025 17:01:53 +0000 Subject: [PATCH 1/4] Show enterprise demo messages for audit and usage --- .../public/locales/en-GB/translation.toml | 4 + .../config/EnterpriseRequiredBanner.tsx | 39 ++++++ .../configSections/AdminAuditSection.tsx | 20 ++- .../configSections/AdminUsageSection.tsx | 119 ++++++++++-------- 4 files changed, 127 insertions(+), 55 deletions(-) create mode 100644 frontend/src/proprietary/components/shared/config/EnterpriseRequiredBanner.tsx diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 83963987d..9729eb01f 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -4104,6 +4104,10 @@ title = "Login Mode Required" message = "Login mode must be enabled to modify admin settings. Please set SECURITY_ENABLELOGIN=true in your environment or security.enableLogin: true in settings.yml, then restart the server." readOnly = "The settings below show example values for reference. Enable login mode to view and edit actual configuration." +[admin.settings.enterpriseRequired] +title = "Enterprise License Required" +message = "An Enterprise license is required to access {{featureName}}. You are viewing demo data for reference." + [admin.settings.restart] title = "Restart Required" message = "Settings have been saved successfully. A server restart is required for the changes to take effect." diff --git a/frontend/src/proprietary/components/shared/config/EnterpriseRequiredBanner.tsx b/frontend/src/proprietary/components/shared/config/EnterpriseRequiredBanner.tsx new file mode 100644 index 000000000..50623871f --- /dev/null +++ b/frontend/src/proprietary/components/shared/config/EnterpriseRequiredBanner.tsx @@ -0,0 +1,39 @@ +import { Alert, Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import LocalIcon from '@app/components/shared/LocalIcon'; + +interface EnterpriseRequiredBannerProps { + show: boolean; + featureName: string; +} + +/** + * Banner that explains enterprise-only features are in demo mode + */ +export default function EnterpriseRequiredBanner({ show, featureName }: EnterpriseRequiredBannerProps) { + const { t } = useTranslation(); + + if (!show) return null; + + return ( + } + title={t('admin.settings.enterpriseRequired.title', 'Enterprise License Required')} + color="yellow" + variant="light" + styles={{ + root: { + borderLeft: '4px solid var(--mantine-color-yellow-6)' + } + }} + > + + {t( + 'admin.settings.enterpriseRequired.message', + 'An Enterprise license is required to access {{featureName}}. You are viewing demo data for reference.', + { featureName } + )} + + + ); +} diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx index c7142b3d2..e92da5ec8 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx @@ -8,10 +8,15 @@ import AuditEventsTable from '@app/components/shared/config/configSections/audit import AuditExportSection from '@app/components/shared/config/configSections/audit/AuditExportSection'; import { useLoginRequired } from '@app/hooks/useLoginRequired'; import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; +import { useAppConfig } from '@app/contexts/AppConfigContext'; +import EnterpriseRequiredBanner from '@app/components/shared/config/EnterpriseRequiredBanner'; const AdminAuditSection: React.FC = () => { const { t } = useTranslation(); const { loginEnabled } = useLoginRequired(); + const { config } = useAppConfig(); + const runningEE = config?.runningEE ?? false; + const showDemoData = !loginEnabled || !runningEE; const [systemStatus, setSystemStatus] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -30,10 +35,11 @@ const AdminAuditSection: React.FC = () => { } }; - if (loginEnabled) { + if (!showDemoData) { fetchSystemStatus(); } else { - // Provide example audit system status when login is disabled + // Provide example audit system status when running in demo mode + setError(null); setSystemStatus({ enabled: true, level: 'INFO', @@ -42,10 +48,10 @@ const AdminAuditSection: React.FC = () => { }); setLoading(false); } - }, [loginEnabled]); + }, [loginEnabled, showDemoData]); - // Override loading state when login is disabled - const actualLoading = loginEnabled ? loading : false; + // Override loading state when showing demo data + const actualLoading = showDemoData ? false : loading; if (actualLoading) { return ( @@ -74,6 +80,10 @@ const AdminAuditSection: React.FC = () => { return ( + {systemStatus.enabled ? ( diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx index 57fd5eb31..f3090a525 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Stack, Group, @@ -16,17 +16,61 @@ import UsageAnalyticsTable from '@app/components/shared/config/configSections/us import LocalIcon from '@app/components/shared/LocalIcon'; import { useLoginRequired } from '@app/hooks/useLoginRequired'; import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; +import { useAppConfig } from '@app/contexts/AppConfigContext'; +import EnterpriseRequiredBanner from '@app/components/shared/config/EnterpriseRequiredBanner'; const AdminUsageSection: React.FC = () => { const { t } = useTranslation(); const { loginEnabled, validateLoginEnabled } = useLoginRequired(); + const { config } = useAppConfig(); + const runningEE = config?.runningEE ?? false; + const showDemoData = !loginEnabled || !runningEE; const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [displayMode, setDisplayMode] = useState<'top10' | 'top20' | 'all'>('top10'); const [dataType, setDataType] = useState<'all' | 'api' | 'ui'>('all'); - const fetchData = async () => { + const buildDemoUsageData = useCallback((): EndpointStatisticsResponse => { + const totalVisits = 15847; + const allEndpoints = [ + { endpoint: 'merge-pdfs', visits: 3245, percentage: (3245 / totalVisits) * 100 }, + { endpoint: 'compress-pdf', visits: 2891, percentage: (2891 / totalVisits) * 100 }, + { endpoint: 'pdf-to-img', visits: 2156, percentage: (2156 / totalVisits) * 100 }, + { endpoint: 'split-pdf', visits: 1834, percentage: (1834 / totalVisits) * 100 }, + { endpoint: 'rotate-pdf', visits: 1523, percentage: (1523 / totalVisits) * 100 }, + { endpoint: 'ocr-pdf', visits: 1287, percentage: (1287 / totalVisits) * 100 }, + { endpoint: 'add-watermark', visits: 945, percentage: (945 / totalVisits) * 100 }, + { endpoint: 'extract-images', visits: 782, percentage: (782 / totalVisits) * 100 }, + { endpoint: 'add-password', visits: 621, percentage: (621 / totalVisits) * 100 }, + { endpoint: 'html-to-pdf', visits: 563, percentage: (563 / totalVisits) * 100 }, + { endpoint: 'remove-password', visits: 487, percentage: (487 / totalVisits) * 100 }, + { endpoint: 'pdf-to-pdfa', visits: 423, percentage: (423 / totalVisits) * 100 }, + { endpoint: 'extract-pdf-metadata', visits: 356, percentage: (356 / totalVisits) * 100 }, + { endpoint: 'add-page-numbers', visits: 298, percentage: (298 / totalVisits) * 100 }, + { endpoint: 'crop', visits: 245, percentage: (245 / totalVisits) * 100 }, + { endpoint: 'flatten', visits: 187, percentage: (187 / totalVisits) * 100 }, + { endpoint: 'sanitize-pdf', visits: 134, percentage: (134 / totalVisits) * 100 }, + { endpoint: 'auto-split-pdf', visits: 98, percentage: (98 / totalVisits) * 100 }, + { endpoint: 'scale-pages', visits: 76, percentage: (76 / totalVisits) * 100 }, + { endpoint: 'compare-pdfs', visits: 42, percentage: (42 / totalVisits) * 100 }, + ]; + + let filteredEndpoints = allEndpoints; + if (displayMode === 'top10') { + filteredEndpoints = allEndpoints.slice(0, 10); + } else if (displayMode === 'top20') { + filteredEndpoints = allEndpoints.slice(0, 20); + } + + return { + totalVisits, + totalEndpoints: filteredEndpoints.length, + endpoints: filteredEndpoints, + }; + }, [displayMode]); + + const fetchData = useCallback(async () => { if (!validateLoginEnabled()) { return; } @@ -44,58 +88,29 @@ const AdminUsageSection: React.FC = () => { } finally { setLoading(false); } - }; + }, [dataType, displayMode, validateLoginEnabled]); useEffect(() => { - if (loginEnabled) { + if (!showDemoData) { fetchData(); - } else { - // Provide example usage analytics data when login is disabled - const totalVisits = 15847; - const allEndpoints = [ - { endpoint: 'merge-pdfs', visits: 3245, percentage: (3245 / totalVisits) * 100 }, - { endpoint: 'compress-pdf', visits: 2891, percentage: (2891 / totalVisits) * 100 }, - { endpoint: 'pdf-to-img', visits: 2156, percentage: (2156 / totalVisits) * 100 }, - { endpoint: 'split-pdf', visits: 1834, percentage: (1834 / totalVisits) * 100 }, - { endpoint: 'rotate-pdf', visits: 1523, percentage: (1523 / totalVisits) * 100 }, - { endpoint: 'ocr-pdf', visits: 1287, percentage: (1287 / totalVisits) * 100 }, - { endpoint: 'add-watermark', visits: 945, percentage: (945 / totalVisits) * 100 }, - { endpoint: 'extract-images', visits: 782, percentage: (782 / totalVisits) * 100 }, - { endpoint: 'add-password', visits: 621, percentage: (621 / totalVisits) * 100 }, - { endpoint: 'html-to-pdf', visits: 563, percentage: (563 / totalVisits) * 100 }, - { endpoint: 'remove-password', visits: 487, percentage: (487 / totalVisits) * 100 }, - { endpoint: 'pdf-to-pdfa', visits: 423, percentage: (423 / totalVisits) * 100 }, - { endpoint: 'extract-pdf-metadata', visits: 356, percentage: (356 / totalVisits) * 100 }, - { endpoint: 'add-page-numbers', visits: 298, percentage: (298 / totalVisits) * 100 }, - { endpoint: 'crop', visits: 245, percentage: (245 / totalVisits) * 100 }, - { endpoint: 'flatten', visits: 187, percentage: (187 / totalVisits) * 100 }, - { endpoint: 'sanitize-pdf', visits: 134, percentage: (134 / totalVisits) * 100 }, - { endpoint: 'auto-split-pdf', visits: 98, percentage: (98 / totalVisits) * 100 }, - { endpoint: 'scale-pages', visits: 76, percentage: (76 / totalVisits) * 100 }, - { endpoint: 'compare-pdfs', visits: 42, percentage: (42 / totalVisits) * 100 }, - ]; - - // Filter based on display mode - let filteredEndpoints = allEndpoints; - if (displayMode === 'top10') { - filteredEndpoints = allEndpoints.slice(0, 10); - } else if (displayMode === 'top20') { - filteredEndpoints = allEndpoints.slice(0, 20); - } - - setData({ - totalVisits: totalVisits, - totalEndpoints: filteredEndpoints.length, - endpoints: filteredEndpoints, - }); - setLoading(false); + return; } - }, [displayMode, dataType, loginEnabled]); + + // Provide example usage analytics data when running in demo mode + setError(null); + setData(buildDemoUsageData()); + setLoading(false); + }, [buildDemoUsageData, fetchData, showDemoData]); const handleRefresh = () => { if (!validateLoginEnabled()) { return; } + if (showDemoData) { + setData(buildDemoUsageData()); + return; + } + fetchData(); }; @@ -112,8 +127,8 @@ const AdminUsageSection: React.FC = () => { } }; - // Override loading state when login is disabled - const actualLoading = loginEnabled ? loading : false; + // Override loading state when showing demo data + const actualLoading = showDemoData ? false : loading; // Early returns for loading/error states if (actualLoading) { @@ -154,6 +169,10 @@ const AdminUsageSection: React.FC = () => { return ( + {/* Controls */} @@ -163,7 +182,7 @@ const AdminUsageSection: React.FC = () => { setDisplayMode(value as 'top10' | 'top20' | 'all')} - disabled={!loginEnabled} + disabled={showDemoData} data={[ { value: 'top10', @@ -184,7 +203,7 @@ const AdminUsageSection: React.FC = () => { leftSection={} onClick={handleRefresh} loading={loading} - disabled={!loginEnabled} + disabled={showDemoData} > {t('usage.controls.refresh', 'Refresh')} @@ -198,7 +217,7 @@ const AdminUsageSection: React.FC = () => { setDataType(value as 'all' | 'api' | 'ui')} - disabled={!loginEnabled} + disabled={showDemoData} data={[ { value: 'all', From 0764cb83683a6cae74ff4067055dcf1e44f7d05a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:01:29 +0000 Subject: [PATCH 2/4] Handle enterprise license demo mode for audit and usage --- .../configSections/AdminAuditSection.tsx | 19 ++++++++++--------- .../configSections/AdminUsageSection.tsx | 7 ++++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx index e92da5ec8..e79f55c99 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx @@ -15,8 +15,9 @@ const AdminAuditSection: React.FC = () => { const { t } = useTranslation(); const { loginEnabled } = useLoginRequired(); const { config } = useAppConfig(); - const runningEE = config?.runningEE ?? false; - const showDemoData = !loginEnabled || !runningEE; + const licenseType = config?.license ?? 'NORMAL'; + const hasEnterpriseLicense = licenseType === 'ENTERPRISE'; + const showDemoData = !loginEnabled || !hasEnterpriseLicense; const [systemStatus, setSystemStatus] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -81,7 +82,7 @@ const AdminAuditSection: React.FC = () => { @@ -89,27 +90,27 @@ const AdminAuditSection: React.FC = () => { {systemStatus.enabled ? ( - + {t('audit.tabs.dashboard', 'Dashboard')} - + {t('audit.tabs.events', 'Audit Events')} - + {t('audit.tabs.export', 'Export')} - + - + - + ) : ( diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx index f3090a525..6ec925f3a 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx @@ -23,8 +23,9 @@ const AdminUsageSection: React.FC = () => { const { t } = useTranslation(); const { loginEnabled, validateLoginEnabled } = useLoginRequired(); const { config } = useAppConfig(); - const runningEE = config?.runningEE ?? false; - const showDemoData = !loginEnabled || !runningEE; + const licenseType = config?.license ?? 'NORMAL'; + const hasEnterpriseLicense = licenseType === 'ENTERPRISE'; + const showDemoData = !loginEnabled || !hasEnterpriseLicense; const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -170,7 +171,7 @@ const AdminUsageSection: React.FC = () => { From 747d4d92e866ddc5b45ae25ea64c6eb8ff33117f Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:16:51 +0000 Subject: [PATCH 3/4] Handle demo usage data safely without enterprise license --- frontend/public/locales/en-GB/translation.toml | 1 + .../config/configSections/AdminUsageSection.tsx | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 9729eb01f..dfc7a0faa 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -5918,6 +5918,7 @@ endpoint = "Endpoint" visits = "Visits" percentage = "Percentage" noData = "No data available" +unknownEndpoint = "Unknown endpoint" [backendHealth] checking = "Checking backend status..." diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx index 6ec925f3a..63866f304 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx @@ -156,12 +156,21 @@ const AdminUsageSection: React.FC = () => { ); } - const endpoints = data?.endpoints ?? []; + const endpoints = (data?.endpoints ?? []).map((endpoint) => ({ + endpoint: endpoint.endpoint ?? t('usage.table.unknownEndpoint', 'Unknown endpoint'), + visits: Number.isFinite(endpoint.visits) ? endpoint.visits : 0, + percentage: Number.isFinite(endpoint.percentage) ? endpoint.percentage : 0, + })); + const chartData = endpoints.map((e) => ({ label: e.endpoint, value: e.visits })); const displayedVisits = endpoints.reduce((sum, e) => sum + e.visits, 0); - const totalVisits = data?.totalVisits ?? displayedVisits ?? 0; - const totalEndpoints = data?.totalEndpoints ?? endpoints.length ?? 0; + const totalVisits = Number.isFinite(data?.totalVisits) + ? (data?.totalVisits as number) + : displayedVisits; + const totalEndpoints = Number.isFinite(data?.totalEndpoints) + ? (data?.totalEndpoints as number) + : endpoints.length; const displayedPercentage = totalVisits > 0 ? ((displayedVisits / (totalVisits || 1)) * 100).toFixed(1) From fab60fd7e535fce237d1c8ac051c7f456307f406 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:40:39 +0000 Subject: [PATCH 4/4] Harden usage demo data rendering --- .../config/configSections/AdminUsageSection.tsx | 13 ++++++++----- .../configSections/usage/UsageAnalyticsChart.tsx | 8 +++++++- .../testing/serverExperienceSimulations.ts | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx index 63866f304..670b26133 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx @@ -158,18 +158,21 @@ const AdminUsageSection: React.FC = () => { const endpoints = (data?.endpoints ?? []).map((endpoint) => ({ endpoint: endpoint.endpoint ?? t('usage.table.unknownEndpoint', 'Unknown endpoint'), - visits: Number.isFinite(endpoint.visits) ? endpoint.visits : 0, - percentage: Number.isFinite(endpoint.percentage) ? endpoint.percentage : 0, + visits: Number.isFinite(endpoint.visits) ? Math.max(0, endpoint.visits) : 0, + percentage: Number.isFinite(endpoint.percentage) ? Math.max(0, endpoint.percentage) : 0, })); - const chartData = endpoints.map((e) => ({ label: e.endpoint, value: e.visits })); + const chartData = endpoints.map((e) => ({ + label: e.endpoint, + value: Number.isFinite(e.visits) ? Math.max(0, e.visits) : 0, + })); const displayedVisits = endpoints.reduce((sum, e) => sum + e.visits, 0); const totalVisits = Number.isFinite(data?.totalVisits) - ? (data?.totalVisits as number) + ? Math.max(0, data?.totalVisits as number) : displayedVisits; const totalEndpoints = Number.isFinite(data?.totalEndpoints) - ? (data?.totalEndpoints as number) + ? Math.max(0, data?.totalEndpoints as number) : endpoints.length; const displayedPercentage = totalVisits > 0 diff --git a/frontend/src/proprietary/components/shared/config/configSections/usage/UsageAnalyticsChart.tsx b/frontend/src/proprietary/components/shared/config/configSections/usage/UsageAnalyticsChart.tsx index 58fa8aae5..9e65494c4 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/usage/UsageAnalyticsChart.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/usage/UsageAnalyticsChart.tsx @@ -73,13 +73,19 @@ interface UsageAnalyticsChartProps { const UsageAnalyticsChart: React.FC = ({ data }) => { const { t } = useTranslation(); + const safeMaxValue = Math.max(...data.map((d) => d.value).filter((value) => Number.isFinite(value)), 1); + const safeData = data.map((item) => ({ + label: item.label, + value: Number.isFinite(item.value) ? Math.max(0, item.value) : 0, + })); + return ( {t('usage.chart.title', 'Endpoint Usage Chart')} - d.value), 1)} /> + ); diff --git a/frontend/src/proprietary/testing/serverExperienceSimulations.ts b/frontend/src/proprietary/testing/serverExperienceSimulations.ts index 0aa745d54..45c9fc90b 100644 --- a/frontend/src/proprietary/testing/serverExperienceSimulations.ts +++ b/frontend/src/proprietary/testing/serverExperienceSimulations.ts @@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.0.0', + appVersion: '2.1.3', serverCertificateEnabled: false, enableAlphaFunctionality: false, serverPort: 8080,