Files
Charon/docs/plans/telegram_implementation_spec.md
akanealw eec8c28fb3
Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
changed perms
2026-04-22 18:19:14 +00:00

29 KiB
Executable File

Telegram Notification Provider — Implementation Plan

Date: 2026-07-10 Author: Planning Agent Confidence Score: 92% (High — existing patterns well-established, Telegram Bot API straightforward)


1. Introduction

Objective

Add Telegram as a first-class notification provider in Charon, following the established architecture used by Discord, Gotify, Email, and generic Webhook providers.

Goals

  • Users can configure a Telegram bot token and chat ID to receive notifications via Telegram
  • All existing notification event types (proxy hosts, certs, uptime, security events) work with Telegram
  • JSON template engine (minimal/detailed/custom) works with Telegram
  • Feature flag allows enabling/disabling Telegram dispatch independently
  • 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

Telegram Bot API Overview

Telegram bots send messages via:

POST https://api.telegram.org/bot<BOT_TOKEN>/sendMessage
Content-Type: application/json

{
  "chat_id": "<CHAT_ID>",
  "text": "Hello world",
  "parse_mode": "HTML"    // optional: "HTML" or "MarkdownV2"
}

Key design decisions:

  • Token storage: The bot token is stored in NotificationProvider.Token (json:"-", encrypted at rest) — never in the URL field. This mirrors the Gotify pattern where secrets are separated from endpoints.
  • URL field: Stores only the chat_id (e.g., 987654321). At dispatch time, the full API URL is constructed dynamically: https://api.telegram.org/bot + decryptedToken + /sendMessage. The chat_id is passed in the POST body alongside the message text. This prevents token leakage via API responses since URL is json:"url".
  • SSRF mitigation: Before dispatching, validate that the constructed URL hostname is exactly api.telegram.org. This prevents SSRF if stored data is tampered with.
  • Dispatch path: Uses sendJSONPayloadhttpWrapper.Send() (same as Gotify), since both are token-based JSON POST providers
  • No schema migration needed: The existing NotificationProvider model accommodates Telegram without changes

Supervisor Review Note: The original design embedded the bot token in the URL field (https://api.telegram.org/bot<TOKEN>/sendMessage?chat_id=<CHAT_ID>). This was rejected because the URL field is json:"url" — exposed in every API response. The token MUST only reside in the Token field (json:"-").


2. Research Findings

Existing Architecture

The notification system follows a provider-based architecture:

Layer File Role
Feature flags backend/internal/notifications/feature_flags.go Flag constants (FlagXxxServiceEnabled)
Feature flag handler backend/internal/api/handlers/feature_flags_handler.go DB-backed flags with defaults
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
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
E2E fixtures tests/fixtures/notifications.ts telegramProvider already defined

Existing Provider Addition Points (Switch Statements / Type Checks)

Every location that checks provider types is listed below — all require a "telegram" case:

# File Function/Line Current Logic
1 feature_flags.go Constants Missing FlagTelegramServiceEnabled
2 feature_flags_handler.go defaultFlags + defaultFlagValues Missing telegram entry
3 router.go ShouldUseNotify() switch Missing case "telegram"
4 notification_service.go isSupportedNotificationProviderType() case "discord", "email", "gotify", "webhook"
5 notification_service.go isDispatchEnabled() switch with per-type flag checks
6 notification_service.go supportsJSONTemplates() case "webhook", "discord", "gotify", "slack", "generic"
7 notification_service.go sendJSONPayload() — service-specific validation Missing case "telegram" for payload validation
8 notification_service.go sendJSONPayload() — dispatch branch Gotify/webhook use httpWrapper.Send(); others use ValidateExternalURL + SafeHTTPClient
9 notification_provider_handler.go Create() type guard providerType != "discord" && providerType != "gotify" && providerType != "webhook" && providerType != "email"
10 notification_provider_handler.go Update() type guard Same pattern as Create
11 notification_provider_handler.go Update() token preservation if providerType == "gotify" && strings.TrimSpace(req.Token) == ""
12 notifications.ts SUPPORTED_NOTIFICATION_PROVIDER_TYPES ['discord', 'gotify', 'webhook', 'email']
13 notifications.ts sanitizeProviderForWriteAction() Token handling only for type !== 'gotify'
14 Notifications.tsx Type <select> options discord/gotify/webhook/email
15 Notifications.tsx normalizeProviderPayloadForSubmit() Token mapping only for type === 'gotify'
16 Notifications.tsx Conditional form fields isGotify shows token input

Test Files Requiring Updates

File Current Behavior Required Change
notification_service_test.go (~L1819) TestTestProvider_NotifyOnlyRejectsUnsupportedProvider tests "telegram" as unsupported Change: telegram is now supported
notification_service_json_test.go Discord/Slack/Gotify/Webhook JSON tests Add telegram payload validation tests
notification_provider_handler_test.go CRUD tests with supported types Add telegram to supported type lists
enhanced_security_notification_service_test.go (~L139) Type: "telegram" marked // Should be filtered Change: telegram is now valid
frontend/src/api/notifications.test.ts Rejects "telegram" as unsupported Accept telegram, add CRUD tests
frontend/src/api/__tests__/notifications.test.ts Same rejection Same fix
tests/settings/notifications.spec.ts CRUD E2E for discord/gotify/webhook/email Add telegram scenarios

E2E Fixture Already Defined

// tests/fixtures/notifications.ts
// NOTE: Fixture must be updated — URL should contain only the chat_id, token goes in the token field
export const telegramProvider: NotificationProviderConfig = {
  name: generateProviderName('telegram'),
  type: 'telegram',
  url: '987654321',  // chat_id only — bot token is stored in the Token field
  token: 'bot123456789:ABCdefGHIjklMNOpqrSTUvwxYZ',  // stored encrypted, never in API responses
  enabled: true,
  notify_proxy_hosts: true,
  notify_certs: true,
  notify_uptime: true,
};

3. Technical Specifications

3.1 Backend — Feature Flags

File: backend/internal/notifications/feature_flags.go

Add constant:

FlagTelegramServiceEnabled = "feature.notifications.service.telegram.enabled"

File: backend/internal/api/handlers/feature_flags_handler.go

Add to defaultFlags slice:

notifications.FlagTelegramServiceEnabled,

Add to defaultFlagValues map:

notifications.FlagTelegramServiceEnabled: true,

Note: Telegram is enabled by default once the provider is toggled on in the UI, matching Gotify/Webhook behavior. The feature flag exists as an admin-level kill switch, not a setup gate.

3.2 Backend — Router

File: backend/internal/notifications/router.go

Add to ShouldUseNotify() switch:

case "telegram":
    return flags[FlagTelegramServiceEnabled]

3.3 Backend — Notification Service

File: backend/internal/services/notification_service.go

isSupportedNotificationProviderType()

case "discord", "email", "gotify", "webhook", "telegram":
    return true

isDispatchEnabled()

case "telegram":
    return s.getFeatureFlagValue(notifications.FlagTelegramServiceEnabled, true)

Both defaultFlagValues (initial DB seed) and the isDispatchEnabled() fallback are true — Telegram is enabled by default once the provider is created in the UI. This matches Gotify/Webhook behavior (enabled-by-default, admin kill-switch via feature flag).

supportsJSONTemplates()

case "webhook", "discord", "gotify", "slack", "generic", "telegram":
    return true

sendJSONPayload() — Service-Specific Validation

Add after the case "gotify": block:

case "telegram":
    // Telegram requires 'text' field for the message body
    if _, hasText := jsonPayload["text"]; !hasText {
        // Auto-map 'message' to 'text' if present (template compatibility)
        if messageValue, hasMessage := jsonPayload["message"]; hasMessage {
            jsonPayload["text"] = messageValue
            normalizedBody, marshalErr := json.Marshal(jsonPayload)
            if marshalErr != nil {
                return fmt.Errorf("failed to normalize telegram payload: %w", marshalErr)
            }
            body.Reset()
            if _, writeErr := body.Write(normalizedBody); writeErr != nil {
                return fmt.Errorf("failed to write normalized telegram payload: %w", writeErr)
            }
        } else {
            return fmt.Errorf("telegram payload requires 'text' field")
        }
    }

This auto-mapping mirrors the Discord pattern (messagecontent) so that the built-in minimal and detailed templates (which use "message" as a field) work out of the box with Telegram.

sendJSONPayload() — Dispatch Branch

Add "telegram" to the httpWrapper.Send() dispatch branch alongside gotify/webhook:

if providerType == "gotify" || providerType == "webhook" || providerType == "telegram" {

For telegram, the dispatch URL must be constructed at send time from the stored token and chat_id:

case "telegram":
    // Construct the API URL dynamically — token is NEVER stored in the URL field
    decryptedToken := provider.Token // already decrypted by service layer
    dispatchURL = "https://api.telegram.org/bot" + decryptedToken + "/sendMessage"

    // SSRF mitigation: validate hostname before dispatch
    parsedURL, err := url.Parse(dispatchURL)
    if err != nil || parsedURL.Hostname() != "api.telegram.org" {
        return fmt.Errorf("telegram dispatch URL validation failed: invalid hostname")
    }

    // Inject chat_id into the JSON payload body (URL field stores the chat_id)
    jsonPayload["chat_id"] = provider.URL
    // Re-marshal the payload with chat_id included
    updatedBody, marshalErr := json.Marshal(jsonPayload)
    if marshalErr != nil {
        return fmt.Errorf("failed to marshal telegram payload with chat_id: %w", marshalErr)
    }
    body.Reset()
    body.Write(updatedBody)

The X-Gotify-Key header is only set when providerType == "gotify" — no header changes needed for telegram.

TestProvider() — Telegram-Specific Error Message

When testing a Telegram provider and the API returns HTTP 401 or 403, return a specific error message:

case "telegram":
    if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
        return fmt.Errorf("provider rejected authentication. Verify your Telegram Bot Token")
    }

This gives users actionable guidance instead of a generic HTTP status error.

3.4 Backend — Handler Layer

File: backend/internal/api/handlers/notification_provider_handler.go

Create() Type Guard

if providerType != "discord" && providerType != "gotify" && providerType != "webhook" && providerType != "email" && providerType != "telegram" {

Update() Type Guard

Same change as Create:

if providerType != "discord" && providerType != "gotify" && providerType != "webhook" && providerType != "email" && providerType != "telegram" {

Update() Token Preservation

Telegram bot tokens should be preserved on update when the user omits them (same UX as Gotify):

if (providerType == "gotify" || providerType == "telegram") && strings.TrimSpace(req.Token) == "" {
    req.Token = existing.Token
}

3.5 Backend — Model (No Changes)

The NotificationProvider model already has:

  • Token string with json:"-" (write-only, never exposed)
  • HasToken bool with gorm:"-" (computed field for frontend)
  • URL string for the endpoint
  • ServiceConfig string for extra JSON config (available for parse_mode if needed)

No schema migration is required.

3.6 Frontend — API Client

File: frontend/src/api/notifications.ts

Supported Types Array

export const SUPPORTED_NOTIFICATION_PROVIDER_TYPES = ['discord', 'gotify', 'webhook', 'email', 'telegram'] as const;

sanitizeProviderForWriteAction()

Minimal diff only. Change only the type guard condition from:

if (type !== 'gotify') {

to:

if (type !== 'gotify' && type !== 'telegram') {

The surrounding normalization logic (token stripping, payload return) MUST remain untouched. No other lines in this function change.

sanitizeProviderForReadLikeAction()

No changes — already calls sanitizeProviderForWriteAction() then strips token.

3.7 Frontend — Notifications Page

File: frontend/src/pages/Notifications.tsx

Type Select Options

Add after the email option:

<option value="telegram">Telegram</option>

Computed Flags

const isTelegram = type === 'telegram';

normalizeProviderPayloadForSubmit()

Add telegram branch alongside gotify:

if (type === 'gotify' || type === 'telegram') {
    const normalizedToken = typeof payload.gotify_token === 'string' ? payload.gotify_token.trim() : '';
    if (normalizedToken.length > 0) {
        payload.token = normalizedToken;
    } else {
        delete payload.token;
    }
} else {
    delete payload.token;
}

Note: Reuses the gotify_token form field for both Gotify and Telegram since both need a token input. This minimizes UI changes. The field label changes based on provider type.

Token Input Field

Expand the conditional from {isGotify && ( to {(isGotify || isTelegram) && (:

{(isGotify || isTelegram) && (
    <div>
        <label htmlFor="provider-gotify-token" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
            {isTelegram ? t('notificationProviders.telegramBotToken') : t('notificationProviders.gotifyToken')}
        </label>
        <input
            id="provider-gotify-token"
            type="password"
            autoComplete="new-password"
            {...register('gotify_token')}
            data-testid="provider-gotify-token"
            placeholder={initialData?.has_token
                ? t('notificationProviders.gotifyTokenKeepPlaceholder')
                : isTelegram
                    ? t('notificationProviders.telegramBotTokenPlaceholder')
                    : t('notificationProviders.gotifyTokenPlaceholder')}
            className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
            aria-describedby={initialData?.has_token ? 'gotify-token-stored-hint' : undefined}
        />
        {initialData?.has_token && (
            <p id="gotify-token-stored-hint" data-testid="gotify-token-stored-indicator" className="text-xs text-green-600 dark:text-green-400 mt-1">
                {t('notificationProviders.gotifyTokenStored')}
            </p>
        )}
        <p className="text-xs text-gray-500 mt-1">{t('notificationProviders.gotifyTokenWriteOnlyHint')}</p>
    </div>
)}

URL Field Placeholder

For Telegram, the URL field stores the chat_id (not a full URL). Update the placeholder and label accordingly:

placeholder={
    isEmail ? 'user@example.com, admin@example.com'
    : type === 'discord' ? 'https://discord.com/api/webhooks/...'
    : type === 'gotify' ? 'https://gotify.example.com/message'
    : isTelegram ? '987654321'
    : 'https://example.com/webhook'
}

Update the label for the URL field when type is telegram:

label={isTelegram ? t('notificationProviders.telegramChatId') : t('notificationProviders.url')}

Clear Token on Type Change

Update the existing useEffect that clears gotify_token:

useEffect(() => {
    if (type !== 'gotify' && type !== 'telegram') {
        setValue('gotify_token', '', { shouldDirty: false, shouldTouch: false });
    }
}, [type, setValue]);

3.8 Frontend — i18n Strings

File: frontend/src/locales/en/translation.json

Add to the notificationProviders section:

"telegram": "Telegram",
"telegramBotToken": "Bot Token",
"telegramBotTokenPlaceholder": "Enter your Telegram Bot Token",
"telegramChatId": "Chat ID",
"telegramChatIdPlaceholder": "987654321",
"telegramChatIdHelp": "Your Telegram chat, group, or channel ID. The bot token is stored securely and separately."

3.9 API Contract (No Changes)

The existing REST endpoints remain unchanged:

Method Endpoint Notes
GET /api/notification-providers Returns all providers (token stripped)
POST /api/notification-providers Create — now accepts type: "telegram"
PUT /api/notification-providers/:id Update — token preserved if omitted
DELETE /api/notification-providers/:id Delete — no type-specific logic
POST /api/notification-providers/test Test — routes through sendJSONPayload

Request/response schemas are unchanged. The type field now accepts "telegram" in addition to existing values.


4. Implementation Plan

Phase 1: Playwright E2E Tests (Test-First)

Rationale: Per project conventions — write feature behaviour tests first.

New file: tests/settings/telegram-notification-provider.spec.ts

Modeled after tests/settings/email-notification-provider.spec.ts.

Test scenarios:

  1. Create a Telegram provider (name, chat_id in URL field, bot token in token field, enable events)
  2. Verify provider appears in the list
  3. Edit the Telegram provider (change name, verify token preservation)
  4. Test the Telegram provider (mock API returns 200)
  5. Delete the Telegram provider
  6. Negative security test: Verify GET /api/notification-providers does NOT expose the bot token in any response field
  7. Negative security test: Verify bot token is NOT present in the URL field of the API response

Update file: tests/settings/notifications-payload.spec.ts

Add telegram to the payload matrix test scenarios.

E2E fixtures: Update telegramProvider in tests/fixtures/notifications.ts — URL must contain only chat_id, token goes in the token field (see Research Findings section for updated fixture).

Phase 2: Backend Implementation

2A — Feature Flags (3 files)

File Change
backend/internal/notifications/feature_flags.go Add FlagTelegramServiceEnabled constant
backend/internal/api/handlers/feature_flags_handler.go Add to defaultFlags + defaultFlagValues
backend/internal/notifications/router.go Add case "telegram" to ShouldUseNotify()

2B — Service Layer (1 file, 4 function changes)

File Function Change
notification_service.go isSupportedNotificationProviderType() Add "telegram" to case
notification_service.go isDispatchEnabled() Add case "telegram" with flag check
notification_service.go supportsJSONTemplates() Add "telegram" to case
notification_service.go sendJSONPayload() Add telegram validation + dispatch branch

2C — Handler Layer (1 file, 3 locations)

File Location Change
notification_provider_handler.go Create() type guard Add && providerType != "telegram"
notification_provider_handler.go Update() type guard Same
notification_provider_handler.go Update() token preservation Add `

Phase 3: Frontend Implementation

3A — API Client (1 file)

File Change
frontend/src/api/notifications.ts Add 'telegram' to SUPPORTED_NOTIFICATION_PROVIDER_TYPES, update token sanitization logic

3B — Notifications Page (1 file)

File Change
frontend/src/pages/Notifications.tsx Add telegram to type select, token field conditional, URL placeholder, normalizeProviderPayloadForSubmit(), type-change useEffect

3C — Localization (1 file)

File Change
frontend/src/locales/en/translation.json Add telegram-specific label strings

Phase 4: Backend Tests

Test File Changes
notification_service_test.go Update "rejects unsupported provider" test (remove telegram from unsupported list). Add telegram dispatch/integration tests.
notification_service_json_test.go Add TestSendJSONPayload_Telegram_* tests: valid payload, missing text with message auto-map, missing both text and message, dispatch via httpWrapper, SSRF hostname validation, 401/403 error message
notification_provider_handler_test.go Add telegram to Create/Update happy path tests, token preservation test. Add negative test: verify GET response does not contain bot token in URL field or response body
enhanced_security_notification_service_test.go Change telegram from "filtered" to "valid provider" in security dispatch tests
Router test (if exists) Add telegram to ShouldUseNotify() tests

Phase 5: Frontend Tests

Test File Changes
frontend/src/api/notifications.test.ts Remove telegram rejection test, add telegram CRUD sanitization tests
frontend/src/api/__tests__/notifications.test.ts Same changes (duplicate test location)
frontend/src/pages/Notifications.test.tsx Add telegram form rendering tests (token field visibility, placeholder text)

Phase 6: Integration, Documentation & Deployment

  • Verify E2E tests pass with Docker container
  • Update docs/features.md with Telegram provider mention
  • No ARCHITECTURE.md changes needed (same provider pattern)
  • No database migration needed

5. Acceptance Criteria

EARS Requirements

ID Requirement
T-01 WHEN a user creates a notification provider with type "telegram", THE SYSTEM SHALL accept the provider and store it in the database
T-02 WHEN a user provides a bot token for a Telegram provider, THE SYSTEM SHALL store it securely and never expose it in API responses
T-03 WHEN a Telegram provider is enabled and a notification event fires, THE SYSTEM SHALL construct the Telegram API URL dynamically from the stored token (https://api.telegram.org/bot + token + /sendMessage), inject chat_id from the URL field into the POST body, and send the rendered template payload
T-04 WHEN the rendered JSON payload contains a "message" field but not a "text" field, THE SYSTEM SHALL auto-map "message" to "text" for Telegram compatibility
T-05 WHEN the Telegram feature flag is disabled, THE SYSTEM SHALL skip dispatch for all Telegram providers
T-06 WHEN a user updates a Telegram provider without providing a token, THE SYSTEM SHALL preserve the existing stored token
T-07 WHEN a user tests a Telegram provider, THE SYSTEM SHALL send a test notification through the standard sendJSONPayload path
T-08 WHEN the frontend renders the provider form with type "telegram", THE SYSTEM SHALL display a bot token input field and a chat_id input field (with appropriate placeholder)
T-09 WHEN dispatching a Telegram notification, THE SYSTEM SHALL validate that the constructed URL hostname is exactly api.telegram.org before sending (SSRF mitigation)
T-10 WHEN a Telegram test request receives HTTP 401 or 403, THE SYSTEM SHALL return the error message "Provider rejected authentication. Verify your Telegram Bot Token"
T-11 WHEN the API returns notification providers via GET, THE SYSTEM SHALL NOT include the bot token in the URL field or any other exposed response field

Definition of Done

  • All 16 code touchpoints updated (see section 2 table)
  • E2E Playwright tests pass for Telegram CRUD + test send
  • Backend unit tests cover: type registration, dispatch routing, payload validation (text field), token preservation, feature flag gating
  • Frontend unit tests cover: type array acceptance, sanitization, form rendering
  • go test ./... passes
  • npm test passes
  • npx playwright test --project=firefox passes
  • make lint-fast passes (staticcheck)
  • Coverage threshold maintained (85%+)
  • GORM security scan passes (no model changes, but verify)
  • Token never appears in API responses, logs, or frontend state
  • Negative security tests pass (bot token not in GET response body or URL field)
  • SSRF hostname validation test passes (only api.telegram.org allowed)
  • Telegram 401/403 returns specific auth error message

6. Commit Slicing Strategy

Decision: 2 PRs

Trigger reasons: Changes span backend + frontend + E2E tests with independent functionality per layer. Splitting improves review quality and rollback safety.

PR-1: Backend — Telegram Provider Support

Scope: Feature flags, service layer, handler layer, all Go unit tests

Files changed:

  • backend/internal/notifications/feature_flags.go
  • backend/internal/api/handlers/feature_flags_handler.go
  • backend/internal/notifications/router.go
  • backend/internal/services/notification_service.go
  • backend/internal/api/handlers/notification_provider_handler.go
  • backend/internal/services/notification_service_test.go
  • backend/internal/services/notification_service_json_test.go
  • backend/internal/api/handlers/notification_provider_handler_test.go
  • backend/internal/services/enhanced_security_notification_service_test.go

Dependencies: None (self-contained backend change)

Validation gates:

  • go test ./... passes
  • make lint-fast passes
  • Coverage ≥ 85%
  • GORM security scan passes

Rollback: Revert PR — no DB migration to undo.

PR-2: Frontend + E2E — Telegram Provider UI

Scope: Frontend API client, Notifications page, i18n strings, frontend unit tests, Playwright E2E tests

Files changed:

  • frontend/src/api/notifications.ts
  • frontend/src/pages/Notifications.tsx
  • frontend/src/locales/en/translation.json
  • frontend/src/api/notifications.test.ts
  • frontend/src/api/__tests__/notifications.test.ts
  • frontend/src/pages/Notifications.test.tsx
  • tests/settings/telegram-notification-provider.spec.ts (new)
  • tests/settings/notifications-payload.spec.ts

Dependencies: PR-1 must be merged first (backend must accept type: "telegram")

Validation gates:

  • npm test passes
  • npm run type-check passes
  • npx playwright test --project=firefox passes
  • Coverage ≥ 85%

Rollback: Revert PR — frontend-only, no cascading effects.


7. Risk Assessment

Risk Likelihood Impact Mitigation
Telegram API rate limiting Low Medium Use existing retry/timeout patterns from httpWrapper
Bot token exposure in responses/logs Low Critical Token stored ONLY in Token field (json:"-"), never in URL field. URL field contains only chat_id. Negative security tests verify this invariant.
Template auto-mapping edge cases Low Low Test with all three template types (minimal, detailed, custom)
URL validation rejects chat_id format Low Low URL field now stores a chat_id string (not a full URL). Validation may need adjustment to accept non-URL values for telegram type.
SSRF via tampered stored data Low High Dispatch-time validation ensures hostname is exactly api.telegram.org. Dedicated test covers this.
E2E test flakiness with mocked API Low Low Existing route-mocking patterns are stable

8. Complexity Estimates

Component Estimate Notes
Backend feature flags S 3 files, ~5 lines each
Backend service layer M 4 function changes + telegram validation block
Backend handler layer S 3 string-level changes
Frontend API client S 2 lines + sanitization tweak
Frontend UI M Template conditional, placeholder, useEffect updates
Frontend i18n S 4 strings
Backend tests L Multiple test files, new test functions, update existing assertions
Frontend tests M Update rejection tests, add rendering tests
E2E tests M New spec file modeled on existing email spec
Total M-L ~2-3 days of focused implementation