feat(notifications): ntfy/Gotify push channels with live delivery (#69)

Per-user notification_channels (ntfy + Gotify): every
insertUserNotification now fans out best-effort to enabled channels —
deployment/backup/security/agent alerts reach the user's phone via the
ntfy app with zero extra infrastructure.

- POST/GET/DELETE /notifications/channels + POST .../:id/test which
  pushes a real probe so misconfiguration surfaces before alerts depend
  on it (502 DELIVERY_FAILED on endpoint errors).
- ntfy: POST to endpoint (topic in path), Title/Tags headers, optional
  Bearer token. Gotify: POST {endpoint}/message?token=...
- Delivery is best-effort by design — a dead relay never breaks the
  producing flow; one shared 5s-timeout client.
- CLI `notifications channels list|add|test|remove`, MCP tools,
  OpenAPI paths + NotificationChannel schema.
- Web settings "Push notifications" section and a Push channels card on
  the mobile notifications tab (add/test/remove).

Verified live: channel CRUD, test-ping hit a recording endpoint with
correct headers/body, and a real deployment fanned out "Deployment
succeeded" through the channel end-to-end.

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomáš DvořákandDevin authored and GitHub committed 2026-10-10 01:39:09 +02:00
1 parent 60145f7b21
commit 9243d020cc
19 files changed
+1261 -6

No files matched your search

+172
View File
@@ -3698,6 +3698,167 @@ export interface paths {
patch?: never;
trace?: never;
};
"/notifications/channels": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** List push notification channels (ntfy/Gotify) */
get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Channel list */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
channels?: components["schemas"]["NotificationChannel"][];
};
};
};
};
};
put?: never;
/**
* Add a push notification channel
* @description ntfy endpoint should include the topic (e.g. https://ntfy.example.com/my-alerts); Gotify endpoint is the server base URL with the app token in `token`.
*/
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @enum {string} */
kind: "ntfy" | "gotify";
/** Format: uri */
endpoint: string;
/** @description ntfy access token (Bearer) or Gotify app token */
token?: string;
};
};
};
responses: {
/** @description Channel created */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
channel?: components["schemas"]["NotificationChannel"];
};
};
};
400: components["schemas"]["Error"];
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/notifications/channels/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** Delete a push notification channel */
delete: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageResponse"];
};
};
404: components["schemas"]["Error"];
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/notifications/channels/{id}/test": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Send a test notification through a push channel */
post: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Test delivered */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageResponse"];
};
};
/** @description Push delivery failed */
502: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/webhooks": {
parameters: {
query?: never;
@@ -11824,6 +11985,17 @@ export interface components {
notifications?: components["schemas"]["Notification"][];
unread?: number;
};
NotificationChannel: {
/** Format: uuid */
id?: string;
/** @enum {string} */
kind?: "ntfy" | "gotify";
/** Format: uri */
endpoint?: string;
enabled?: boolean;
/** Format: date-time */
created_at?: string;
};
OutboundWebhook: {
/** Format: uuid */
id?: string;