From 337c4e1fec1a045b9f59c0218c51157fe277c38a Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 25 Dec 2025 21:29:46 +0000 Subject: [PATCH] feat: improve Pro feature UI gating and error handling - AISettings: Add Pro badge & disabled state for Autonomous Mode toggle - APIClient: Parse JSON error responses for better user-facing messages - Supports new 402 Payment Required responses from license-gated endpoints --- .../src/components/Settings/AISettings.tsx | 18 +++++++++- frontend-modern/src/utils/apiClient.ts | 35 +++++++++++++------ 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/frontend-modern/src/components/Settings/AISettings.tsx b/frontend-modern/src/components/Settings/AISettings.tsx index 6447a6e90e..7f194349ef 100644 --- a/frontend-modern/src/components/Settings/AISettings.tsx +++ b/frontend-modern/src/components/Settings/AISettings.tsx @@ -1177,17 +1177,33 @@ export const AISettings: Component = () => { ENABLED + + Pro +

{form.autonomousMode ? 'AI will execute all commands without asking for approval. Only enable if you trust your configured model.' : 'AI will ask for approval before running commands that modify your system. Read-only commands (like df, ps, docker stats) run automatically.'}

+ +

+ Pulse Pro required for autonomous mode.{' '} + + Upgrade + +

+
setForm('autonomousMode', event.currentTarget.checked)} - disabled={saving()} + disabled={saving() || autoFixLocked()} /> diff --git a/frontend-modern/src/utils/apiClient.ts b/frontend-modern/src/utils/apiClient.ts index ed4684fd13..4c0848bee2 100644 --- a/frontend-modern/src/utils/apiClient.ts +++ b/frontend-modern/src/utils/apiClient.ts @@ -325,18 +325,31 @@ class ApiClient { // Try to extract just the error message without HTTP status codes let errorMessage = text; - // If it looks like an HTML error page, try to extract the message - if (text.includes('
') && text.includes('
')) { - const match = text.match(/
(.*?)<\/pre>/s);
-        if (match) errorMessage = match[1];
-      }
+      // First try to parse as JSON (our API returns structured errors like {error, message, feature, upgrade_url})
+      try {
+        const jsonError = JSON.parse(text);
+        if (jsonError.message) {
+          errorMessage = jsonError.message;
+        } else if (jsonError.error && typeof jsonError.error === 'string') {
+          // Some APIs return {error: "message"} format
+          errorMessage = jsonError.error;
+        }
+      } catch {
+        // Not JSON, try other formats
 
-      // If the backend sent a plain text error, use it directly
-      if (!text.includes('<') && text.length < 200) {
-        errorMessage = text;
-      } else if (text.length > 200) {
-        // For long responses, just use a generic message
-        errorMessage = `Request failed with status ${response.status}`;
+        // If it looks like an HTML error page, try to extract the message
+        if (text.includes('
') && text.includes('
')) { + const match = text.match(/
(.*?)<\/pre>/s);
+          if (match) errorMessage = match[1];
+        }
+
+        // If the backend sent a plain text error, use it directly
+        if (!text.includes('<') && text.length < 200) {
+          errorMessage = text;
+        } else if (text.length > 200) {
+          // For long responses, just use a generic message
+          errorMessage = `Request failed with status ${response.status}`;
+        }
       }
 
       throw new Error(errorMessage || `Request failed with status ${response.status}`);