diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx
index 0a4a03e4..959ce875 100644
--- a/frontend/src/components/Billing.jsx
+++ b/frontend/src/components/Billing.jsx
@@ -52,6 +52,7 @@ import {
Cancel as CancelIcon,
Shield as ShieldIcon,
Cancel as XCircleIcon,
+ LockOutlined as LockIcon,
FlashOn as ZapIcon,
People as UsersIcon,
FmdGoodOutlined as FmdGoodOutlinedIcon,
@@ -203,7 +204,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
>
{isProdStatusOn
? 'Your organization has full access to all enterprise features and capabilities.'
- : 'View your current limits and available features. Upgrade to unlock enterprise capabilities.'}
+ : 'Your organization is running on the open-source plan. Upgrade to Enterprise to remove limits and unlock advanced capabilities.'}
{/* Features Grid */}
@@ -223,8 +224,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
})
.map((feature, index) => {
const Icon = feature.icon;
- const isAvailable = isProdStatusOn && feature.isActive;
- const statusColor = isAvailable ? colors.success : colors.disabled;
+ const isAvailable = isProdStatusOn;
+ const statusColor = isAvailable ? colors.success : colors.warning;
const bgColor = isAvailable ? themeMode === "dark" ? "#212121" : "#ffffff" : colors.disabledBg;
return (
@@ -235,19 +236,34 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
alignItems: 'center',
gap: 14,
padding: '14px 16px',
+ paddingLeft: !isAvailable ? 20 : 16,
borderRadius: 10,
background: bgColor,
border: `1px solid ${isAvailable ? colors.success + '40' : colors.border}`,
transition: 'all 0.2s',
+ position: 'relative',
+ overflow: 'hidden',
}}
>
+ {!isAvailable && (
+
fontSize: 15,
fontWeight: 600,
color: colors.textPrimary,
- marginBottom: 4,
+ marginBottom: 3,
}}
>
{feature.label}
@@ -272,7 +288,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
@@ -285,8 +301,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
style={{ color: colors.success, fontSize: 20, flexShrink: 0 }}
/>
) : (
-
)}
@@ -304,80 +320,323 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
/>
{!isProdStatusOn && (
-
-
-
-
-
- About Shuffle Enterprise
-
-
- Shuffle Enterprise is designed for organizations that require scalability, high
- availability, dedicated support, and robust infrastructure to run mission-critical
- workflows in production environments. learn more
-
+ overflow: 'hidden',
+ border: `1px solid ${colors.accent}40`,
+ marginBottom: 24,
+ }}>
+ {/* Header */}
+
+
+
+
+
+
+ Unlock Shuffle Enterprise
+
+
+ Scale your security operations without limits
+
+
-
-
)}
- {/* CTA Section */}
- {!isProdStatusOn && (
-
-
-
- Ready to upgrade?
+ {/* Body */}
+
+
+ {[
+ { icon: ZapIcon, text: 'Higher App Run Limits' },
+ { icon: UsersIcon, text: 'Multi-Tenant Support' },
+ { icon: ShieldIcon, text: 'Enterprise SLA' },
+ { icon: FmdGoodOutlinedIcon, text: 'Multi-Location Deploy' },
+ ].map((item, i) => {
+ const ItemIcon = item.icon;
+ return (
+
+
+
+ {item.text}
+
+
+ );
+ })}
+
+
+
+ Purpose-built for security teams that need scalability, high availability, and dedicated
+ expert support to run mission-critical workflows in production.{' '}
+
+ Learn more
+
-
+ }
+ >
+ Upgrade Now
+
+ }
>
- Contact our team to learn more about enterprise features and pricing.
-
+ Talk to Sales
+
+
-
}
- endIcon={
}
- >
- Contact Sales
-
)}
);
};
+const AppRunsQueueCard = memo(({ environment, isAirGapped, isCloudSynching, totalRuns, limit, theme, navigate }) => {
+
+ const usagePct = limit > 0 ? (totalRuns / limit) * 100 : 0;
+ const hardPauseLimit = limit * 2;
+ const hardPausePct = hardPauseLimit > 0 ? Math.min((totalRuns / hardPauseLimit) * 100, 100) : 0;
+ const mainBarPct = Math.min(usagePct, 100);
+
+ const queueSize = environment?.queue !== undefined && environment?.queue !== null
+ ? Math.max(0, environment.queue)
+ : 0;
+
+ const isThrottled = isCloudSynching ? false : (isAirGapped ? hardPausePct >= 100 : usagePct >= 100);
+
+ let status, statusColor, statusBg;
+ if (isThrottled) {
+ status = 'Throttled';
+ statusColor = '#ef4444';
+ statusBg = 'rgba(239, 68, 68, 0.12)';
+ } else if (!isCloudSynching && usagePct >= 80) {
+ status = 'Warning';
+ statusColor = '#f59e0b';
+ statusBg = 'rgba(245, 158, 11, 0.12)';
+ } else {
+ status = 'Healthy';
+ statusColor = theme.palette.green;
+ statusBg = `${theme.palette.green}1f`;
+ }
+
+ const throttleRate = isThrottled ? '1/min' : '\u2014';
+ const estClearTime = isThrottled && queueSize > 0 ? `${queueSize} min` : '\u2014';
+ const mainBarColor = isThrottled ? '#ef4444' : usagePct >= 80 && !isCloudSynching ? '#f59e0b' : theme.palette.green;
+
+ const envTypeLabel = environment?.run_type === 'cloud' ? 'Cloud' : 'On-prem';
+ const envName = environment?.Name || environment?.name || 'Default';
+
+ const borderColor = theme.palette.slateGrayColor;
+ const trackBg = theme.palette.slateGrayColor;
+ const mutedText = theme.palette.text.secondary;
+
+ return (
+
+ {/* Title row */}
+
+
+ {envTypeLabel} - {envName} · App runs / month
+
+
+
+ {status}
+
+
+
+ {/* Main number */}
+
+
+ {totalRuns.toLocaleString()}
+
+
+ / {limit.toLocaleString()}
+
+
+
+ {isAirGapped && !isCloudSynching && (
+
+ No throttle until {hardPauseLimit.toLocaleString()} runs · 2× your plan limit
+
+ )}
+
+ {/* Main usage bar */}
+
+
+ {/* 80% threshold marker */}
+
+
+
+ 0
+ 80% threshold
+ {limit.toLocaleString()}
+
+
+ {/* Throttle limit row */}
+ {isAirGapped && (
+ <>
+
+
+ Burst throttle threshold (2× limit) · workflows throttle to 1/min above this
+
+
+ {totalRuns.toLocaleString()} / {hardPauseLimit.toLocaleString()}
+
+
+
+
+ >
+ )}
+
+ {/* Alert box for Warning / Throttled */}
+ {status !== 'Healthy' && (
+
+
+ {isThrottled ? 'Running slow \u2014 workflows are still running' : 'Approaching your monthly limit'}
+
+
+ {isThrottled
+ ? isAirGapped
+ ? `You've exceeded the burst threshold of ${hardPauseLimit.toLocaleString()} runs (2\u00d7 your plan limit). Your workflows are still running \u2014 there is no hard stop. Executions slow to 1 per minute until next month.`
+ : `You've exceeded your ${limit.toLocaleString()} monthly limit. Your instance keeps running \u2014 executions slow to 1 per minute until next month. Nothing is lost. You can view or clear the queue from the Locations tab.`
+ : isAirGapped
+ ? `You've used ${totalRuns.toLocaleString()} of ${limit.toLocaleString()} app runs. Workflows run normally \u2014 slowdown only begins at ${hardPauseLimit.toLocaleString()} runs (2\u00d7 your plan limit). No action needed.`
+ : `You've used ${totalRuns.toLocaleString()} of ${limit.toLocaleString()} app runs (${Math.max(0, limit - totalRuns).toLocaleString()} remaining). If you reach 100%, executions continue at a reduced rate of 1 per minute, nothing stops or is lost.`
+ }
+
+ {
+ if (isThrottled) navigate('/admin?tab=locations');
+ else window.open('https://shuffler.io/contact', '_blank', 'noopener,noreferrer');
+ }}
+ >
+ {isThrottled ? 'Manage queue' : 'Contact Us'}
+
+
+ )}
+
+ {/* Stats row */}
+
+ {[
+ { label: 'Queued jobs', value: queueSize },
+ { label: 'Throttle rate', value: throttleRate },
+ { label: 'Est. clear time', value: estClearTime },
+ ].map((stat, i) => (
+
+
+ {stat.label}
+
+
+ {stat.value}
+
+
+ ))}
+
+
+ );
+});
+
const Billing = memo((props) => {
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
//const alert = useAlert();
@@ -413,7 +672,7 @@ const Billing = memo((props) => {
const [statistics, setStatistics] = useState([])
const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0)
const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0)
-
+ const [billingEnvironments, setBillingEnvironments] = useState([])
useEffect(() => {
if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) {
const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100;
@@ -525,6 +784,29 @@ const Billing = memo((props) => {
}, [])
+ const getBillingEnvironments = () => {
+ fetch(globalUrl + "/api/v1/getenvironments", {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) return;
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson && Array.isArray(responseJson)) {
+ setBillingEnvironments(responseJson);
+ }
+ })
+ .catch((error) => {
+ console.log("Error fetching environments for billing:", error);
+ });
+ };
+
const getStats = (orgid) => {
if (orgid === undefined || orgid === null) {
@@ -563,6 +845,9 @@ const Billing = memo((props) => {
useEffect(() => {
if (selectedOrganization && selectedOrganization?.id?.length > 0) {
getStats(selectedOrganization.id);
+ if (!isCloud) {
+ getBillingEnvironments();
+ }
}
}, [selectedOrganization]);
@@ -2387,6 +2672,17 @@ const Billing = memo((props) => {
const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null
+ const activeQueueEnvs = Array.isArray(billingEnvironments) ? billingEnvironments.filter(env => env != null && !env.archived && env.Type !== 'cloud') : [];
+ const totalQueueSize = activeQueueEnvs.reduce((sum, env) => sum + Math.max(0, env?.queue || 0), 0);
+ const aggregatedQueueEnv = {
+ Name: `${activeQueueEnvs.length} Runtime Location${activeQueueEnvs.length !== 1 ? 's' : ''}`,
+ run_type: 'on-prem',
+ queue: totalQueueSize,
+ };
+ const appExecLimit = selectedOrganization?.sync_features?.app_executions?.limit ?? 0;
+ const isAirGapped = selectedOrganization != null && (selectedOrganization.cloud_sync_active === true || selectedOrganization.cloud_sync === true) ? false : appExecLimit < 300000 ? false : true;
+ const isCloudSynching = selectedOrganization != null && selectedOrganization.cloud_sync === true && appExecLimit >= 300000;
+
useEffect(() => {
if (isChildOrg && currentTab === 0) {
setCurrentTab(1);
@@ -2800,6 +3096,28 @@ const Billing = memo((props) => {
) : null*/}
+
+ {/* Queue Management */}
+ {!isCloud && activeQueueEnvs.length > 0 && !isChildOrg && (
+