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:
@@ -1,24 +1,3 @@
|
||||
## 14) Error boundary granularity is too coarse
|
||||
- Where found:
|
||||
- [frontend/src/App.tsx](frontend/src/App.tsx)
|
||||
- [frontend/src/components/ErrorBoundary.tsx](frontend/src/components/ErrorBoundary.tsx)
|
||||
- Why this is needed:
|
||||
- Single top boundary causes full app fallback on local component failures.
|
||||
- Goal:
|
||||
- Add page-level and section-level boundaries.
|
||||
- What to do:
|
||||
- Wrap risky pages/widgets with local boundaries.
|
||||
- Preserve shell/navigation when a section fails.
|
||||
- Possible traps and issues:
|
||||
- Too many boundaries can complicate fallback UX consistency.
|
||||
- Docs changes needed:
|
||||
- Add frontend resilience/fallback strategy.
|
||||
- Doc references:
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
- https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
|
||||
|
||||
---
|
||||
|
||||
## 15) Fragmented async error UX handling in components
|
||||
- Where found:
|
||||
- [frontend/src/pages/jails/BanUnbanForm.tsx](frontend/src/pages/jails/BanUnbanForm.tsx)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user