Further notification tweaks (#671)

- Improved multi-URL notification handling
- Tweak Apprise validation to catch errors earlier
- Much improved notification logging and UI response
- More robust notification tests
This commit is contained in:
Alex
2026-02-28 10:16:01 +00:00
committed by GitHub
parent 0f3a06bc9c
commit ea0d06ae08
5 changed files with 397 additions and 69 deletions
@@ -84,7 +84,14 @@ export const ActionButton = ({ field, onAction, disabled }: ActionButtonProps) =
: 'bg-red-500/20 text-red-700 dark:text-red-300'
}`}
>
{result.message}
<p>{result.message}</p>
{Array.isArray(result.details) && result.details.length > 0 && (
<ul className="mt-2 list-disc pl-5 space-y-1 text-xs opacity-90">
{result.details.map((detail, index) => (
<li key={`${detail}-${index}`}>{detail}</li>
))}
</ul>
)}
</div>
)}
+65 -16
View File
@@ -94,6 +94,31 @@ export const isApiResponseError = (error: unknown): error is ApiResponseError =>
return error instanceof ApiResponseError;
};
const mapApiErrorToActionResult = (error: unknown): ActionResult | null => {
if (!isApiResponseError(error) || !error.payload) {
return null;
}
const payload = error.payload;
const message =
typeof payload.message === 'string'
? payload.message
: (typeof payload.error === 'string' ? payload.error : null);
if (!message) {
return null;
}
const details = Array.isArray(payload.details)
? payload.details.filter((detail): detail is string => typeof detail === 'string' && detail.trim().length > 0)
: undefined;
return {
success: false,
message,
...(details && details.length > 0 ? { details } : {}),
};
};
// Default request timeout in milliseconds (30 seconds)
const DEFAULT_TIMEOUT_MS = 30000;
@@ -470,10 +495,18 @@ export const executeSettingsAction = async (
actionKey: string,
currentValues?: Record<string, unknown>
): Promise<ActionResult> => {
return fetchJSON<ActionResult>(`${API.settings}/${tabName}/action/${actionKey}`, {
method: 'POST',
body: currentValues ? JSON.stringify(currentValues) : undefined,
});
try {
return await fetchJSON<ActionResult>(`${API.settings}/${tabName}/action/${actionKey}`, {
method: 'POST',
body: currentValues ? JSON.stringify(currentValues) : undefined,
});
} catch (error) {
const mapped = mapApiErrorToActionResult(error);
if (mapped) {
return mapped;
}
throw error;
}
};
// Onboarding API functions
@@ -709,25 +742,41 @@ export const testAdminUserNotificationPreferences = async (
userId: number,
routes: Array<Record<string, unknown>>
): Promise<import('../types/settings').ActionResult> => {
return fetchJSON<import('../types/settings').ActionResult>(
`${API_BASE}/admin/users/${userId}/notification-preferences/test`,
{
method: 'POST',
body: JSON.stringify({ USER_NOTIFICATION_ROUTES: routes }),
try {
return await fetchJSON<import('../types/settings').ActionResult>(
`${API_BASE}/admin/users/${userId}/notification-preferences/test`,
{
method: 'POST',
body: JSON.stringify({ USER_NOTIFICATION_ROUTES: routes }),
}
);
} catch (error) {
const mapped = mapApiErrorToActionResult(error);
if (mapped) {
return mapped;
}
);
throw error;
}
};
export const testSelfNotificationPreferences = async (
routes: Array<Record<string, unknown>>
): Promise<import('../types/settings').ActionResult> => {
return fetchJSON<import('../types/settings').ActionResult>(
`${API_BASE}/users/me/notification-preferences/test`,
{
method: 'POST',
body: JSON.stringify({ USER_NOTIFICATION_ROUTES: routes }),
try {
return await fetchJSON<import('../types/settings').ActionResult>(
`${API_BASE}/users/me/notification-preferences/test`,
{
method: 'POST',
body: JSON.stringify({ USER_NOTIFICATION_ROUTES: routes }),
}
);
} catch (error) {
const mapped = mapApiErrorToActionResult(error);
if (mapped) {
return mapped;
}
);
throw error;
}
};
export interface SettingsOverrideUserDetail {
+1
View File
@@ -220,6 +220,7 @@ export interface SettingsResponse {
export interface ActionResult {
success: boolean;
message: string;
details?: string[];
}
export interface UpdateResult {