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,