fix(ui): extract meaningful error messages from API responses
All checks were successful
CI / build (push) Successful in 2m9s
CI / docker (push) Successful in 1m28s

Introduces ApiError class in client.ts that parses Spring Boot error
bodies to extract human-readable messages (message, error, detail fields).
Adds errorMessage() helper used by all toast descriptions instead of
raw String(err) which dumped JSON blobs to the user.

Affected: all 10 page components that display error toasts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-26 21:10:28 +02:00
parent 73e41e5607
commit 088bc34e67
11 changed files with 95 additions and 40 deletions

View File

@@ -1,4 +1,5 @@
import { useRef, useState } from 'react';
import { errorMessage } from '../../api/client';
import {
Alert,
Badge,
@@ -163,7 +164,7 @@ export function CertificatesPage() {
toast({ title: 'Validation failed', description: result.errors.join(', '), variant: 'error' });
}
} catch (err) {
toast({ title: 'Upload failed', description: String(err), variant: 'error' });
toast({ title: 'Upload failed', description: errorMessage(err), variant: 'error' });
}
}
@@ -172,7 +173,7 @@ export function CertificatesPage() {
await activateMutation.mutateAsync();
toast({ title: 'Certificate activated', variant: 'success' });
} catch (err) {
toast({ title: 'Activation failed', description: String(err), variant: 'error' });
toast({ title: 'Activation failed', description: errorMessage(err), variant: 'error' });
}
}
@@ -181,7 +182,7 @@ export function CertificatesPage() {
await restoreMutation.mutateAsync();
toast({ title: 'Certificate restored from archive', variant: 'success' });
} catch (err) {
toast({ title: 'Restore failed', description: String(err), variant: 'error' });
toast({ title: 'Restore failed', description: errorMessage(err), variant: 'error' });
}
}
@@ -190,7 +191,7 @@ export function CertificatesPage() {
await discardMutation.mutateAsync();
toast({ title: 'Staged certificate discarded', variant: 'success' });
} catch (err) {
toast({ title: 'Discard failed', description: String(err), variant: 'error' });
toast({ title: 'Discard failed', description: errorMessage(err), variant: 'error' });
}
}

View File

@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { Button, Card, FormField, Input, useToast } from '@cameleer/design-system';
import { useCreateTenant } from '../../api/vendor-hooks';
import { errorMessage } from '../../api/client';
import { toSlug } from '../../utils/slug';
const TIERS = ['STARTER', 'TEAM', 'BUSINESS', 'ENTERPRISE'];
@@ -35,7 +36,7 @@ export function CreateTenantPage() {
toast({ title: 'Tenant created — provisioning in progress', variant: 'success' });
navigate(`/vendor/tenants/${result.id}`);
} catch (err) {
toast({ title: 'Failed to create tenant', description: String(err), variant: 'error' });
toast({ title: 'Failed to create tenant', description: errorMessage(err), variant: 'error' });
}
}

View File

@@ -9,6 +9,7 @@ import {
useToast,
} from '@cameleer/design-system';
import { Send, Trash2, Save, Power } from 'lucide-react';
import { errorMessage } from '../../api/client';
import {
useEmailConnector,
useSaveEmailConnector,
@@ -70,7 +71,7 @@ export function EmailConfigPage() {
setEditing(false);
setPassword('');
} catch (err) {
toast({ title: 'Failed to save', description: String(err), variant: 'error' });
toast({ title: 'Failed to save', description: errorMessage(err), variant: 'error' });
}
}
@@ -81,7 +82,7 @@ export function EmailConfigPage() {
setConfirmDelete(false);
setEditing(false);
} catch (err) {
toast({ title: 'Failed to delete', description: String(err), variant: 'error' });
toast({ title: 'Failed to delete', description: errorMessage(err), variant: 'error' });
}
}
@@ -98,7 +99,7 @@ export function EmailConfigPage() {
toast({ title: 'Test failed', description: result.message, variant: 'error' });
}
} catch (err) {
toast({ title: 'Test failed', description: String(err), variant: 'error' });
toast({ title: 'Test failed', description: errorMessage(err), variant: 'error' });
}
}
@@ -112,7 +113,7 @@ export function EmailConfigPage() {
variant: 'success',
});
} catch (err) {
toast({ title: 'Failed to toggle registration', description: String(err), variant: 'error' });
toast({ title: 'Failed to toggle registration', description: errorMessage(err), variant: 'error' });
}
}

View File

@@ -7,6 +7,7 @@ import {
} from '@cameleer/design-system';
import { Clipboard, Search, ShieldCheck } from 'lucide-react';
import { useVerifyLicense, usePublicKey } from '../../api/vendor-hooks';
import { errorMessage } from '../../api/client';
import styles from '../../styles/platform.module.css';
import type { VerifyLicenseResponse } from '../../types/api';
@@ -49,7 +50,7 @@ export function LicenseVerifyPage() {
const res = await verifyLicense.mutateAsync(token.trim());
setResult(res);
} catch (err) {
toast({ title: 'Verification failed', description: String(err), variant: 'error' });
toast({ title: 'Verification failed', description: errorMessage(err), variant: 'error' });
}
}

View File

@@ -20,6 +20,7 @@ import {
useRestartServer,
useUpgradeServer,
} from '../../api/vendor-hooks';
import { errorMessage } from '../../api/client';
import { ServerStatusBadge } from '../../components/ServerStatusBadge';
import { tierColor } from '../../utils/tier';
import styles from '../../styles/platform.module.css';
@@ -155,7 +156,7 @@ export function TenantDetailPage() {
variant: 'success',
});
} catch (err) {
toast({ title: 'Minting failed', description: String(err), variant: 'error' });
toast({ title: 'Minting failed', description: errorMessage(err), variant: 'error' });
}
}
@@ -200,7 +201,7 @@ export function TenantDetailPage() {
toast({ title: 'Tenant suspended', variant: 'warning' });
}
} catch (err) {
toast({ title: 'Action failed', description: String(err), variant: 'error' });
toast({ title: 'Action failed', description: errorMessage(err), variant: 'error' });
}
}
@@ -210,7 +211,7 @@ export function TenantDetailPage() {
await restartServer.mutateAsync(id);
toast({ title: 'Server restarted', variant: 'success' });
} catch (err) {
toast({ title: 'Restart failed', description: String(err), variant: 'error' });
toast({ title: 'Restart failed', description: errorMessage(err), variant: 'error' });
}
}
@@ -220,7 +221,7 @@ export function TenantDetailPage() {
await upgradeServer.mutateAsync(id);
toast({ title: 'Server upgrade started — pulling latest images', variant: 'success' });
} catch (err) {
toast({ title: 'Upgrade failed', description: String(err), variant: 'error' });
toast({ title: 'Upgrade failed', description: errorMessage(err), variant: 'error' });
}
}
@@ -231,7 +232,7 @@ export function TenantDetailPage() {
toast({ title: 'Tenant deleted', variant: 'success' });
navigate('/vendor/tenants');
} catch (err) {
toast({ title: 'Delete failed', description: String(err), variant: 'error' });
toast({ title: 'Delete failed', description: errorMessage(err), variant: 'error' });
}
}