# Pushover Notification Provider — Implementation Plan **Date:** 2026-07-21 **Author:** Planning Agent **Confidence Score:** 90% (High — established provider patterns, Pushover API well-documented) **Prior Art:** `docs/plans/telegram_implementation_spec.md` (Telegram followed identical pattern) --- ## 1. Introduction ### Objective Add Pushover as a first-class notification provider in Charon, following the same architectural pattern used by Telegram, Slack, Gotify, Discord, Email, and generic Webhook providers. ### Goals - Users can configure a Pushover API token and user key to receive push notifications - All existing notification event types (proxy hosts, certs, uptime, security events) work with Pushover - JSON template engine (minimal/detailed/custom) works with Pushover - Feature flag allows enabling/disabling Pushover dispatch independently - API token is treated as a secret (write-only, never exposed in API responses) - Full test coverage: Go unit tests, Vitest frontend tests, Playwright E2E tests ### Pushover API Overview Pushover messages are sent via: ``` POST https://api.pushover.net/1/messages.json Content-Type: application/x-www-form-urlencoded token=&user=&message=Hello+world ``` **Or** as JSON with `Content-Type: application/json`: ```json POST https://api.pushover.net/1/messages.json { "token": "", "user": "", "message": "Hello world", "title": "Charon Alert", "priority": 0, "sound": "pushover" } ``` Required parameters: `token`, `user`, `message` Optional parameters: `title`, `priority` (-2 to 2), `sound`, `device`, `url`, `url_title`, `html` (1 for HTML), `timestamp`, `ttl` **Key design decisions:** | Decision | Rationale | |----------|-----------| | **Token storage:** `NotificationProvider.Token` (`json:"-"`) stores the Pushover **Application API Token** | Mirrors Telegram/Slack/Gotify pattern — secrets are never in the URL field | | **URL field:** Stores the Pushover **User Key** (e.g., `uQiRzpo4DXghDmr9QzzfQu27cmVRsG`) | Follows Telegram pattern where URL stores the recipient identifier (chat_id → user key) | | **Dispatch uses JSON POST:** Despite Pushover supporting form-encoded, we send JSON with `Content-Type: application/json` | Aligns with existing `sendJSONPayload()` pipeline — reuses template engine, httpWrapper, validation | | **Fixed API endpoint:** `https://api.pushover.net/1/messages.json` constructed at dispatch time | Mirrors Telegram pattern (dynamic URL from token); prevents SSRF via stored data | | **SSRF mitigation:** Validate constructed URL hostname is `api.pushover.net` before dispatch | Same pattern as Telegram's `api.telegram.org` pin | | **No schema migration:** Existing `NotificationProvider` model accommodates Pushover | Token, URL, Config fields are sufficient | > **Important:** The user stated "Pushover is ALREADY part of the notification engine backend code" — however, research confirms Pushover is currently treated as **UNSUPPORTED** everywhere. It appears only in tests as an example of an unsupported/deprecated type. All dispatch code, type guards, feature flags, and UI must be built from scratch following the Telegram/Slack pattern. --- ## 2. Research Findings ### 2.1 Existing Architecture | Layer | File | Role | |-------|------|------| | Feature flags | `backend/internal/notifications/feature_flags.go` | Flag constants (`FlagXxxServiceEnabled`) | | Router | `backend/internal/notifications/router.go` | `ShouldUseNotify()` per-type dispatch | | Service | `backend/internal/services/notification_service.go` | Core dispatch: `isSupportedNotificationProviderType()`, `isDispatchEnabled()`, `supportsJSONTemplates()`, `sendJSONPayload()`, `TestProvider()` | | Handlers | `backend/internal/api/handlers/notification_provider_handler.go` | CRUD + type validation + token preservation | | Enhanced Security | `backend/internal/services/enhanced_security_notification_service.go` | Security event notifications with provider aggregation | | Model | `backend/internal/models/notification_provider.go` | GORM model with Token (`json:"-"`), HasToken | | Frontend API | `frontend/src/api/notifications.ts` | `SUPPORTED_NOTIFICATION_PROVIDER_TYPES`, sanitization | | Frontend UI | `frontend/src/pages/Notifications.tsx` | Provider form with conditional fields per type | | i18n | `frontend/src/locales/en/translation.json` | Label strings under `notificationProviders` | | E2E fixtures | `tests/fixtures/notifications.ts` | Provider configs and type union | ### 2.2 All Type-Check Locations Requiring `"pushover"` Addition | # | File | Function/Location | Current Types | |---|------|-------------------|---------------| | 1 | `feature_flags.go` | Constants | discord, email, gotify, webhook, telegram, slack | | 2 | `router.go` | `ShouldUseNotify()` switch | discord, email, gotify, webhook, telegram (missing slack! — **fix in same PR**) | | 3 | `notification_service.go` L137 | `isSupportedNotificationProviderType()` | discord, email, gotify, webhook, telegram, slack | | 4 | `notification_service.go` L146 | `isDispatchEnabled()` | discord, email, gotify, webhook, telegram, slack | | 5 | `notification_service.go` L128 | `supportsJSONTemplates()` | webhook, discord, gotify, slack, generic, telegram | | 6 | `notification_service.go` L470 | `sendJSONPayload()` — payload validation switch | discord, slack, gotify, telegram | | 7 | `notification_service.go` L512 | `sendJSONPayload()` — dispatch branch (`httpWrapper.Send`) | gotify, webhook, telegram, slack | | 8 | `notification_provider_handler.go` L186 | `Create()` type guard | discord, gotify, webhook, email, telegram, slack | | 9 | `notification_provider_handler.go` L246 | `Update()` type guard | discord, gotify, webhook, email, telegram, slack | | 10 | `notification_provider_handler.go` L250 | `Update()` token preservation | gotify, telegram, slack | | 11 | `notification_provider_handler.go` L312-316 | `Test()` token write-only guards | gotify, slack (**Note:** telegram is missing here — add in same PR) | | 12 | `enhanced_security_notification_service.go` L87 | `getProviderAggregatedConfig()` supportedTypes | webhook, discord, slack, gotify, telegram | | 13 | `notifications.ts` L3 | `SUPPORTED_NOTIFICATION_PROVIDER_TYPES` | discord, gotify, webhook, email, telegram, slack | | 14 | `notifications.ts` L62 | `sanitizeProviderForWriteAction()` token handling | gotify, telegram, slack | | 15 | `Notifications.tsx` L204 | Type ` ...existing stored indicator and hint... )} ``` #### URL Field Label and Placeholder (~L215-240) Update the URL label ternary chain to include Pushover: ```tsx {isEmail ? t('notificationProviders.recipients') : isTelegram ? t('notificationProviders.telegramChatId') : isSlack ? t('notificationProviders.slackChannelName') : isPushover ? t('notificationProviders.pushoverUserKey') : <>{t('notificationProviders.urlWebhook')} } ``` Update the placeholder: ```tsx placeholder={ isEmail ? 'user@example.com, admin@example.com' : isTelegram ? '987654321' : isSlack ? '#general' : isPushover ? 'uQiRzpo4DXghDmr9QzzfQu27cmVRsG' : type === 'discord' ? 'https://discord.com/api/webhooks/...' : type === 'gotify' ? 'https://gotify.example.com/message' : 'https://example.com/webhook' } ``` #### URL Validation Pushover User Key is not a URL, so skip URL format validation (like Telegram and Email): ```tsx {...register('url', { required: (isEmail || isSlack) ? false : (t('notificationProviders.urlRequired') as string), validate: (isEmail || isTelegram || isSlack || isPushover) ? undefined : validateUrl, })} ``` Note: Pushover User Key IS required (unlike Slack channel name), so it remains in the `required` logic. Only URL format validation is skipped. ### 3.9 Frontend — i18n Strings **File:** `frontend/src/locales/en/translation.json` Add to the `notificationProviders` section (after the Slack entries): ```json "pushover": "Pushover", "pushoverApiToken": "API Token (Application)", "pushoverApiTokenPlaceholder": "Enter your Pushover Application API Token", "pushoverUserKey": "User Key", "pushoverUserKeyPlaceholder": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG", "pushoverUserKeyHelp": "Your Pushover user or group key. The API token is stored securely and separately." ``` ### 3.10 API Contract (No Changes) The existing REST endpoints remain unchanged: | Method | Endpoint | Notes | |--------|----------|-------| | `GET` | `/api/v1/notifications/providers` | Returns all providers (token stripped) | | `POST` | `/api/v1/notifications/providers` | Create — now accepts `type: "pushover"` | | `PUT` | `/api/v1/notifications/providers/:id` | Update — token preserved if omitted | | `DELETE` | `/api/v1/notifications/providers/:id` | Delete — no type-specific logic | | `POST` | `/api/v1/notifications/providers/test` | Test — routes through `sendJSONPayload` | --- ## 4. Implementation Plan ### Phase 1: Playwright E2E Tests (Test-First) **Rationale:** Per project conventions — write feature behaviour tests first. #### New File: `tests/settings/pushover-notification-provider.spec.ts` Modeled after `tests/settings/telegram-notification-provider.spec.ts` and `tests/settings/slack-notification-provider.spec.ts`. **Test Sections:** ``` test.describe('Pushover Notification Provider') ├── test.describe('Form Rendering') │ ├── should show API token field and user key placeholder when pushover type selected │ ├── should toggle form fields when switching between pushover and discord types │ └── should show JSON template section for pushover ├── test.describe('CRUD Operations') │ ├── should create pushover notification provider │ │ └── Verify payload: type=pushover, url=, token=, gotify_token=undefined │ ├── should edit pushover notification provider and preserve token │ │ └── Verify update omits token when unchanged │ ├── should test pushover notification provider │ └── should delete pushover notification provider ├── test.describe('Security') │ ├── GET response should NOT expose API token │ └── API token should not leak in URL field └── test.describe('Payload Contract') └── POST payload should use correct field mapping ``` #### Update File: `tests/fixtures/notifications.ts` Add to `NotificationProviderType` union: ```typescript export type NotificationProviderType = | 'discord' | 'slack' | 'gotify' | 'telegram' | 'generic' | 'email' | 'webhook' | 'pushover'; ``` Add fixtures: ```typescript export const pushoverProvider: NotificationProviderConfig = { name: generateProviderName('pushover'), type: 'pushover', url: 'uQiRzpo4DXghDmr9QzzfQu27cmVRsG', // User Key token: 'azGDORePK8gMaC0QOYAMyEEuzJnyUi', // App API Token enabled: true, notify_proxy_hosts: true, notify_certs: true, notify_uptime: true, }; ``` #### Update File: `tests/settings/notifications.spec.ts` Provider type count assertions that currently expect 6 options need updating to 7. The existing tests at L144 and L191 that mock pushover as an existing provider should be updated: - **Replace Pushover mock data with a genuinely unsupported type** (e.g., `"pagerduty"`) for tests that assert "deprecated" badges. Using a real unsupported type removes ambiguity. - Any assertions about "deprecated" or "read-only" badges for pushover must be removed since it is now a supported type. ### Phase 2: Backend Implementation #### 2A — Feature Flags & Router (2 files) | File | Change | Complexity | |------|--------|------------| | `backend/internal/notifications/feature_flags.go` | Add `FlagPushoverServiceEnabled` constant | Trivial | | `backend/internal/notifications/router.go` | Add `case "pushover"` + `case "slack"` (missing) to `ShouldUseNotify()` | Trivial | #### 2B — Notification Service (1 file, 5 functions) | Function | Change | Complexity | |----------|--------|------------| | `isSupportedNotificationProviderType()` | Add `"pushover"` to case | Trivial | | `isDispatchEnabled()` | Add pushover case with feature flag | Low | | `supportsJSONTemplates()` | Add `"pushover"` to case | Trivial | | `sendJSONPayload()` — validation | Add `case "pushover"` requiring `message` field | Low | | `sendJSONPayload()` — dispatch | Add pushover dispatch block (inject token+user into body, SSRF pin) | Medium | #### 2C — Handler Layer (1 file, 4 locations) | Location | Change | Complexity | |----------|--------|------------| | `Create()` type guard | Add `"pushover"` | Trivial | | `Update()` type guard | Add `"pushover"` | Trivial | | `Update()` token preservation | Add `"pushover"` | Trivial | | `Test()` token write-only guard | Add pushover block | Low | #### 2D — Enhanced Security Service (1 file) | Location | Change | Complexity | |----------|--------|------------| | `getProviderAggregatedConfig()` supportedTypes | Add `"pushover": true` | Trivial | #### 2E — Backend Unit Tests (4-6 files) | File | Change | Complexity | |------|--------|------------| | `notification_service_test.go` | Replace `"pushover"` as unsupported with `"sms"`. Add pushover dispatch tests (success, missing token, missing user key, SSRF validation, payload injection). Add pushover to `supportsJSONTemplates` test. | Medium | | `notification_coverage_test.go` | Replace `Type: "pushover"` with `Type: "sms"` in Update_UnsupportedType test | Trivial | | `notification_provider_discord_only_test.go` | Replace `"type": "pushover"` with `"type": "sms"` | Trivial | | `security_notifications_final_blockers_test.go` | Replace `"pushover"` with `"sms"` in unsupportedTypes | Trivial | **New pushover-specific test cases to add in `notification_service_test.go`:** | Test Case | What It Validates | |-----------|-------------------| | `TestPushoverDispatch_Success` | Token + user injected into payload body, POST to `api.pushover.net`, returns nil | | `TestPushoverDispatch_MissingToken` | Returns error when Token is empty | | `TestPushoverDispatch_MissingUserKey` | Returns error when URL (user key) is empty | | `TestPushoverDispatch_SSRFValidation` | Constructed URL hostname pinned to `api.pushover.net` | | `TestPushoverDispatch_PayloadInjection` | `token` and `user` fields in body match DB values, not template-provided values | | `TestPushoverDispatch_MessageFieldRequired` | Payload without `message` field returns error | | `TestPushoverDispatch_EmergencyPriorityRejected` | Payload with `"priority": 2` returns error about unsupported emergency priority | | `TestPushoverDispatch_FeatureFlagDisabled` | Dispatch skipped when flag is false | ### Phase 3: Frontend Implementation #### 3A — API Client (1 file) | Location | Change | Complexity | |----------|--------|------------| | `SUPPORTED_NOTIFICATION_PROVIDER_TYPES` | Add `'pushover'` | Trivial | | `sanitizeProviderForWriteAction()` | Add `type !== 'pushover'` to token guard | Trivial | #### 3B — Notifications Page (1 file, ~9 locations) | Location | Change | Complexity | |----------|--------|------------| | Type `