fix(FN-3825): surface inline notification test feedback in settings
- Persist per-provider notification test results and render inline success/error feedback blocks - Add ntfy "Test message notification" action wired to message-event test payload - Keep toast notifications while also showing provider-specific status text with aria-live feedback - Update SettingsModal tests to verify ntfy message-event call path and inline webhook/ntfy feedback rendering Fusion-Task-Id: FN-3825
This commit is contained in:
@@ -41,6 +41,7 @@ export class NotificationService {
|
||||
private notificationsEnabled = false;
|
||||
private ntfyProvider?: NtfyNotificationProvider;
|
||||
private webhookProvider?: WebhookNotificationProvider;
|
||||
private refreshInFlight: Promise<void> | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly store: NotificationServiceStore,
|
||||
@@ -236,8 +237,19 @@ export class NotificationService {
|
||||
}
|
||||
|
||||
private handleMessageSent = (message: Message): void => {
|
||||
void this.handleMessageSentAsync(message);
|
||||
};
|
||||
|
||||
private async handleMessageSentAsync(message: Message): Promise<void> {
|
||||
schedulerLog.log(
|
||||
`NotificationService.handleMessageSent messageId=${message.id} type=${message.type} notificationsEnabled=${String(this.notificationsEnabled)} hasNtfyProvider=${String(Boolean(this.ntfyProvider))}`,
|
||||
);
|
||||
|
||||
if (!this.notificationsEnabled) {
|
||||
return;
|
||||
await this.refreshNotificationState("message:sent");
|
||||
if (!this.notificationsEnabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let eventType: NotificationEvent;
|
||||
@@ -270,7 +282,11 @@ export class NotificationService {
|
||||
preview,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
schedulerLog.log(
|
||||
`NotificationService.handleMessageSent scheduled eventType=${eventType} messageId=${message.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
private setNotificationsEnabledFromSettings(settings: Settings): void {
|
||||
this.notificationsEnabled = Boolean(
|
||||
@@ -281,13 +297,37 @@ export class NotificationService {
|
||||
|
||||
async dispatch(eventType: NotificationEvent, payload: NotificationPayload): Promise<void> {
|
||||
if (!this.notificationsEnabled) {
|
||||
return;
|
||||
await this.refreshNotificationState("manual-dispatch");
|
||||
if (!this.notificationsEnabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const dedupTaskId = payload.taskId ?? "global";
|
||||
this.maybeNotify(dedupTaskId, eventType, payload);
|
||||
}
|
||||
|
||||
private async refreshNotificationState(reason: string): Promise<void> {
|
||||
if (this.refreshInFlight) {
|
||||
await this.refreshInFlight;
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshInFlight = (async () => {
|
||||
const settings = await this.store.getSettings();
|
||||
this.setNotificationsEnabledFromSettings(settings);
|
||||
await this.syncNtfyProvider(settings);
|
||||
await this.syncWebhookProvider(settings);
|
||||
schedulerLog.log(`NotificationService refreshed notification state reason=${reason} enabled=${String(this.notificationsEnabled)}`);
|
||||
})();
|
||||
|
||||
try {
|
||||
await this.refreshInFlight;
|
||||
} finally {
|
||||
this.refreshInFlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
private createTaskPayload(task: Task, event: NotificationEvent): NotificationPayload {
|
||||
return {
|
||||
taskId: task.id,
|
||||
@@ -300,10 +340,12 @@ export class NotificationService {
|
||||
private maybeNotify(taskId: string, eventType: NotificationEvent, payload: NotificationPayload): void {
|
||||
const key = `${taskId}:${eventType}`;
|
||||
if (this.notifiedEvents.has(key)) {
|
||||
schedulerLog.log(`NotificationService.maybeNotify suppressed duplicate key=${key}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifiedEvents.add(key);
|
||||
schedulerLog.log(`NotificationService.maybeNotify dispatching key=${key}`);
|
||||
this.dispatcher.dispatch(eventType, payload).catch(() => {
|
||||
// best effort dispatch
|
||||
});
|
||||
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
buildNtfyClickUrl,
|
||||
formatTaskIdentifier,
|
||||
resolveNtfyEvents,
|
||||
sendNtfyNotification,
|
||||
sendNtfyNotificationWithResult,
|
||||
} from "../notifier.js";
|
||||
import { schedulerLog } from "../logger.js";
|
||||
|
||||
export interface NtfyProviderConfig {
|
||||
/** ntfy topic name */
|
||||
@@ -75,11 +76,16 @@ export class NtfyNotificationProvider implements NotificationProvider {
|
||||
|
||||
isEventSupported(event: NotificationEvent): boolean {
|
||||
if (!SUPPORTED_EVENTS.has(event as SupportedNtfyEvent)) {
|
||||
schedulerLog.log(`NtfyNotificationProvider event filtered unsupported event=${event}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const enabledEvents = this.config?.events ?? [...DEFAULT_NTFY_EVENTS];
|
||||
return enabledEvents.includes(event as NtfyNotificationEvent);
|
||||
const allowed = enabledEvents.includes(event as NtfyNotificationEvent);
|
||||
schedulerLog.log(
|
||||
`NtfyNotificationProvider allowlist event=${event} decision=${allowed ? "allowed" : "filtered-by-event"}`,
|
||||
);
|
||||
return allowed;
|
||||
}
|
||||
|
||||
async sendNotification(
|
||||
@@ -173,7 +179,20 @@ export class NtfyNotificationProvider implements NotificationProvider {
|
||||
};
|
||||
|
||||
const content = contentByEvent[event as SupportedNtfyEvent];
|
||||
await sendNtfyNotification({
|
||||
const resolvedBaseUrl = this.config.ntfyBaseUrl?.trim() || "https://ntfy.sh";
|
||||
const host = (() => {
|
||||
try {
|
||||
return new URL(resolvedBaseUrl).host;
|
||||
} catch {
|
||||
return "invalid-host";
|
||||
}
|
||||
})();
|
||||
|
||||
schedulerLog.log(
|
||||
`NtfyNotificationProvider send event=${event} host=${host} topic=${this.config.topic}`,
|
||||
);
|
||||
|
||||
const response = await sendNtfyNotificationWithResult({
|
||||
ntfyBaseUrl: this.config.ntfyBaseUrl,
|
||||
topic: this.config.topic,
|
||||
title: content.title,
|
||||
@@ -183,6 +202,14 @@ export class NtfyNotificationProvider implements NotificationProvider {
|
||||
signal: this.abortController?.signal,
|
||||
});
|
||||
|
||||
return { success: true, providerId: this.getProviderId() };
|
||||
schedulerLog.log(
|
||||
`NtfyNotificationProvider delivery event=${event} status=${response?.status ?? "error"} ok=${String(response?.ok ?? false)}`,
|
||||
);
|
||||
|
||||
return {
|
||||
success: Boolean(response?.ok),
|
||||
providerId: this.getProviderId(),
|
||||
...(response?.ok ? {} : { error: response ? `${response.status} ${response.statusText}` : "request failed" }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user