feat: push selected events to a phone over ntfy
Adds a Notifications tab to Settings so an operator can pick which events reach their phone, published to a single self-hosted ntfy topic. The notifier subscribes to the realtime event hub the dashboard already consumes, so every notifiable event keeps exactly one publisher. Inbound media had no event, so `media.received` is added and published from the existing durable `gotImage` gate in the chat service — one notification per conversation, never repeated when the room re-syncs. It feeds the in-app notification bell as well. - Best-effort delivery: a rejecting or unreachable ntfy server is logged and never propagated into the path that produced the event. - Events buffered before the notifier attaches are skipped, so a restart cannot replay a backlog to somebody's phone. - Settings are applied field by field onto the live config object, so a save takes effect without a restart. - The access token is environment-owned (NTFY_AUTH_TOKEN) like the other secrets: stripped before the settings record is written, never returned by the API, which reports only `hasAuthToken`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
827eb3a1e0
commit
38d175e4a1
34 files changed
+1728
-8
No files matched your search
@@ -44,6 +44,7 @@ describe('ConfigStore', () => {
|
||||
config.web.authToken = 'also-secret';
|
||||
config.web.agentReadToken = 'read-agent-secret';
|
||||
config.web.agentControlToken = 'control-agent-secret';
|
||||
config.pushNotifications.authToken = 'ntfy-secret';
|
||||
store.save(config);
|
||||
|
||||
const row = db.prepare('SELECT config_json FROM app_config WHERE id = 1').get() as { config_json: string };
|
||||
@@ -51,9 +52,31 @@ describe('ConfigStore', () => {
|
||||
expect(row.config_json).not.toContain('also-secret');
|
||||
expect(row.config_json).not.toContain('read-agent-secret');
|
||||
expect(row.config_json).not.toContain('control-agent-secret');
|
||||
expect(row.config_json).not.toContain('ntfy-secret');
|
||||
expect(JSON.parse(row.config_json)).not.toHaveProperty('database');
|
||||
});
|
||||
|
||||
it('takes the ntfy access token from the environment on every load', () => {
|
||||
const config = getTestConfig();
|
||||
config.pushNotifications.topic = 'redditbot-7f3a9c21';
|
||||
config.pushNotifications.authToken = 'saved-token-must-be-ignored';
|
||||
store.save(config);
|
||||
|
||||
const withToken = store.load({
|
||||
OPENROUTER_API_KEY: 'environment-key',
|
||||
WEB_DASHBOARD_TOKEN: 'environment-dashboard-token',
|
||||
NTFY_AUTH_TOKEN: 'environment-ntfy-token',
|
||||
});
|
||||
const withoutToken = store.load({
|
||||
OPENROUTER_API_KEY: 'environment-key',
|
||||
WEB_DASHBOARD_TOKEN: 'environment-dashboard-token',
|
||||
});
|
||||
|
||||
expect(withToken?.pushNotifications.topic).toBe('redditbot-7f3a9c21');
|
||||
expect(withToken?.pushNotifications.authToken).toBe('environment-ntfy-token');
|
||||
expect(withoutToken?.pushNotifications.authToken).toBeUndefined();
|
||||
});
|
||||
|
||||
it('updates the singleton record instead of appending versions', () => {
|
||||
store.save(getTestConfig());
|
||||
store.save(getTestConfig({ browser: { sessionDir: './data/browser-session', dryRun: true } }));
|
||||
|
||||
Reference in new issue
Block a user