feat: centralized error notification service (issue #15)

- Create NotificationService with context provider for centralized error/success messaging
- Add NotificationContainer component to render notification stack
- Integrate NotificationProvider into App root
- Refactor BanUnbanForm to use notification service instead of local error state
- Update fetchError utility to optionally use notification callbacks
- Add comprehensive error handling guidelines to Web-Development.md
- Prevent duplicate notifications with deduplication logic
- Support auto-dismiss with configurable TTL per notification type

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-28 08:41:33 +02:00
parent da6433b2cf
commit ae34d98859
8 changed files with 557 additions and 152 deletions

View File

@@ -810,10 +810,123 @@ When an API request returns 401 or 403:
## 12. Error Handling & Resilience
### Centralized Error Notification Service
The application provides a centralized `NotificationService` for displaying consistent, user-facing error and success messages across the entire UI. This prevents fragmented error handling and duplicate messaging.
**When to use:**
- **Form operations** (ban, unban, save settings) → use global notifications for success/error feedback
- **Async actions** that affect application state → use notifications to inform the user
- **API errors** in components → convert to user-friendly messages and show via notifications
**When NOT to use (keep local state):**
- Input validation errors that should not trigger API calls (display inline field errors instead)
- Component-internal loading states (no need for notifications)
- Errors in error boundaries (error boundaries have their own fallback UI)
**API:**
```ts
// In any component or hook within NotificationProvider
const notification = useNotification();
notification.success("Data saved successfully!");
notification.error("Failed to save: invalid format");
notification.warning("This action cannot be undone");
notification.info("Processing in background…");
// Control auto-dismiss duration (milliseconds)
notification.error("Connection lost", 10000); // Auto-dismiss after 10 seconds
notification.error("Critical error", null); // Never auto-dismiss
```
**Auto-dismiss defaults:**
- Success: 5000ms
- Error: 8000ms
- Warning: 6000ms
- Info: 5000ms
**Duplicate prevention:** The service prevents identical notifications from appearing multiple times. If you try to show the same message with the same intent level twice, the second is ignored.
**Example refactor — from local state to notifications:**
```ts
// Before: Local error/success state in component
function MyForm() {
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const handleSubmit = async () => {
setError(null);
setSuccess(null);
try {
await saveData();
setSuccess("Data saved!");
} catch (err) {
setError("Failed to save");
}
};
return (
<div>
{error && <MessageBar intent="error">{error}</MessageBar>}
{success && <MessageBar intent="success">{success}</MessageBar>}
<button onClick={handleSubmit}>Save</button>
</div>
);
}
// After: Using notification service
function MyForm() {
const notification = useNotification();
const handleSubmit = async () => {
try {
await saveData();
notification.success("Data saved!");
} catch (err) {
notification.error("Failed to save");
}
};
return <button onClick={handleSubmit}>Save</button>;
}
```
**Using notifications with hooks:**
If a hook manages data fetching and needs to notify of errors, accept a notification callback or use `useNotification` directly within the hook:
```ts
// hooks/useSaveData.ts
export function useSaveData() {
const notification = useNotification();
const [loading, setLoading] = useState(false);
const save = useCallback(async (data: unknown) => {
setLoading(true);
try {
await api.post("/data", data);
notification.success("Data saved");
return true;
} catch (err) {
notification.error("Failed to save data");
return false;
} finally {
setLoading(false);
}
}, [notification]);
return { save, loading };
}
```
### API Error Handling
- Wrap API calls in `try-catch` inside hooks — components should never see raw exceptions.
- **All hook catch blocks must use `handleFetchError` rather than directly calling `setError`.** This ensures auth errors (401/403) are routed to the global session-expiry flow instead of displaying confusing error text in the UI. Use the pattern: `handleFetchError(err, setError, "User-friendly fallback message")`.
- **All hook catch blocks should use `handleFetchError` or notifications.** This ensures auth errors (401/403) are routed to the global session-expiry flow instead of displaying confusing error text in the UI.
- **With local state:** `handleFetchError(err, setError, "User-friendly fallback message")`
- **With notifications:** `handleFetchError(err, setError, "Default message", notification.error)`
- Display user-friendly error messages — never expose stack traces or raw server responses in the UI.
- Log errors to the console (or a future logging service) with sufficient context for debugging.
- Always handle the **loading**, **error**, and **empty** states for every data-driven component.