feat(notifications): add a Gotify payload format to the webhook

Gotify's POST /message types priority as an integer, so the generic body, which sends the word, was refused with a 400 before the token was ever the question. The webhook card now offers a payload format: generic, which stays byte for byte what it was, and gotify, which sends title, message, an integer priority and extras.

Priorities follow the Gotify Android client's channel thresholds: 5 for routine events (audible), 8 for medication reminders (the lowest heads-up value), 10 for urgent events. The extras declare plain text and carry the event name, masked to reminder in discreet mode as the generic body does.

The choice lives in the encrypted channel config, so no migration: an absent format is generic, only gotify is written, and a save that omits the field keeps the stored choice. GET returns the resolved value. The sender comment no longer suggests an Authorization Bearer header for Gotify, which takes X-Gotify-Key or a token query parameter.

Refs #947
This commit is contained in:
Marc-André Bombeck committed 2026-09-13 14:19:48 +02:00
1 parent 92606f6409
commit fafb5cdc08
17 files changed
+556 -50

No files matched your search

+7 -1
View File
@@ -22,6 +22,7 @@ import {
SHARING_NOT_PERMITTED_DESCRIPTION,
stdResponses,
} from "./shared";
import { WEBHOOK_PAYLOAD_FORMATS } from "@/lib/notifications/types";
// v1.16.11 — the one threshold read every dose-status consumer makes
// (cards, table, take-all-due derivation). `lateMinutes` /
@@ -255,7 +256,7 @@ const telegramSettingsRequest = telegramSettingsSchema.meta({
const webhookSettingsRequest = webhookSettingsSchema.meta({
id: "WebhookSettingsRequest",
description:
"Generic-webhook channel config — one channel covering Gotify, Discord, Slack, a Matrix bridge, Home Assistant, or any relay accepting an inbound JSON POST. `url` must pass the SSRF floor at input time and is re-checked at dispatch time; a private origin passes both only when the operator listed it in `NOTIFICATION_PRIVATE_ORIGINS`. An EMPTY OR OMITTED `headerValue` preserves the stored one, for the same reason as ntfy's `authToken`: the GET never returns it. There is no way to clear a stored header value through this endpoint.",
"Generic-webhook channel config — one channel covering Gotify, Discord, Slack, a Matrix bridge, Home Assistant, or any relay accepting an inbound JSON POST. `url` must pass the SSRF floor at input time and is re-checked at dispatch time; a private origin passes both only when the operator listed it in `NOTIFICATION_PRIVATE_ORIGINS`. An EMPTY OR OMITTED `headerValue` preserves the stored one, for the same reason as ntfy's `authToken`: the GET never returns it. There is no way to clear a stored header value through this endpoint. An omitted `format` keeps the stored body shape.",
});
/**
@@ -343,6 +344,11 @@ const webhookSettingsResponse = z
.describe(
"Whether a header value (the shared secret) is stored. The value itself is never returned.",
),
format: z
.enum(WEBHOOK_PAYLOAD_FORMATS)
.describe(
"Body shape the webhook sends. `generic` when no channel row exists or the config predates the choice.",
),
})
.meta({
id: "WebhookSettingsResponse",