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

@@ -3,14 +3,13 @@ import {
Button,
Field,
Input,
MessageBar,
MessageBarBody,
Select,
Text,
} from "@fluentui/react-components";
import { LockClosedRegular, LockOpenRegular } from "@fluentui/react-icons";
import { useCommonSectionStyles } from "../../components/commonStyles";
import { useJailsPageStyles } from "./jailsPageStyles";
import { useNotification } from "../../services/notificationService";
import { ApiError } from "../../api/client";
interface BanUnbanFormProps {
@@ -22,12 +21,11 @@ interface BanUnbanFormProps {
export function BanUnbanForm({ jailNames, onBan, onUnban }: BanUnbanFormProps): React.JSX.Element {
const styles = useJailsPageStyles();
const sectionStyles = useCommonSectionStyles();
const notification = useNotification();
const [banIpVal, setBanIpVal] = useState("");
const [banJail, setBanJail] = useState("");
const [unbanIpVal, setUnbanIpVal] = useState("");
const [unbanJail, setUnbanJail] = useState("");
const [formError, setFormError] = useState<string | null>(null);
const [formSuccess, setFormSuccess] = useState<string | null>(null);
const [isBanning, setIsBanning] = useState(false);
const [isUnbanning, setIsUnbanning] = useState(false);
@@ -39,10 +37,8 @@ export function BanUnbanForm({ jailNames, onBan, onUnban }: BanUnbanFormProps):
: String(err);
const handleBan = async (): Promise<void> => {
setFormError(null);
setFormSuccess(null);
if (!banIpVal.trim() || !banJail) {
setFormError("Both IP address and jail are required.");
notification.error("Both IP address and jail are required.");
return;
}
@@ -50,20 +46,18 @@ export function BanUnbanForm({ jailNames, onBan, onUnban }: BanUnbanFormProps):
try {
const ip = banIpVal.trim();
await onBan(banJail, ip);
setFormSuccess(`${ip} banned in ${banJail}.`);
notification.success(`${ip} banned in ${banJail}.`);
setBanIpVal("");
} catch (err: unknown) {
setFormError(formatErrorMessage(err));
notification.error(formatErrorMessage(err));
} finally {
setIsBanning(false);
}
};
const handleUnban = async (fromAllJails: boolean): Promise<void> => {
setFormError(null);
setFormSuccess(null);
if (!unbanIpVal.trim()) {
setFormError("IP address is required.");
notification.error("IP address is required.");
return;
}
@@ -73,11 +67,11 @@ export function BanUnbanForm({ jailNames, onBan, onUnban }: BanUnbanFormProps):
const jail = fromAllJails ? undefined : unbanJail || undefined;
await onUnban(ip, jail);
const scope = jail ?? "all jails";
setFormSuccess(`${ip} unbanned from ${scope}.`);
notification.success(`${ip} unbanned from ${scope}.`);
setUnbanIpVal("");
setUnbanJail("");
} catch (err: unknown) {
setFormError(formatErrorMessage(err));
notification.error(formatErrorMessage(err));
} finally {
setIsUnbanning(false);
}
@@ -91,17 +85,6 @@ export function BanUnbanForm({ jailNames, onBan, onUnban }: BanUnbanFormProps):
</Text>
</div>
{formError && (
<MessageBar intent="error">
<MessageBarBody>{formError}</MessageBarBody>
</MessageBar>
)}
{formSuccess && (
<MessageBar intent="success">
<MessageBarBody>{formSuccess}</MessageBarBody>
</MessageBar>
)}
<Text size={300} weight="semibold">
Ban an IP
</Text>