feat(FN-1127): add awaiting-approval ntfy notifications
- Extend ntfy event types and default ntfyEvents to include awaiting-approval - Send high-priority "Plan needs approval" notifications for awaiting-approval task updates with deep-link support - Add Notifications settings checkbox handling for awaiting-approval event selection in Settings modal - Expand notifier and settings modal tests for awaiting-approval defaults, toggles, filtering, and deduplication - Update AGENTS.md ntfy documentation and sample config to include the new event
This commit is contained in:
@@ -104,7 +104,7 @@ export interface WorkflowStep {
|
||||
|
||||
/** Input for creating a new workflow step. */
|
||||
/** Event types that can trigger ntfy notifications */
|
||||
export type NtfyNotificationEvent = "in-review" | "merged" | "failed";
|
||||
export type NtfyNotificationEvent = "in-review" | "merged" | "failed" | "awaiting-approval";
|
||||
|
||||
export interface WorkflowStepInput {
|
||||
/** Built-in template source ID when creating a concrete step from a template. */
|
||||
@@ -990,7 +990,7 @@ export const DEFAULT_GLOBAL_SETTINGS: Required<Pick<GlobalSettings, "themeMode"
|
||||
defaultThinkingLevel: undefined,
|
||||
ntfyEnabled: false,
|
||||
ntfyTopic: undefined,
|
||||
ntfyEvents: ["in-review", "merged", "failed"],
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
ntfyDashboardHost: undefined,
|
||||
openrouterModelSync: true,
|
||||
modelOnboardingComplete: undefined,
|
||||
|
||||
@@ -1679,7 +1679,7 @@ export function SettingsModal({
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("in-review") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed"] as NtfyNotificationEvent[]);
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed", "awaiting-approval"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? (current.includes("in-review") ? current : [...current, "in-review" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "in-review");
|
||||
@@ -1695,7 +1695,7 @@ export function SettingsModal({
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("merged") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed"] as NtfyNotificationEvent[]);
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed", "awaiting-approval"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? (current.includes("merged") ? current : [...current, "merged" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "merged");
|
||||
@@ -1711,7 +1711,7 @@ export function SettingsModal({
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("failed") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed"] as NtfyNotificationEvent[]);
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed", "awaiting-approval"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? (current.includes("failed") ? current : [...current, "failed" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "failed");
|
||||
@@ -1721,6 +1721,22 @@ export function SettingsModal({
|
||||
Task failed
|
||||
</label>
|
||||
<small>When a task fails during execution (high priority)</small>
|
||||
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("awaiting-approval") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed", "awaiting-approval"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? (current.includes("awaiting-approval") ? current : [...current, "awaiting-approval" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "awaiting-approval");
|
||||
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
|
||||
}}
|
||||
/>
|
||||
Plan needs approval
|
||||
</label>
|
||||
<small>When a task specification needs manual approval before execution</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
|
||||
@@ -22,7 +22,7 @@ const defaultSettings: Settings = {
|
||||
defaultPresetBySize: {},
|
||||
ntfyEnabled: false,
|
||||
ntfyTopic: undefined,
|
||||
ntfyEvents: ["in-review", "merged", "failed"],
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
taskStuckTimeoutMs: undefined,
|
||||
maxStuckKills: 6,
|
||||
runStepsInNewSessions: false,
|
||||
@@ -1570,7 +1570,7 @@ describe("SettingsModal", () => {
|
||||
...defaultSettings,
|
||||
ntfyEnabled: true,
|
||||
ntfyTopic: "my-topic",
|
||||
ntfyEvents: ["in-review", "merged", "failed"],
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
@@ -1580,6 +1580,22 @@ describe("SettingsModal", () => {
|
||||
await waitFor(() => expect(screen.getByLabelText("Task completed (in-review)")).toBeTruthy());
|
||||
expect(screen.getByLabelText("Task merged")).toBeTruthy();
|
||||
expect(screen.getByLabelText("Task failed")).toBeTruthy();
|
||||
expect(screen.getByLabelText("Plan needs approval")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows awaiting-approval checkbox when ntfy is enabled", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
ntfyEnabled: true,
|
||||
ntfyTopic: "my-topic",
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Notifications"));
|
||||
expect(screen.getByLabelText("Plan needs approval")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides ntfyEvents checkboxes when ntfy is disabled", async () => {
|
||||
@@ -1590,6 +1606,7 @@ describe("SettingsModal", () => {
|
||||
expect(screen.queryByLabelText("Task completed (in-review)")).toBeNull();
|
||||
expect(screen.queryByLabelText("Task merged")).toBeNull();
|
||||
expect(screen.queryByLabelText("Task failed")).toBeNull();
|
||||
expect(screen.queryByLabelText("Plan needs approval")).toBeNull();
|
||||
});
|
||||
|
||||
it("ntfyEvents checkboxes are all checked by default", async () => {
|
||||
@@ -1606,6 +1623,7 @@ describe("SettingsModal", () => {
|
||||
expect((screen.getByLabelText("Task completed (in-review)") as HTMLInputElement).checked).toBe(true);
|
||||
expect((screen.getByLabelText("Task merged") as HTMLInputElement).checked).toBe(true);
|
||||
expect((screen.getByLabelText("Task failed") as HTMLInputElement).checked).toBe(true);
|
||||
expect((screen.getByLabelText("Plan needs approval") as HTMLInputElement).checked).toBe(true);
|
||||
});
|
||||
|
||||
it("saves ntfyEvents correctly when checkboxes are toggled", async () => {
|
||||
@@ -1613,7 +1631,7 @@ describe("SettingsModal", () => {
|
||||
...defaultSettings,
|
||||
ntfyEnabled: true,
|
||||
ntfyTopic: "my-topic",
|
||||
ntfyEvents: ["in-review", "merged", "failed"],
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
@@ -1629,7 +1647,7 @@ describe("SettingsModal", () => {
|
||||
await waitFor(() => expect(updateGlobalSettings).toHaveBeenCalledTimes(1));
|
||||
|
||||
const payload = (updateGlobalSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(payload.ntfyEvents).toEqual(["in-review", "failed"]);
|
||||
expect(payload.ntfyEvents).toEqual(["in-review", "failed", "awaiting-approval"]);
|
||||
});
|
||||
|
||||
it("sets ntfyEvents to undefined when all checkboxes are unchecked", async () => {
|
||||
@@ -1637,7 +1655,7 @@ describe("SettingsModal", () => {
|
||||
...defaultSettings,
|
||||
ntfyEnabled: true,
|
||||
ntfyTopic: "my-topic",
|
||||
ntfyEvents: ["in-review", "merged", "failed"],
|
||||
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
@@ -1645,10 +1663,11 @@ describe("SettingsModal", () => {
|
||||
|
||||
fireEvent.click(screen.getByText("Notifications"));
|
||||
|
||||
// Uncheck all three
|
||||
// Uncheck all four
|
||||
fireEvent.click(screen.getByLabelText("Task completed (in-review)"));
|
||||
fireEvent.click(screen.getByLabelText("Task merged"));
|
||||
fireEvent.click(screen.getByLabelText("Task failed"));
|
||||
fireEvent.click(screen.getByLabelText("Plan needs approval"));
|
||||
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
await waitFor(() => expect(updateGlobalSettings).toHaveBeenCalledTimes(1));
|
||||
@@ -1672,6 +1691,7 @@ describe("SettingsModal", () => {
|
||||
expect((screen.getByLabelText("Task completed (in-review)") as HTMLInputElement).checked).toBe(true);
|
||||
expect((screen.getByLabelText("Task merged") as HTMLInputElement).checked).toBe(false);
|
||||
expect((screen.getByLabelText("Task failed") as HTMLInputElement).checked).toBe(false);
|
||||
expect((screen.getByLabelText("Plan needs approval") as HTMLInputElement).checked).toBe(false);
|
||||
});
|
||||
|
||||
// Model filter tests with CustomModelDropdown
|
||||
|
||||
@@ -178,6 +178,29 @@ describe("NtfyNotifier", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sends high priority notification when task is awaiting approval", async () => {
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
|
||||
const awaitingApprovalTask = createTask("FN-002", "Spec Task", "awaiting-approval");
|
||||
store.triggerTaskUpdated(awaitingApprovalTask);
|
||||
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://ntfy.sh/test-topic",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.objectContaining({
|
||||
"Title": "Plan needs approval for FN-002",
|
||||
"Priority": "high",
|
||||
}),
|
||||
body: 'Task "Spec Task" needs your approval before it can proceed',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sends notification when task is merged", async () => {
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
@@ -391,6 +414,29 @@ describe("NtfyNotifier", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("includes Click header for awaiting-approval notifications when dashboard host is set", async () => {
|
||||
store.setSettings({
|
||||
ntfyEnabled: true,
|
||||
ntfyTopic: "test-topic",
|
||||
ntfyDashboardHost: "https://fusion.example.com",
|
||||
});
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
|
||||
const awaitingApprovalTask = createTask("FN-002", "Spec Task", "awaiting-approval");
|
||||
store.triggerTaskUpdated(awaitingApprovalTask);
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://ntfy.sh/test-topic",
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"Click": "https://fusion.example.com/?task=FN-002",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("includes Click header for merged task notifications when dashboard host is set", async () => {
|
||||
store.setSettings({
|
||||
ntfyEnabled: true,
|
||||
@@ -700,6 +746,29 @@ describe("NtfyNotifier", () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("prevents duplicate awaiting-approval notifications for the same task", async () => {
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
|
||||
const task = createTask("FN-004", "Approval Task", "awaiting-approval");
|
||||
|
||||
store.triggerTaskUpdated(task);
|
||||
store.triggerTaskUpdated(task);
|
||||
store.triggerTaskUpdated(task);
|
||||
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://ntfy.sh/test-topic",
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"Title": "Plan needs approval for FN-004",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("allows different event types for the same task", async () => {
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
@@ -726,6 +795,21 @@ describe("NtfyNotifier", () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("allows awaiting-approval alongside other event types for the same task", async () => {
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
|
||||
const task = createTask("FN-005", "Approval + Failure");
|
||||
|
||||
store.triggerTaskUpdated({ ...task, status: "awaiting-approval" });
|
||||
await flushAsyncWork();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
store.triggerTaskUpdated({ ...task, status: "failed" });
|
||||
await flushAsyncWork();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("sends notification only once on merge when task:moved and task:merged both fire", async () => {
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
@@ -982,7 +1066,7 @@ describe("NtfyNotifier", () => {
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not notify on task:updated when status is not failed", async () => {
|
||||
it("does not notify on task:updated when status is neither failed nor awaiting-approval", async () => {
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic" });
|
||||
fetchMock.mockResolvedValue({ ok: true });
|
||||
notifier = new NtfyNotifier(store);
|
||||
@@ -1016,7 +1100,7 @@ describe("NtfyNotifier", () => {
|
||||
});
|
||||
|
||||
it("does not send in-review notification when 'in-review' is not in ntfyEvents", async () => {
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic", ntfyEvents: ["merged", "failed"] });
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic", ntfyEvents: ["merged", "failed", "awaiting-approval"] });
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
|
||||
@@ -1027,7 +1111,7 @@ describe("NtfyNotifier", () => {
|
||||
});
|
||||
|
||||
it("does not send merged notification when 'merged' is not in ntfyEvents", async () => {
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic", ntfyEvents: ["in-review", "failed"] });
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic", ntfyEvents: ["in-review", "failed", "awaiting-approval"] });
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
|
||||
@@ -1045,7 +1129,7 @@ describe("NtfyNotifier", () => {
|
||||
});
|
||||
|
||||
it("does not send failed notification when 'failed' is not in ntfyEvents", async () => {
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic", ntfyEvents: ["in-review", "merged"] });
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic", ntfyEvents: ["in-review", "merged", "awaiting-approval"] });
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
|
||||
@@ -1056,6 +1140,18 @@ describe("NtfyNotifier", () => {
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not send awaiting-approval notification when 'awaiting-approval' is not in ntfyEvents", async () => {
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic", ntfyEvents: ["in-review", "merged", "failed"] });
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
|
||||
const awaitingApprovalTask = createTask("FN-006", "Approval Task", "awaiting-approval");
|
||||
store.triggerTaskUpdated(awaitingApprovalTask);
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends notification for enabled events while others are disabled", async () => {
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic", ntfyEvents: ["in-review"] });
|
||||
notifier = new NtfyNotifier(store);
|
||||
@@ -1083,6 +1179,12 @@ describe("NtfyNotifier", () => {
|
||||
store.triggerTaskUpdated(failedTask);
|
||||
await flushAsyncWork();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1); // Still 1, no new call
|
||||
|
||||
// awaiting-approval - should NOT send
|
||||
const awaitingApprovalTask = createTask("FN-004", "Test Task 4", "awaiting-approval");
|
||||
store.triggerTaskUpdated(awaitingApprovalTask);
|
||||
await flushAsyncWork();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1); // Still 1, no new call
|
||||
});
|
||||
|
||||
it("defaults to all events when ntfyEvents is undefined", async () => {
|
||||
@@ -1109,10 +1211,15 @@ describe("NtfyNotifier", () => {
|
||||
store.triggerTaskUpdated(failedTask);
|
||||
await flushAsyncWork();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
|
||||
const awaitingApprovalTask = createTask("FN-004", "Test Task 4", "awaiting-approval");
|
||||
store.triggerTaskUpdated(awaitingApprovalTask);
|
||||
await flushAsyncWork();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("updates notifications when ntfyEvents changes at runtime", async () => {
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic", ntfyEvents: ["in-review", "merged", "failed"] });
|
||||
store.setSettings({ ntfyEnabled: true, ntfyTopic: "test-topic", ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"] });
|
||||
notifier = new NtfyNotifier(store);
|
||||
await notifier.start();
|
||||
|
||||
@@ -1122,14 +1229,14 @@ describe("NtfyNotifier", () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Disable in-review
|
||||
store.setSettings({ ntfyEvents: ["merged", "failed"] });
|
||||
store.setSettings({ ntfyEvents: ["merged", "failed", "awaiting-approval"] });
|
||||
|
||||
store.triggerTaskMoved(createTask("FN-002", "Test Task 2"), "in-progress", "in-review");
|
||||
await flushAsyncWork();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1); // No new call for in-review
|
||||
|
||||
// Enable in-review again
|
||||
store.setSettings({ ntfyEvents: ["in-review", "merged", "failed"] });
|
||||
store.setSettings({ ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"] });
|
||||
|
||||
store.triggerTaskMoved(createTask("FN-003", "Test Task 3"), "in-progress", "in-review");
|
||||
await flushAsyncWork();
|
||||
|
||||
@@ -40,7 +40,7 @@ interface NtfyConfig {
|
||||
}
|
||||
|
||||
/** Event types for notification deduplication */
|
||||
type NotificationEventType = "in-review" | "merged" | "failed";
|
||||
type NotificationEventType = "in-review" | "merged" | "failed" | "awaiting-approval";
|
||||
|
||||
/**
|
||||
* NtfyNotifier sends push notifications via ntfy.sh when tasks complete
|
||||
@@ -50,11 +50,11 @@ type NotificationEventType = "in-review" | "merged" | "failed";
|
||||
* Features:
|
||||
* - Runtime reconfiguration via settings:updated events
|
||||
* - Best-effort delivery (errors are logged but never thrown)
|
||||
* - Duplicate prevention per event type (in-review, merged, failed)
|
||||
* - Duplicate prevention per event type (in-review, merged, failed, awaiting-approval)
|
||||
* - Configurable notification events (hardcoded defaults)
|
||||
*/
|
||||
export class NtfyNotifier {
|
||||
private config: NtfyConfig = { enabled: false, topic: undefined, dashboardHost: undefined, events: ["in-review", "merged", "failed"] };
|
||||
private config: NtfyConfig = { enabled: false, topic: undefined, dashboardHost: undefined, events: ["in-review", "merged", "failed", "awaiting-approval"] };
|
||||
private ntfyBaseUrl: string;
|
||||
/** Project identifier for deep links in notifications */
|
||||
private projectId?: string;
|
||||
@@ -157,6 +157,20 @@ export class NtfyNotifier {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Notify when task requires manual plan approval
|
||||
if (task.status === "awaiting-approval" && this.isEventEnabled("awaiting-approval")) {
|
||||
const clickUrl = this.buildTaskUrl(task.id);
|
||||
this.maybeNotify(task.id, "awaiting-approval", () =>
|
||||
this.sendNotification(
|
||||
this.config.topic!,
|
||||
`Plan needs approval for ${task.id}`,
|
||||
`Task "${formatTaskIdentifier(task)}" needs your approval before it can proceed`,
|
||||
"high",
|
||||
clickUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private handleTaskMerged = (result: MergeResult): void => {
|
||||
@@ -207,7 +221,7 @@ export class NtfyNotifier {
|
||||
enabled: settings.ntfyEnabled ?? false,
|
||||
topic: settings.ntfyTopic,
|
||||
dashboardHost: settings.ntfyDashboardHost,
|
||||
events: settings.ntfyEvents ?? ["in-review", "merged", "failed"],
|
||||
events: settings.ntfyEvents ?? ["in-review", "merged", "failed", "awaiting-approval"],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user