Add notification channels and a provider abstraction

Users get a notification_channels table (replacing users.ntfy_topic); pages fan
out to every enabled channel through a notify.Provider registry, with ntfy as
the first provider. Fallbacks are typed {type, address}. Schema edited in place:
existing databases must be recreated.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
This commit is contained in:
Niklas Ye
2026-10-12 21:00:40 +02:00
parent 6292ad6708
commit b114251f45
42 changed files with 2200 additions and 611 deletions
+12 -8
View File
@@ -36,10 +36,10 @@ export async function load() {
]);
steps = [
{
id: 'topic',
id: 'channel',
title: 'Set where your pages go',
text: 'An ntfy topic on your account. Without one, incidents assigned to you page the team’s fallback topic instead of your phone.',
done: Boolean(state.me?.user?.ntfy_topic),
text: 'A notification channel on your account. Without one, incidents assigned to you page the team’s fallback instead of you.',
done: Boolean(firstChannel()),
action: { label: 'Account', go: '/more' },
},
{
@@ -115,10 +115,10 @@ function stepRow(step) {
class: 'btn-sm', type: 'button', text: step.action.label,
onclick: () => navigate(step.action.go),
}),
// The topic step is the only one this page can finish by itself, and
// The channel step is the only one this page can finish by itself, and
// the only proof that matters is a phone buzzing.
step.id === 'topic' && state.me?.user?.ntfy_topic && h('button', {
class: 'btn-sm', type: 'button', text: 'Send a test push',
step.id === 'channel' && firstChannel() && h('button', {
class: 'btn-sm', type: 'button', text: 'Send a test',
disabled: busy,
onclick: sendTest,
}),
@@ -126,11 +126,15 @@ function stepRow(step) {
);
}
// The first channel that is switched on: the one a page would reach first.
const firstChannel = () => (state.me?.user?.channels || []).find((c) => c.enabled);
async function sendTest() {
busy = true;
try {
await api.testNotification();
testResult = { ok: true, text: 'Sent. If nothing arrives, the topic is wrong or ntfy is not reachable.' };
const channel = firstChannel();
await api.testChannel(state.me.user.id, channel.id);
testResult = { ok: true, text: 'Sent. If nothing arrives, the address is wrong or the server cannot reach the provider.' };
} catch (err) {
testResult = { ok: false, text: err.message };
} finally {