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
This commit is contained in:
rcourtman
2025-12-25 21:29:46 +00:00
parent 3790c63e95
commit 337c4e1fec
2 changed files with 41 additions and 12 deletions
@@ -1177,17 +1177,33 @@ export const AISettings: Component = () => {
ENABLED
</span>
</Show>
<Show when={autoFixLocked()}>
<span class="px-1.5 py-0.5 text-[10px] font-semibold bg-amber-100 dark:bg-amber-900 text-amber-700 dark:text-amber-300 rounded">Pro</span>
</Show>
</label>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
{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.'}
</p>
<Show when={autoFixLocked()}>
<p class="text-[10px] text-amber-600 dark:text-amber-400 mt-1">
Pulse Pro required for autonomous mode.{' '}
<a
class="underline decoration-dotted"
href="https://pulserelay.pro"
target="_blank"
rel="noreferrer"
>
Upgrade
</a>
</p>
</Show>
</div>
<Toggle
checked={form.autonomousMode}
onChange={(event) => setForm('autonomousMode', event.currentTarget.checked)}
disabled={saving()}
disabled={saving() || autoFixLocked()}
/>
</div>
</div>
+24 -11
View File
@@ -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('<pre>') && text.includes('</pre>')) {
const match = text.match(/<pre>(.*?)<\/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('<pre>') && text.includes('</pre>')) {
const match = text.match(/<pre>(.*?)<\/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}`);