- Added validation to reject non-discord provider types in create, update, test, and preview operations. - Updated the notifications form to automatically normalize non-discord types to discord. - Modified UI to display explicit messaging for deprecated and non-dispatch statuses for non-discord providers. - Enhanced tests to cover new validation logic and UI changes for provider types.
27 KiB
post_title, categories, tags, summary, post_date
| post_title | categories | tags | summary | post_date | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Current Spec: Discord Notify-Only Dispatch (No Legacy Fallback) |
|
|
Authoritative implementation plan for Discord Notify-only dispatch with explicit retirement of legacy fallback ambiguity, deterministic migration behavior, and proof-oriented testing. | 2026-02-21 |
Active Plan: Discord Notify-Only Dispatch (No Legacy Fallback)
Date: 2026-02-21 Status: Active and authoritative Scope Type: Product implementation plan (frontend + backend + migration + verification)
1) Introduction
This plan defines the implementation to satisfy the updated requirement direction:
- Do not rely on legacy fallback engine for Discord dispatch.
- Discord payloads saved in DB must be sent by Notify engine directly.
- Remove ambiguity where delivery success could still come through legacy path.
- Keep rollout scope focused on Discord-only cutover without unrelated refactors.
This plan is intentionally scoped to Discord-only dispatch/runtime behavior, fallback retirement, and auditable proof that no hidden legacy path remains.
2) Research Findings
2.1 Frontend provider-type control points (exact files)
frontend/src/pages/Notifications.tsx- Provider type is normalized to Discord (
DISCORD_PROVIDER_TYPE) before submit/test/preview. - Provider dropdown currently renders Discord-only option, and request payloads are forced to Discord.
- Provider type is normalized to Discord (
frontend/src/api/notifications.tswithDiscordType(...)normalizes outbound payload type to Discord.- Interface remains
type: string, requiring backend to remain authoritative for no-fallback invariants.
frontend/src/pages/__tests__/Notifications.test.tsx- Must verify Discord-only create/edit/test behavior and avoid implying fallback rescue semantics.
tests/settings/notifications.spec.ts- Includes coverage for Discord behavior and deprecated non-Discord rendering semantics.
2.2 Backend/provider type validation/default/control points (exact files)
backend/internal/api/handlers/notification_provider_handler.go- Enforces Discord-only on create/update and blocks enabling deprecated non-Discord providers.
backend/internal/services/notification_service.go- Enforces Discord-only for provider lifecycle/test and skips non-Discord dispatch.
- Contains explicit legacy-fallback-disabled sentinel/error path that must stay fail-closed.
- Includes
EnsureNotifyOnlyProviderMigration(...)for deterministic cutover state.
backend/internal/services/enhanced_security_notification_service.goSendViaProviders(...)dispatches only to Discord providers in current rollout stage.dispatchToProvider(...)still has non-Discord branches and is a key ambiguity-removal target.
backend/internal/models/notification_provider.goTypecomment still lists non-Discord types.Engineand URL comments still include retired legacy-notifier URL semantics.
backend/internal/notifications/engine.go- Defines a retired legacy notification engine constant.
backend/internal/notifications/router.go- Legacy engine branch remains.
backend/internal/notifications/feature_flags.go- Retired fallback flag key remains in feature-flag wiring.
backend/internal/api/handlers/feature_flags_handler.go- Still exposes a retired legacy fallback key in defaults and update path guards.
backend/internal/api/routes/routes.go- Notification services are wired via both
NotificationServiceandEnhancedSecurityNotificationService; migration path still references transitional behavior.
- Notification services are wired via both
2.3 Exact backend/frontend files impacted by fallback removal
Backend files:
backend/internal/notifications/engine.gobackend/internal/notifications/router.gobackend/internal/notifications/feature_flags.gobackend/internal/api/handlers/feature_flags_handler.gobackend/internal/services/notification_service.gobackend/internal/services/enhanced_security_notification_service.gobackend/internal/api/handlers/notification_provider_handler.gobackend/internal/api/routes/routes.gobackend/internal/models/notification_provider.go
Frontend files:
frontend/src/pages/Notifications.tsxfrontend/src/api/notifications.ts
2.4 Dependency/runtime current state (legacy notifier)
backend/go.mod: no active legacy notifier dependency.backend/go.sum: no active legacy notifier entry found.- Direct import/call in backend source currently not present.
- Legacy strings/constants/tests remain in runtime code paths and tests.
- Historical artifacts and cached module content may contain legacy notifier references but are not production dependencies.
2.5 Firefox failing specs context
- Artifact
playwright-output/manual-dns-targeted/.last-run.jsoncontains 16 failed test IDs only; suite attribution must come from Playwright report/traces, not this file alone. - Separate summary
FIREFOX_E2E_FIXES_SUMMARY.mdreferences prior Firefox remediation work. - User-reported “5 failing Firefox specs” is treated as active triage input and must be classified at execution time into in-scope vs out-of-scope suites (defined below).
3) Requirements (EARS)
- R1: WHEN opening notification provider create/edit UI, THE SYSTEM SHALL offer only
discordin provider type selection for this rollout. - R2: WHEN create/update/test notification provider API endpoints receive a non-Discord type, THE SYSTEM SHALL reject with deterministic validation error.
- R3: WHILE legacy non-Discord providers exist in DB, THE SYSTEM SHALL handle them per explicit compatibility policy without enabling dispatch.
- R4: WHEN verifying runtime and dependency state, THE SYSTEM SHALL produce auditable evidence that the retired legacy notifier is not installed, linked, or used by active runtime code paths.
- R5: IF additional services are introduced in future, THEN THE SYSTEM SHALL enable them behind explicit validation gates and staged rollout controls.
- R6: WHEN Discord notifications are delivered successfully, THE SYSTEM SHALL provide evidence that Notify path handled dispatch and not a legacy fallback path.
- R7: IF rollback is required, THEN THE SYSTEM SHALL use explicit rollback controls without reintroducing hidden legacy fallback behavior.
4) Technical Specification
4.1 Discord-only enforcement strategy (defense in depth)
Layer A: UI/input restriction
frontend/src/pages/Notifications.tsx- Replace provider type dropdown options with a single option:
discord. - Keep visible label semantics intact for accessibility.
- Prevent stale-form values from preserving non-Discord types in edit mode.
- Replace provider type dropdown options with a single option:
Layer B: Frontend request contract hardening
frontend/src/api/notifications.ts- Frontend request contract SHALL submit
typeasdiscordonly; client must reject or normalize any stale non-discord value before submit.
- Frontend request contract SHALL submit
Layer C: API handler validation (authoritative gate)
backend/internal/api/handlers/notification_provider_handler.go- Enforce
req.Type == "discord"for all create/update paths (not only security-event cases). - Return stable error code/message for non-Discord type attempts.
- Enforce
Layer D: Service-layer invariant
backend/internal/services/notification_service.go- Add service-level type guard for create/update/test/send paths to reject non-Discord.
- Keep this guard even if handler validation exists (defense in depth).
Layer E: Dispatch/runtime invariant
backend/internal/services/enhanced_security_notification_service.go- Maintain dispatch as Discord-only.
- Remove non-Discord dispatch branches from active runtime logic for this phase.
4.2 Feature-flag policy change for legacy fallback
Policy for feature.notifications.legacy.fallback_enabled:
- Deprecate now.
- Force false in reads and writes.
- Immutable false: never true at runtime.
- Reject
trueupdate attempts with deterministic API error. - Keep compatibility aliases read-only and forced false during transition.
- Remove
feature.notifications.legacy.fallback_enabledfrom all public/default flag responses in PR-3 of this plan, and remove all retired env aliases in the same PR. No compatibility-window extension is allowed without a new approved spec revision.
Migration implications:
- Existing settings row for legacy fallback is normalized to false if present.
- Transition period allows key visibility as false-only for compatibility.
- Final state keeps this key out of runtime dispatch decisions; there is no fallback runtime path.
Legacy fallback control surface policy: any API write attempting to set legacy fallback true SHALL return 400 with code LEGACY_FALLBACK_REMOVED; any legacy fallback read endpoint/field retained temporarily SHALL return hard-false only and SHALL be removed in PR-3. No hidden route or config path may alter dispatch away from Notify, and Notify failures SHALL NOT reroute to a fallback engine.
4.3 Runtime dispatch contract (Discord-only Notify path)
Contract:
- Discord dispatch source of truth is Notify engine path (
notify_v1) only. NotificationService.SendExternal(...)andEnhancedSecurityNotificationService.SendViaProviders(...)must fail closed for non-Discord delivery attempts.- No implicit retry/reroute to legacy engine on Notify failure.
- Any successful Discord delivery during rollout must be attributable to Notify path execution.
Layer F: Optional DB invariant (recommended)
- Add migration with DB-level check constraint for notification provider type to
discordfor newly created/updated rows in this rollout.- If DB constraint is deferred, service/API guards remain mandatory.
4.4 Data migration and compatibility policy for existing non-Discord providers
Policy decision for this rollout: Deprecate + read-only visibility + non-dispatch.
- Existing non-Discord rows are preserved for audit/history but cannot be newly created or converted back to active dispatch in this phase.
- Compatibility behavior:
enabledforced false for non-Discord providers during migration reconciliation.migration_stateremains failed/deprecated for non-supported providers.- UI list behavior: render non-Discord rows as deprecated read-only rows with a clear non-dispatch badge.
- API contract: non-Discord existing rows are always non-dispatch, non-enable, deletable, and return deterministic validation error on enable/type mutation attempts.
- Explicitly block enable and type mutation attempts for non-Discord rows via API with deterministic validation errors.
- Allow deletion of deprecated rows.
Migration safety requirements:
- Migration SHALL be idempotent.
- Migration SHALL execute within an explicit transaction boundary.
- Migration SHALL require a pre-migration backup before mutating provider rows.
- Migration SHALL define and document a rollback procedure.
- Migration SHALL persist audit log fields for every mutated provider row (including provider identifier, previous values, new values, mutation timestamp, and operation identifier).
Migration implementation candidates:
backend/internal/services/notification_service.go(EnsureNotifyOnlyProviderMigration)backend/internal/services/enhanced_security_notification_service.go- new migration file under backend migration path if schema/data migration is introduced.
4.5 Legacy notifier retirement/removal scope
Targets to remove from active runtime/config surface:
backend/internal/notifications/engine.golegacy engine constantbackend/internal/notifications/router.golegacy engine branchbackend/internal/notifications/feature_flags.golegacy flag constantbackend/internal/api/handlers/feature_flags_handler.goretired legacy flag exposurebackend/internal/models/notification_provider.goretired legacy notifier comments/engine semanticsbackend/internal/services/notification_service.golegacy naming/comments/hooksbackend/internal/services/enhanced_security_notification_service.gonon-Discord dispatch branches indispatchToProviderfrontend/src/pages/Notifications.tsxandfrontend/src/api/notifications.tsto keep Discord-only UX/request contract aligned with backend fallback retirement
Out of scope for blocking completion:
- Archived docs/history artifacts under
docs/plans/archive/**and similar archive/report folders.
5) Dependency and Runtime Audit Plan (verify Discord-only runtime)
Run and record all commands in implementation evidence.
Task-aligned proof gates (mandatory):
- Run task
Security: Verify SBOMand capture output evidence. - Run task
Security: Scan Docker Image (Local)and capture output evidence. - Store reproducibility evidence under
test-results/notify-rollout-audit/(task outputs, command transcripts, and generated reports).
5.1 Dependency manifests
cd /projects/Charon/backend && go mod graph | grep -i "legacy_shoutrrr|feature.notifications.legacy.fallback_enabled|EngineLegacy|legacySendFunc|shoutrrr" || truegrep -i "legacy_shoutrrr|feature.notifications.legacy.fallback_enabled|EngineLegacy|legacySendFunc|shoutrrr" /projects/Charon/backend/go.mod /projects/Charon/backend/go.sum || truegrep -i "legacy_shoutrrr|feature.notifications.legacy.fallback_enabled|EngineLegacy|legacySendFunc|shoutrrr" /projects/Charon/package-lock.json /projects/Charon/frontend/package-lock.json 2>/dev/null || true
5.2 Source/runtime references (non-archive)
grep -RIn --exclude-dir=.git --exclude-dir=.cache --exclude-dir=docs --exclude-dir=playwright-output --exclude-dir=test-results "legacy_shoutrrr|feature.notifications.legacy.fallback_enabled|EngineLegacy|legacySendFunc|shoutrrr" /projects/Charon/backend /projects/Charon/frontend || true
5.3 Built artifact/container runtime checks
- Build image:
docker build -t charon:discord-only-audit /projects/Charon - Module metadata in binary:
docker run --rm charon:discord-only-audit sh -lc 'go version -m /app/charon 2>/dev/null | grep -i "legacy_shoutrrr|feature.notifications.legacy.fallback_enabled|EngineLegacy|legacySendFunc|shoutrrr" || true'
- Runtime filesystem/strings scan:
docker run --rm charon:discord-only-audit sh -lc 'find /app /usr/local/bin -maxdepth 4 -type f | xargs grep -Iin "legacy_shoutrrr|feature.notifications.legacy.fallback_enabled|EngineLegacy|legacySendFunc|shoutrrr" 2>/dev/null || true'
5.4 Documentation references (active docs only)
grep -RIn "legacy_shoutrrr|feature.notifications.legacy.fallback_enabled|EngineLegacy|legacySendFunc|shoutrrr" /projects/Charon/README.md /projects/Charon/ARCHITECTURE.md /projects/Charon/docs/features /projects/Charon/docs/security* || true
Audit pass criteria:
- No active dependency references.
- No active runtime/path references.
- No active user-facing docs claiming retired multi-service notifier behavior for this rollout.
6) Implementation Plan (phased)
Phase 1: Docker E2E rebuild decision and environment prep
- Apply repo testing-rule decision for E2E container rebuild based on changed paths.
- Rebuild/start E2E environment when required before any test execution.
Phase 2: E2E first and failing-spec triage baseline
- Capture exact Firefox failures for current branch:
cd /projects/Charon && npx playwright test --project=firefox
- Classify failing specs:
- In-scope: notification provider tests (
tests/settings/notifications.spec.tsand related). - Out-of-scope: manual DNS provider suites unless directly regressed by this change.
- In-scope: notification provider tests (
- If user-reported 5 failing specs are not notifications, create/attach separate tracking item and do not block Discord-only implementation on unrelated suite failures.
Phase 3: Local Patch Report preflight (mandatory before unit/coverage)
- Run local patch coverage preflight and generate required artifacts:
test-results/local-patch-report.mdtest-results/local-patch-report.json
Phase 4: Backend hard gate (authoritative)
- Implement Discord-only validation in API handlers and service layer.
- Remove/retire active legacy engine and flag exposure from runtime path.
- Apply migration policy for existing non-Discord rows.
Phase 5: Conditional GORM security check gate
- Run GORM security scanner in check mode when backend model/database trigger paths are modified.
- Gate completion on zero CRITICAL/HIGH findings for triggered changes.
Phase 6: Frontend Discord-only UX
- Restrict dropdown/options to Discord only.
- Align frontend type contract and submission behavior.
- Ensure edit/create forms cannot persist stale non-Discord values.
Phase 7: Legacy notifier retirement cleanup + audit evidence
- Remove active runtime legacy references listed in Section 4.5.
- Execute dependency/runtime/doc audit commands in Section 5 and save evidence.
Phase 8: Validation and release readiness
- Run validation sequence in required order (Section 7).
- Confirm rollout note: additional providers remain disabled pending per-provider validation PRs.
7) Targeted Testing Strategy
Mandatory repo QA/test order for this plan:
- Docker E2E rebuild decision and required rebuild/start.
- E2E first (Firefox baseline + targeted notification suite).
- Local Patch Report preflight artifact generation.
- Conditional GORM security check gate (
--checksemantics when trigger paths change). - CodeQL CI-aligned scans (Go and JS).
- Coverage gates (backend/frontend coverage thresholds and patch coverage review).
- Frontend type-check and backend/frontend build verification.
7.1 Backend tests (in scope)
backend/internal/api/handlers/notification_provider_blocker3_test.go- Expand from security-event-only gating to global Discord-only gating.
backend/internal/services/notification_service_test.go- Ensure non-Discord create/update/test/send rejection.
backend/internal/notifications/router_test.go- Prove legacy fallback decision remains disabled and cannot be toggled into active use.
backend/internal/services/notification_service_discord_only_test.go- Prove Discord-only dispatch behavior and no successful fallback path.
backend/internal/services/security_notification_service_test.go- Prove security notifications route through Discord-only provider dispatch in rollout mode.
- Add/adjust tests for migration policy behavior on existing non-Discord rows.
7.1.1 Notify-vs-fallback evidence points (required)
- Unit proof SHALL assert legacy dispatch hook call-count is exactly 0 for all Discord-success and Discord-failure scenarios.
- Unit/integration proof SHALL assert Notify dispatch hook/request path is called for Discord deliveries (including
Content-Type: application/jsonand deterministic provider-type guard behavior). - Negative proof SHALL assert non-Discord create/update/test attempts fail with stable error codes and cannot produce a successful delivery event.
- CI gate SHALL fail if any test demonstrates delivery success through a legacy fallback path.
7.2 Frontend unit tests (in scope)
frontend/src/pages/__tests__/Notifications.test.tsx- Assert only Discord option exists in provider-type select.
- Remove/adjust non-Discord create assumptions.
7.3 E2E tests (in scope)
tests/settings/notifications.spec.ts- Keep Discord create/edit/test flows.
- Replace or retire non-Discord CRUD expectations for this phase.
7.4 Firefox failing specs handling (required context)
- If the current failing 5 specs are notification-related, they are in scope and must be fixed within this PR.
- If they are unrelated (for example manual-dns-provider failures), track separately and avoid scope creep; do not mark as fixed under this provider-type change.
7.5 Suggested verification command set
cd /projects/Charon && npx playwright test --project=firefox tests/settings/notifications.spec.tscd /projects/Charon && npx playwright test --project=firefox --grep "Notification Providers"cd /projects/Charon && bash scripts/local-patch-report.shcd /projects/Charon && ./scripts/scan-gorm-security.sh --check(conditional on trigger paths)cd /projects/Charon && pre-commit run codeql-go-scan --all-filescd /projects/Charon && pre-commit run codeql-js-scan --all-filescd /projects/Charon && go test ./backend/internal/api/handlers -run Blocker3 -vcd /projects/Charon && go test ./backend/internal/services -run Notification -vcd /projects/Charon && scripts/go-test-coverage.shcd /projects/Charon/frontend && npm test -- Notificationscd /projects/Charon && scripts/frontend-test-coverage.shcd /projects/Charon/frontend && npm run type-check && npm run buildcd /projects/Charon/backend && go build ./...
8) Review of .gitignore, .dockerignore, codecov.yml, Dockerfile
Findings and required updates decision
.gitignore- No mandatory change required for feature behavior.
- Optional: ensure any new audit evidence artifacts (if added) are either committed intentionally or ignored consistently.
.dockerignore- No mandatory change required for Discord-only behavior.
- If adding migration/audit helper scripts needed at build time, ensure they are not excluded.
codecov.yml- No mandatory change required.
- If new test files are added, verify they are not accidentally ignored by broad patterns.
Dockerfile- No direct package-level retired notifier dependency is declared.
- No mandatory Dockerfile change for Discord-only behavior unless runtime audit shows residual retired notifier-linked binaries or references.
9) PR Slicing Strategy
Decision: Multiple PRs (3 slices) for safer rollout and easier rollback.
PR-1: Backend guardrails + migration policy
- Scope:
- API/service Discord-only validation.
- Existing non-Discord compatibility policy implementation.
- Legacy flag/engine runtime retirement in backend.
- Primary files:
backend/internal/api/handlers/notification_provider_handler.gobackend/internal/services/notification_service.gobackend/internal/services/enhanced_security_notification_service.gobackend/internal/notifications/engine.gobackend/internal/notifications/router.gobackend/internal/notifications/feature_flags.gobackend/internal/api/handlers/feature_flags_handler.gobackend/internal/models/notification_provider.go
- Gate:
- Backend tests pass, API rejects non-Discord, migration behavior verified.
- Rollback:
- Revert PR-1 if existing-provider compatibility regresses.
- Do not reintroduce fallback by setting legacy fallback flag true or restoring legacy dispatch routing as a hotfix.
PR-2: Frontend Discord-only UX and tests
- Scope:
- Dropdown/UI restriction to Discord only.
- Frontend test updates for new provider-type policy.
- Primary files:
frontend/src/pages/Notifications.tsxfrontend/src/api/notifications.tsfrontend/src/pages/__tests__/Notifications.test.tsxtests/settings/notifications.spec.ts
- Gate:
- Dependency: PR-2 entry is blocked until PR-1 is merged.
- Firefox notifications E2E targeted suite passes.
- Exit: frontend request contract submits
type=discordonly and backend rejects non-Discord mutations.
- Rollback:
- Revert PR-2 without reverting backend safeguards.
- No rollback action may re-enable hidden fallback behavior.
PR-3: Legacy notifier retirement evidence + docs alignment
- Scope:
- Execute audit commands, capture evidence, update active docs if needed.
- Primary files:
README.md,ARCHITECTURE.md, relevantdocs/features/**anddocs/security*files (only if active references remain).
- Gate:
- Dependency: PR-3 entry is blocked until PR-2 is merged.
- Entry blocker: runtime/dependency audit must pass before PR-3 can proceed.
- Dependency/runtime/doc audit pass criteria met.
- Exit: docs must reflect final backend/frontend state after PR-1 and PR-2 behavior.
- Rollback:
- Revert docs-only evidence changes without impacting runtime behavior.
- Preserve explicit no-fallback runtime guarantees from earlier slices.
10) Rollback Strategy (No Hidden Fallback Ambiguity)
Allowed rollback mechanisms:
- Revert entire rollout slice(s) to previous known-good commit/tag.
- Pause notifications by disabling delivery globally while keeping legacy fallback permanently disabled (
feature.notifications.legacy.fallback_enabled=false, immutable). Rollback SHALL revert to a prior release tag/DB snapshot only; it SHALL NOT re-enable or simulate legacy fallback. - Restore DB snapshot if migration state must be reverted.
Disallowed rollback mechanisms:
- Re-enable
feature.notifications.legacy.fallback_enabledfor runtime dispatch. - Reintroduce runtime
EngineLegacyor implicit fallback retry paths. - Apply emergency patches that create dual-path ambiguity for Discord success attribution.
11) Risks and Mitigations
- Risk: Existing non-Discord providers silently break user expectations.
- Mitigation: explicit deprecated/read-only policy and clear API errors.
- Risk: Hidden runtime legacy paths remain while UI appears Discord-only.
- Mitigation: backend-first enforcement and runtime audit commands.
- Risk: Scope creep from unrelated Firefox failures.
- Mitigation: strict in-scope/out-of-scope classification in Phase 1.
- Risk: Future provider rollout pressure bypasses validation discipline.
- Mitigation: staged enablement policy with per-provider validation gate.
12) Acceptance Criteria
- Provider type dropdown in notifications UI exposes only Discord.
- API create/update/test rejects any non-Discord provider type with deterministic error.
- Service/runtime dispatch is Discord-only for this rollout and attributable to Notify path.
- Existing non-Discord DB rows follow explicit compatibility policy (deprecated read-only and non-dispatch).
- Retired notifier references are absent from active dependency manifests (
go.mod,go.sum, package lock files). - Runtime audit confirms no active retired-notifier install/link/reference in executable/runtime paths.
- Active docs no longer claim multi-service notifier behavior for the current rollout.
- Firefox failing-spec triage clearly distinguishes in-scope notification failures from separately tracked suites.
.gitignore,.dockerignore,codecov.yml, andDockerfileare reviewed and updated only if required by actual implementation deltas.- Additional provider services remain disabled pending one-by-one validated rollout PRs.
- Legacy fallback feature-flag behavior is force-false and non-revivable through standard API operations.
- Rollback procedures preserve no-fallback ambiguity constraints.
13) Handoff to Supervisor
This spec is ready for Supervisor review and implementation assignment.
Review focus:
- Verify defense-in-depth layering is complete (UI + API + service + runtime).
- Confirm migration policy is explicit and reversible.
- Confirm audit command set is sufficient to prove legacy notifier retirement.
- Confirm PR slicing minimizes risk and isolates rollback paths.