feat: add Apprise as a fourth notification channel (#1644)

* feat: add Apprise as a fourth notification channel

Support keyed and stateless Apprise endpoints with secret-safe public DTOs, fail-closed malformed config, and mode-specific Settings UI. Docs and screenshots updated for four-channel Channels and routing.

* fix: harden Apprise secrets at rest and preserve-on-write saves

Encrypt Apprise endpoint and config with CryptoService so a downgrade cannot leak via SELECT *. Align channel and routing saves so blank destination fields omit config on same-mode URL edits, enforce keyed notify IDs, and keep secrets_redacted truthful.

* fix: harden Apprise route type changes and mixed-version config UI

Require a raw channel_url when switching notification-route types so ciphertext cannot strand under Discord/Slack/webhook. Default missing remote apprise status, replace Channels state on node switch, and exercise the production config-column migrator.

* fix: tolerate stub fleet configuration payloads without agents

Normalize remote Apprise agent status only when notifications.agents is present so successful Pilot/stub fetches stay online instead of throwing into the offline catch path.

* fix: correct TypeScript in configuration normalize tests

* fix: ignore stale Channels agent bodies after node switch

Compare the active node after response JSON parsing so a slow body
cannot overwrite the newly selected node's channel state.

* fix: isolate corrupt Apprise crypto and keep keyed Tags visible

Decrypt failures on one Apprise row no longer 500 agent/route lists or
suppress sibling channel dispatch. Treat public /notify/<redacted> as keyed
so Tags remain editable after reload.
This commit is contained in:
Anso
2026-07-18 16:32:58 -04:00
committed by GitHub
parent 674220b9de
commit 83b3d932e5
42 changed files with 2916 additions and 136 deletions
@@ -108,4 +108,29 @@ describe('conditionalJsonParser remote-proxy bypass', () => {
expect(lastUpstreamAuth).toBeNull();
expect([200, 404]).toContain(res.status);
});
it('forwards Apprise agent config bodies intact to the remote', async () => {
lastUpstreamBody = null;
lastUpstreamAuth = null;
const payload = {
type: 'apprise',
url: 'http://apprise.local/notify',
enabled: true,
config: { urls: 'discord://webhook-id/webhook-token?token=query-secret' },
};
const res = await request(app)
.post('/api/agents')
.set('Authorization', authHeader)
.set('x-node-id', String(remoteNodeId))
.set('Content-Type', 'application/json')
.send(payload);
expect(res.status).toBe(200);
expect(lastUpstreamBody).not.toBeNull();
expect(JSON.parse(lastUpstreamBody!.toString('utf-8'))).toEqual(payload);
expect(lastUpstreamAuth).toBe('Bearer bypass-test-token');
expect(JSON.stringify(res.body)).not.toContain('query-secret');
});
});