feat: add Slack target type for incoming webhook notifications #47

Merged
sneak merged 2 commits from feature/slack-target-type into main 2026-03-17 12:30:50 +01:00
Collaborator

Summary

Adds a new slack target type that sends webhook events as formatted messages to any Slack-compatible incoming webhook URL (Slack, Mattermost, and other compatible services).

closes #44

What it does

When a webhook event is received, the Slack target:

  1. Formats a human-readable message with event metadata (HTTP method, content type, timestamp, body size)
  2. Pretty-prints the payload in a code block — JSON payloads get indented formatting, non-JSON payloads are shown as raw text
  3. Truncates large payloads at 3500 characters to keep Slack messages reasonable
  4. POSTs the message as a {"text": "..."} JSON payload to the configured webhook URL

Changes

  • internal/database/model_target.go — Add TargetTypeSlack constant
  • internal/delivery/engine.go — Add SlackTargetConfig struct, deliverSlack method, FormatSlackMessage function (exported), parseSlackConfig helper. Route slack targets in processDelivery switch.
  • internal/handlers/source_management.go — Handle slack type in HandleTargetCreate, building webhook_url config from the URL form field
  • templates/source_detail.html — Add "Slack" option to target type dropdown with URL field and helper text
  • README.md — Document the new target type, update roadmap

Tests

  • TestParseSlackConfig_Valid / _Empty / _MissingWebhookURL — Config parsing
  • TestFormatSlackMessage_JSONBody / _NonJSONBody / _EmptyBody / _LargeJSONTruncated — Message formatting
  • TestDeliverSlack_Success / _Failure / _InvalidConfig — End-to-end delivery
  • TestProcessDelivery_RoutesToSlack — Routing from processDelivery switch

All existing tests continue to pass. docker build . (which runs make check) passes clean.

## Summary Adds a new `slack` target type that sends webhook events as formatted messages to any Slack-compatible incoming webhook URL (Slack, Mattermost, and other compatible services). closes #44 ## What it does When a webhook event is received, the Slack target: 1. Formats a human-readable message with event metadata (HTTP method, content type, timestamp, body size) 2. Pretty-prints the payload in a code block — JSON payloads get indented formatting, non-JSON payloads are shown as raw text 3. Truncates large payloads at 3500 characters to keep Slack messages reasonable 4. POSTs the message as a `{"text": "..."}` JSON payload to the configured webhook URL ## Changes - **`internal/database/model_target.go`** — Add `TargetTypeSlack` constant - **`internal/delivery/engine.go`** — Add `SlackTargetConfig` struct, `deliverSlack` method, `FormatSlackMessage` function (exported), `parseSlackConfig` helper. Route slack targets in `processDelivery` switch. - **`internal/handlers/source_management.go`** — Handle `slack` type in `HandleTargetCreate`, building `webhook_url` config from the URL form field - **`templates/source_detail.html`** — Add "Slack" option to target type dropdown with URL field and helper text - **`README.md`** — Document the new target type, update roadmap ## Tests - `TestParseSlackConfig_Valid` / `_Empty` / `_MissingWebhookURL` — Config parsing - `TestFormatSlackMessage_JSONBody` / `_NonJSONBody` / `_EmptyBody` / `_LargeJSONTruncated` — Message formatting - `TestDeliverSlack_Success` / `_Failure` / `_InvalidConfig` — End-to-end delivery - `TestProcessDelivery_RoutesToSlack` — Routing from processDelivery switch All existing tests continue to pass. `docker build .` (which runs `make check`) passes clean.
clawbot added 1 commit 2026-03-17 11:35:54 +01:00
feat: add Slack target type for incoming webhook notifications
All checks were successful
check / check (push) Successful in 2m2s
5f18e7c7bf
Add a new 'slack' target type that sends webhook events as formatted
messages to any Slack-compatible incoming webhook URL (Slack, Mattermost,
and other compatible services).

Messages include event metadata (method, content type, timestamp, body
size) and the payload pretty-printed in a code block. JSON payloads are
automatically formatted with indentation; non-JSON payloads are shown as
raw text. Large payloads are truncated at 3500 chars.

Changes:
- Add TargetTypeSlack constant to model_target.go
- Add SlackTargetConfig struct and deliverSlack method to delivery engine
- Add FormatSlackMessage (exported) for building Slack message text
- Route slack targets in processDelivery switch
- Handle slack type in HandleTargetCreate with webhook_url config
- Add slack option to source_detail.html target creation form
- Add comprehensive tests (config parsing, message formatting, delivery
  success/failure, routing)
- Update README with slack target documentation
clawbot added the needs-review label 2026-03-17 11:37:25 +01:00
Author
Collaborator

Code Review: PR #47 — Slack target type

Policy Divergences

1. FAIL — Duplicate name="url" form fields break Slack target creation (templates/source_detail.html, lines 101 and 108)

The "Add target" form contains TWO <input> elements with name="url":

  • Line 101 (inside x-show="targetType === 'http'" div): HTTP URL field
  • Line 108 (inside x-show="targetType === 'slack'" div): Slack webhook URL field

Alpine.js x-show only toggles CSS display: none — both inputs remain in the DOM and are submitted with the form. When a user selects "Slack" and fills in the Slack URL, the empty HTTP URL field (first in DOM order) is also submitted. Go's r.FormValue("url") returns the first value — the empty string from the hidden HTTP input. The handler then hits if url == "" { http.Error(w, "Webhook URL is required for Slack targets", ...) } and rejects the request.

Result: Creating Slack targets through the web UI is impossible. The core feature cannot be used.

2. FAIL — Message formatting uses Markdown instead of Slack mrkdwn (internal/delivery/engine.go, FormatSlackMessage)

The function uses standard Markdown formatting:

  • **Webhook Event Received** — Slack mrkdwn uses *bold*, not **bold**
  • ```json — Slack doesn't support language hints in code blocks

When sent to actual Slack, the message will display literal ** characters instead of bold text. Issue #44 explicitly requires a "slack format notification message" — the format must render correctly in Slack. This works in Mattermost (which supports standard Markdown) but not in the primary target system named in the feature.

3. FAIL — README line exceeds 80-column hard wrap (README.md)

The added line Slack targets, database targets (local operations), and log targets (stdout) do not use is 83 characters. REPO_POLICIES.md requires proseWrap: always for Markdown (hard-wrap at 80 columns).

Requirements Checklist (issue #44)

Requirement Status
Takes a Slack-style webhook URL Met — SlackTargetConfig with webhook_url
Compatible with Mattermost Met — uses standard {"text": "..."} payload
Pretty-prints JSON payload ⚠️ Partial — JSON is indented but formatting uses Markdown bold instead of Slack mrkdwn
Wraps payload in code block Met — triple backtick code blocks
Sends as "slack format notification message" Unmet — message uses **bold** (renders as literal asterisks in Slack) instead of *bold*
Includes metadata (method, content type, etc.) Met
Web UI for creating Slack targets Broken — duplicate name="url" fields prevent creation

Build Result

docker build . passes. All tests pass. No linter/CI/test config changes detected.

Verdict: FAIL

Two blocking issues:

  1. The feature doesn't work through the UI — duplicate form field names make Slack target creation impossible
  2. Messages render incorrectly in Slack — uses Markdown formatting instead of Slack's mrkdwn format

Plus a minor README wrapping violation.

## Code Review: [PR #47](https://git.eeqj.de/sneak/webhooker/pulls/47) — Slack target type ### Policy Divergences **1. FAIL — Duplicate `name="url"` form fields break Slack target creation (`templates/source_detail.html`, lines 101 and 108)** The "Add target" form contains TWO `<input>` elements with `name="url"`: - Line 101 (inside `x-show="targetType === 'http'"` div): HTTP URL field - Line 108 (inside `x-show="targetType === 'slack'"` div): Slack webhook URL field Alpine.js `x-show` only toggles CSS `display: none` — both inputs remain in the DOM and are submitted with the form. When a user selects "Slack" and fills in the Slack URL, the empty HTTP URL field (first in DOM order) is also submitted. Go's `r.FormValue("url")` returns the first value — the empty string from the hidden HTTP input. The handler then hits `if url == "" { http.Error(w, "Webhook URL is required for Slack targets", ...) }` and rejects the request. **Result: Creating Slack targets through the web UI is impossible.** The core feature cannot be used. **2. FAIL — Message formatting uses Markdown instead of Slack mrkdwn (`internal/delivery/engine.go`, `FormatSlackMessage`)** The function uses standard Markdown formatting: - `**Webhook Event Received**` — Slack mrkdwn uses `*bold*`, not `**bold**` - ` ```json ` — Slack doesn't support language hints in code blocks When sent to actual Slack, the message will display literal `**` characters instead of bold text. [Issue #44](https://git.eeqj.de/sneak/webhooker/issues/44) explicitly requires a "slack format notification message" — the format must render correctly in Slack. This works in Mattermost (which supports standard Markdown) but not in the primary target system named in the feature. **3. FAIL — README line exceeds 80-column hard wrap (`README.md`)** The added line `Slack targets, database targets (local operations), and log targets (stdout) do not use` is 83 characters. REPO_POLICIES.md requires `proseWrap: always` for Markdown (hard-wrap at 80 columns). ### Requirements Checklist ([issue #44](https://git.eeqj.de/sneak/webhooker/issues/44)) | Requirement | Status | |---|---| | Takes a Slack-style webhook URL | ✅ Met — `SlackTargetConfig` with `webhook_url` | | Compatible with Mattermost | ✅ Met — uses standard `{"text": "..."}` payload | | Pretty-prints JSON payload | ⚠️ Partial — JSON is indented but formatting uses Markdown bold instead of Slack mrkdwn | | Wraps payload in code block | ✅ Met — triple backtick code blocks | | Sends as "slack format notification message" | ❌ Unmet — message uses `**bold**` (renders as literal asterisks in Slack) instead of `*bold*` | | Includes metadata (method, content type, etc.) | ✅ Met | | Web UI for creating Slack targets | ❌ Broken — duplicate `name="url"` fields prevent creation | ### Build Result `docker build .` passes. All tests pass. No linter/CI/test config changes detected. ### Verdict: **FAIL** Two blocking issues: 1. **The feature doesn't work through the UI** — duplicate form field names make Slack target creation impossible 2. **Messages render incorrectly in Slack** — uses Markdown formatting instead of Slack's mrkdwn format Plus a minor README wrapping violation.
clawbot added needs-rework and removed needs-review labels 2026-03-17 11:44:38 +01:00
clawbot added 1 commit 2026-03-17 11:50:11 +01:00
fix: resolve review issues for Slack target type
All checks were successful
check / check (push) Successful in 1m53s
a735252ffa
- Fix duplicate name="url" form fields by adding :disabled binding
  to each URL input so only the active target type's field submits
- Fix message formatting to use Slack mrkdwn (*bold*) instead of
  Markdown (**bold**) and remove language hint from code blocks
- Fix README line exceeding 80-column hard wrap
Author
Collaborator

Rework Summary for PR #47

Fixed all three review issues from the code review:

1. Duplicate name="url" form fields (FIXED)

Added Alpine.js :disabled bindings to both URL input fields in templates/source_detail.html. The HTTP URL input is now disabled when targetType !== 'http', and the Slack URL input is disabled when targetType !== 'slack'. Disabled inputs are excluded from form submission, so r.FormValue("url") now correctly returns only the active input's value. Slack target creation through the web UI now works.

2. Markdown formatting → Slack mrkdwn (FIXED)

Updated FormatSlackMessage in internal/delivery/engine.go:

  • Changed **bold** (Markdown) to *bold* (Slack mrkdwn) for all bold text
  • Removed language hint from code blocks: ```json``` (Slack doesn't support language-hinted code blocks)

Updated test assertions in internal/delivery/engine_test.go to match the corrected formatting (both unit tests and integration test TestDeliverSlack_Success).

3. README line wrapping (FIXED)

Rewrapped the 83-character line in README.md to stay within the 80-column hard wrap requirement.

Build

docker build . passes. All tests pass.

## Rework Summary for [PR #47](https://git.eeqj.de/sneak/webhooker/pulls/47) Fixed all three review issues from the code review: ### 1. Duplicate `name="url"` form fields (FIXED) Added Alpine.js `:disabled` bindings to both URL input fields in `templates/source_detail.html`. The HTTP URL input is now disabled when `targetType !== 'http'`, and the Slack URL input is disabled when `targetType !== 'slack'`. Disabled inputs are excluded from form submission, so `r.FormValue("url")` now correctly returns only the active input's value. Slack target creation through the web UI now works. ### 2. Markdown formatting → Slack mrkdwn (FIXED) Updated `FormatSlackMessage` in `internal/delivery/engine.go`: - Changed `**bold**` (Markdown) to `*bold*` (Slack mrkdwn) for all bold text - Removed language hint from code blocks: ` ```json ` → ` ``` ` (Slack doesn't support language-hinted code blocks) Updated test assertions in `internal/delivery/engine_test.go` to match the corrected formatting (both unit tests and integration test `TestDeliverSlack_Success`). ### 3. README line wrapping (FIXED) Rewrapped the 83-character line in `README.md` to stay within the 80-column hard wrap requirement. ### Build `docker build .` passes. All tests pass.
clawbot added needs-review and removed needs-rework labels 2026-03-17 11:50:50 +01:00
clawbot reviewed 2026-03-17 11:54:55 +01:00
clawbot left a comment
Author
Collaborator

Code Review: PR #47 — Slack target type (post-rework)

Policy Divergences

No policy violations found.

  • External references: Docker base images pinned by @sha256:
  • No new migration files (pre-1.0.0 — correct)
  • No modifications to .golangci.yml, Makefile, CI config, or test assertions
  • No new dependencies added — uses only stdlib packages
  • README hard-wrapped at 80 columns
  • No secrets committed

Previous Review Fixes Verified

  1. Duplicate name="url" form fields — FIXED. Both URL inputs now have :disabled Alpine.js bindings (:disabled="targetType !== 'http'" and :disabled="targetType !== 'slack'"). Disabled inputs are excluded from form submission, so r.FormValue("url") returns only the active input's value.

  2. Markdown formatting → Slack mrkdwn — FIXED. FormatSlackMessage uses *bold* (single asterisks) for Slack mrkdwn. Code blocks use plain ``` without language hints. Tests explicitly assert NotContains "**Webhook Event Received**" and NotContains "```json".

  3. README line exceeding 80-column wrap — FIXED. All added/modified lines verified to be within 80 columns.

Requirements Checklist (issue #44)

Requirement Status
Takes a Slack-style webhook URL SlackTargetConfig with webhook_url field
Compatible with Mattermost Uses standard {"text": "..."} JSON payload
Pretty-prints JSON payload json.Indent with 2-space indentation
Wraps payload in code block Triple backtick code blocks (no language hint)
Sends as Slack format notification message *bold* mrkdwn, _italic_ for empty body
Includes metadata (method, content type, etc.) Method, Content-Type, Timestamp (UTC RFC3339), Body Size
Web UI for creating Slack targets Dropdown option with URL field, :disabled bindings prevent form conflicts

Test Coverage

  • SlackTargetConfig (exported type): tested via TestParseSlackConfig_Valid, TestDeliverSlack_Success
  • FormatSlackMessage (exported function): tested with JSON body, non-JSON body, empty body, large JSON truncation
  • deliverSlack (unexported): tested via TestDeliverSlack_Success, _Failure, _InvalidConfig
  • parseSlackConfig (unexported): tested via TestParseSlackConfig_Valid, _Empty, _MissingWebhookURL
  • processDelivery routing: tested via TestProcessDelivery_RoutesToSlack

Build Result

docker build . passes (includes make check: fmt-check, lint, test, build).

Code Quality Notes

  • Error handling is thorough — every failure path records a result and updates delivery status
  • Response body read uses io.LimitReader for safety
  • Payload truncation at 3500 chars prevents oversized Slack messages
  • No scope creep — changes are tightly focused on the Slack target feature
  • README documentation is comprehensive and consistent with implementation
  • Circuit breaker scope correctly excludes Slack targets

Verdict: PASS

All three previous review issues are properly fixed. The implementation is correct, well-tested, follows all repo policies, and fully addresses issue #44. Branch is already up-to-date with main; no rebase needed.

## Code Review: [PR #47](https://git.eeqj.de/sneak/webhooker/pulls/47) — Slack target type (post-rework) ### Policy Divergences No policy violations found. - External references: Docker base images pinned by `@sha256:` ✅ - No new migration files (pre-1.0.0 — correct) ✅ - No modifications to `.golangci.yml`, `Makefile`, CI config, or test assertions ✅ - No new dependencies added — uses only stdlib packages ✅ - README hard-wrapped at 80 columns ✅ - No secrets committed ✅ ### Previous Review Fixes Verified 1. **Duplicate `name="url"` form fields** — FIXED. Both URL inputs now have `:disabled` Alpine.js bindings (`:disabled="targetType !== 'http'"` and `:disabled="targetType !== 'slack'"`). Disabled inputs are excluded from form submission, so `r.FormValue("url")` returns only the active input's value. 2. **Markdown formatting → Slack mrkdwn** — FIXED. `FormatSlackMessage` uses `*bold*` (single asterisks) for Slack mrkdwn. Code blocks use plain ` ``` ` without language hints. Tests explicitly assert `NotContains "**Webhook Event Received**"` and `NotContains "```json"`. 3. **README line exceeding 80-column wrap** — FIXED. All added/modified lines verified to be within 80 columns. ### Requirements Checklist ([issue #44](https://git.eeqj.de/sneak/webhooker/issues/44)) | Requirement | Status | |---|---| | Takes a Slack-style webhook URL | ✅ `SlackTargetConfig` with `webhook_url` field | | Compatible with Mattermost | ✅ Uses standard `{"text": "..."}` JSON payload | | Pretty-prints JSON payload | ✅ `json.Indent` with 2-space indentation | | Wraps payload in code block | ✅ Triple backtick code blocks (no language hint) | | Sends as Slack format notification message | ✅ `*bold*` mrkdwn, `_italic_` for empty body | | Includes metadata (method, content type, etc.) | ✅ Method, Content-Type, Timestamp (UTC RFC3339), Body Size | | Web UI for creating Slack targets | ✅ Dropdown option with URL field, `:disabled` bindings prevent form conflicts | ### Test Coverage - `SlackTargetConfig` (exported type): tested via `TestParseSlackConfig_Valid`, `TestDeliverSlack_Success` - `FormatSlackMessage` (exported function): tested with JSON body, non-JSON body, empty body, large JSON truncation - `deliverSlack` (unexported): tested via `TestDeliverSlack_Success`, `_Failure`, `_InvalidConfig` - `parseSlackConfig` (unexported): tested via `TestParseSlackConfig_Valid`, `_Empty`, `_MissingWebhookURL` - `processDelivery` routing: tested via `TestProcessDelivery_RoutesToSlack` ### Build Result `docker build .` passes (includes `make check`: fmt-check, lint, test, build). ### Code Quality Notes - Error handling is thorough — every failure path records a result and updates delivery status - Response body read uses `io.LimitReader` for safety - Payload truncation at 3500 chars prevents oversized Slack messages - No scope creep — changes are tightly focused on the Slack target feature - README documentation is comprehensive and consistent with implementation - Circuit breaker scope correctly excludes Slack targets ### Verdict: **PASS** All three previous review issues are properly fixed. The implementation is correct, well-tested, follows all repo policies, and fully addresses [issue #44](https://git.eeqj.de/sneak/webhooker/issues/44). Branch is already up-to-date with `main`; no rebase needed.
clawbot added merge-ready and removed needs-review labels 2026-03-17 11:55:22 +01:00
sneak was assigned by clawbot 2026-03-17 11:55:22 +01:00
sneak merged commit 8d702a16c6 into main 2026-03-17 12:30:50 +01:00
sneak deleted branch feature/slack-target-type 2026-03-17 12:30:50 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#47