Backend fix:
- Added presence check in UpdateEmailConfig to detect when rateLimit is
omitted from JSON (vs explicitly set to 0)
- Preserves existing rateLimit value when field is not present in request
- Added comprehensive integration tests covering all scenarios
Frontend fix:
- Added rateLimit to EmailConfig interface
- Fixed getEmailConfig to read rateLimit from server response
- Fixed updateEmailConfig to include rateLimit when set
- Fixed two places in Alerts.tsx that hardcoded rateLimit: 60
Additional fixes:
- Added Array.isArray guards in DiagnosticsPanel sanitization
- Initialized Nodes/PBS arrays in diagnostics response to prevent null
Closes rate limit persistence bug where updating email settings would
reset the rate limit to default value.
Implements Phase 1-2 of multi-tenancy support using a directory-per-tenant
strategy that preserves existing file-based persistence.
Key changes:
- Add MultiTenantPersistence manager for org-scoped config routing
- Add TenantMiddleware for X-Pulse-Org-ID header extraction and context propagation
- Add MultiTenantMonitor for per-tenant monitor lifecycle management
- Refactor handlers (ConfigHandlers, AlertHandlers, AIHandlers, etc.) to be
context-aware with getConfig(ctx)/getMonitor(ctx) helpers
- Add Organization model for future tenant metadata
- Update server and router to wire multi-tenant components
All handlers maintain backward compatibility via legacy field fallbacks
for single-tenant deployments using the "default" org.
When GetWebhooks returns webhooks, headers and customFields are masked
with ***REDACTED*** for security. However, when the frontend toggled
a webhook's enabled state, it sent back the redacted values, which
overwrote the actual header values (like Authorization tokens).
This broke webhooks after disabling and re-enabling them, as the auth
headers were replaced with "***REDACTED***".
Now UpdateWebhook detects redacted values and preserves the original
headers/customFields from the existing webhook.
Related to #938
The function was using substring matching for sensitive param names,
causing parameters like "extra_token" or "myapikey" to be incorrectly
redacted when they matched "token=" or "apikey=" as substrings.
Now checks for proper boundary characters (? or &) before matching,
so only actual parameter names are redacted.
Related to ADA knowledge entry: "Query param redaction uses substring matching"
The Telegram bot token redaction had an off-by-one bug: it searched for
the next "/" starting from the "/bot" position, which found the "/" in
"/bot" itself (offset 0) instead of the next "/" after the token.
Result: tokens were not properly redacted and the URL got corrupted with
duplicated path segments, potentially leaking secrets to logs/API responses.
Fix: search from idx+4 (after "/bot") and handle edge cases where there's
no trailing slash (token at end of URL or before query string).
Added 20 comprehensive test cases covering:
- No secrets (passthrough)
- Telegram bot tokens (various patterns)
- Query parameter secrets (token, apikey, api_key, key, secret, password)
- Multiple parameters and edge cases
- Change default server listen addresses to empty string (listen on all interfaces including IPv6)
- Add short hostname matching fallback in host lookup API to handle FQDN vs short name mismatches
- Implement retry loop (30s) in both Windows and Linux/macOS installers for registration verification
- Fix lint errors: remove unnecessary fmt.Sprintf and nil checks before len()
This resolves the 'Installer could not yet confirm host registration with Pulse' warning
by addressing timing issues, hostname matching, and network connectivity.
Allow homelab users to send webhooks to internal services while maintaining security defaults.
Changes:
- Add webhookAllowedPrivateCIDRs field to SystemSettings (persistent config)
- Implement CIDR parsing and validation in NotificationManager
- Convert ValidateWebhookURL to instance method to access allowlist
- Add UI controls in System Settings for configuring trusted CIDR ranges
- Maintain strict security by default (block all private IPs)
- Keep localhost, link-local, and cloud metadata services blocked regardless of allowlist
- Re-validate on both config save and webhook delivery (DNS rebinding protection)
- Add comprehensive tests for CIDR parsing and IP matching
Backend:
- UpdateAllowedPrivateCIDRs() parses comma-separated CIDRs with validation
- Support for bare IPs (auto-converts to /32 or /128)
- Thread-safe allowlist updates with RWMutex
- Logging when allowlist is updated or used
- Validation errors prevent invalid CIDRs from being saved
Frontend:
- New "Webhook Security" section in System Settings
- Input field with examples and helpful placeholder text
- Real-time unsaved changes tracking
- Loads and saves allowlist via system settings API
Security:
- Default behavior unchanged (all private IPs blocked)
- Explicit opt-in required via configuration
- Localhost (127/8) always blocked
- Link-local (169.254/16) always blocked
- Cloud metadata services always blocked
- DNS resolution checked at both save and send time
Testing:
- Tests for CIDR parsing (valid/invalid inputs)
- Tests for IP allowlist matching
- Tests for bare IP address handling
- Tests for security boundaries (localhost, link-local remain blocked)
Related to #673🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Backend:
- Add IsEncryptionEnabled() method to ConfigPersistence
- Include encryption status in /api/notifications/health response
- Allows frontend to warn when credentials are stored in plaintext
Frontend:
- Update NotificationHealth type to include encryption.enabled field
- Frontend can now display warnings when encryption is disabled
This addresses the P2 requirement for encryption visibility, allowing
operators to know when notification credentials are not encrypted at rest.