Notify a phone that has this installation installed
A `webpush` alert channel, and the PWA it needs to arrive. The payload is encrypted to the subscription (RFC 8291) and the request signed with this installation's own keypair (RFC 8292), both over `http-ece` — `pywebpush` does the same in one call but brings `requests` and `aiohttp` with it, two HTTP stacks beside httpx on a machine that may be a Raspberry Pi. The manifest and the worker are hand-written rather than `vite-plugin-pwa`: there is nothing worth precaching when the page carrying the credential is `no-store`, so the worker handles `push` and `notificationclick` and nothing else. `registration.scope` is the app's root in both places it runs, which is why the payload carries no URL. A run finishing in error is the first event worth waking someone for; `ok` and `cancelled` describe to nothing, so a nightly batch that works stays quiet. The events were already on the bus — only the filter changed. `WEBPUSH_FILE` is a derived path, so the keypair lands on the data volume with the alerts beside it. Off it, a rebuild would silently stop every phone being notified: the key they subscribed against would be gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EbeFPm6WNC3YD9vrqqT3a
This commit is contained in:
@@ -103,6 +103,17 @@ export const ArtifactRowSchema = {
|
||||
media_type: {
|
||||
type: 'string',
|
||||
title: 'Media Type'
|
||||
},
|
||||
filename: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'null'
|
||||
}
|
||||
],
|
||||
title: 'Filename'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
@@ -351,7 +362,7 @@ export const ChannelSchema = {
|
||||
},
|
||||
kind: {
|
||||
type: 'string',
|
||||
enum: ['ntfy', 'smtp', 'webhook', 'dashboard'],
|
||||
enum: ['ntfy', 'smtp', 'webhook', 'dashboard', 'webpush'],
|
||||
title: 'Kind'
|
||||
},
|
||||
enabled: {
|
||||
@@ -3130,6 +3141,26 @@ export const ShareRequestSchema = {
|
||||
title: 'ShareRequest'
|
||||
} as const;
|
||||
|
||||
export const SubscriptionSchema = {
|
||||
properties: {
|
||||
endpoint: {
|
||||
type: 'string',
|
||||
title: 'Endpoint'
|
||||
},
|
||||
keys: {
|
||||
additionalProperties: {
|
||||
type: 'string'
|
||||
},
|
||||
type: 'object',
|
||||
title: 'Keys'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['endpoint'],
|
||||
title: 'Subscription',
|
||||
description: "What a browser handed us. Opaque apart from the endpoint's host."
|
||||
} as const;
|
||||
|
||||
export const SweepCreateSchema = {
|
||||
properties: {
|
||||
runs: {
|
||||
@@ -3293,6 +3324,18 @@ export const TriggerRequestSchema = {
|
||||
title: 'TriggerRequest'
|
||||
} as const;
|
||||
|
||||
export const UnsubscribeSchema = {
|
||||
properties: {
|
||||
endpoint: {
|
||||
type: 'string',
|
||||
title: 'Endpoint'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['endpoint'],
|
||||
title: 'Unsubscribe'
|
||||
} as const;
|
||||
|
||||
export const UpdatePasswordSchema = {
|
||||
properties: {
|
||||
current_password: {
|
||||
@@ -3699,6 +3742,18 @@ export const WaitingNodeSchema = {
|
||||
title: 'WaitingNode'
|
||||
} as const;
|
||||
|
||||
export const WebPushKeySchema = {
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
title: 'Key'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['key'],
|
||||
title: 'WebPushKey'
|
||||
} as const;
|
||||
|
||||
export const WidgetDefSchema = {
|
||||
properties: {
|
||||
id: {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -34,6 +34,7 @@ export type ArtifactRow = {
|
||||
digest: string;
|
||||
size: number;
|
||||
media_type: string;
|
||||
filename?: (string | null);
|
||||
};
|
||||
|
||||
export type Body_login_login_access_token = {
|
||||
@@ -97,14 +98,14 @@ export type BrainNode = {
|
||||
*/
|
||||
export type Channel = {
|
||||
name: string;
|
||||
kind: 'ntfy' | 'smtp' | 'webhook' | 'dashboard';
|
||||
kind: 'ntfy' | 'smtp' | 'webhook' | 'dashboard' | 'webpush';
|
||||
enabled?: boolean;
|
||||
config?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export type kind = 'ntfy' | 'smtp' | 'webhook' | 'dashboard';
|
||||
export type kind = 'ntfy' | 'smtp' | 'webhook' | 'dashboard' | 'webpush';
|
||||
|
||||
/**
|
||||
* A dashboard as stored, and as the API hands it over.
|
||||
@@ -1116,6 +1117,16 @@ export type ShareRequest = {
|
||||
lib_name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* What a browser handed us. Opaque apart from the endpoint's host.
|
||||
*/
|
||||
export type Subscription = {
|
||||
endpoint: string;
|
||||
keys?: {
|
||||
[key: string]: (string);
|
||||
};
|
||||
};
|
||||
|
||||
export type SweepCreate = {
|
||||
runs?: Array<SweepEntry>;
|
||||
draft?: boolean;
|
||||
@@ -1163,6 +1174,10 @@ export type TriggerRequest = {
|
||||
};
|
||||
};
|
||||
|
||||
export type Unsubscribe = {
|
||||
endpoint: string;
|
||||
};
|
||||
|
||||
export type UpdatePassword = {
|
||||
current_password: string;
|
||||
new_password: string;
|
||||
@@ -1249,6 +1264,10 @@ export type WaitingNode = {
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
export type WebPushKey = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* One tile: what it shows or does, and where it sits.
|
||||
*
|
||||
@@ -1312,6 +1331,20 @@ export type AlertsSaveAlertsConfigData = {
|
||||
|
||||
export type AlertsSaveAlertsConfigResponse = (AlertsConfig);
|
||||
|
||||
export type AlertsReadWebpushKeyResponse = (WebPushKey);
|
||||
|
||||
export type AlertsAddWebpushSubscriptionData = {
|
||||
requestBody: Subscription;
|
||||
};
|
||||
|
||||
export type AlertsAddWebpushSubscriptionResponse = (Message);
|
||||
|
||||
export type AlertsRemoveWebpushSubscriptionData = {
|
||||
requestBody: Unsubscribe;
|
||||
};
|
||||
|
||||
export type AlertsRemoveWebpushSubscriptionResponse = (Message);
|
||||
|
||||
export type AlertsTestChannelData = {
|
||||
channelName: string;
|
||||
};
|
||||
@@ -1677,6 +1710,8 @@ export type ObservabilityReadTimeseriesData = {
|
||||
flow?: (string | null);
|
||||
hours?: number;
|
||||
node?: (string | null);
|
||||
since?: (string | null);
|
||||
until?: (string | null);
|
||||
};
|
||||
|
||||
export type ObservabilityReadTimeseriesResponse = (Array<SeriesPoint>);
|
||||
|
||||
Reference in New Issue
Block a user