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).
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.
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
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.
**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).
docker build . passes. All tests pass. No linter/CI/test config changes detected.
Verdict: FAIL
Two blocking issues:
The feature doesn't work through the UI — duplicate form field names make Slack target creation impossible
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.
- 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
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.
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
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.
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".
README line exceeding 80-column wrap — FIXED. All added/modified lines verified to be within 80 columns.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Adds a new
slacktarget 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:
{"text": "..."}JSON payload to the configured webhook URLChanges
internal/database/model_target.go— AddTargetTypeSlackconstantinternal/delivery/engine.go— AddSlackTargetConfigstruct,deliverSlackmethod,FormatSlackMessagefunction (exported),parseSlackConfighelper. Route slack targets inprocessDeliveryswitch.internal/handlers/source_management.go— Handleslacktype inHandleTargetCreate, buildingwebhook_urlconfig from the URL form fieldtemplates/source_detail.html— Add "Slack" option to target type dropdown with URL field and helper textREADME.md— Document the new target type, update roadmapTests
TestParseSlackConfig_Valid/_Empty/_MissingWebhookURL— Config parsingTestFormatSlackMessage_JSONBody/_NonJSONBody/_EmptyBody/_LargeJSONTruncated— Message formattingTestDeliverSlack_Success/_Failure/_InvalidConfig— End-to-end deliveryTestProcessDelivery_RoutesToSlack— Routing from processDelivery switchAll existing tests continue to pass.
docker build .(which runsmake check) passes clean.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 withname="url":x-show="targetType === 'http'"div): HTTP URL fieldx-show="targetType === 'slack'"div): Slack webhook URL fieldAlpine.js
x-showonly toggles CSSdisplay: 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'sr.FormValue("url")returns the first value — the empty string from the hidden HTTP input. The handler then hitsif 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 blocksWhen 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 useis 83 characters. REPO_POLICIES.md requiresproseWrap: alwaysfor Markdown (hard-wrap at 80 columns).Requirements Checklist (issue #44)
SlackTargetConfigwithwebhook_url{"text": "..."}payload**bold**(renders as literal asterisks in Slack) instead of*bold*name="url"fields prevent creationBuild Result
docker build .passes. All tests pass. No linter/CI/test config changes detected.Verdict: FAIL
Two blocking issues:
Plus a minor README wrapping violation.
Rework Summary for PR #47
Fixed all three review issues from the code review:
1. Duplicate
name="url"form fields (FIXED)Added Alpine.js
:disabledbindings to both URL input fields intemplates/source_detail.html. The HTTP URL input is now disabled whentargetType !== 'http', and the Slack URL input is disabled whentargetType !== 'slack'. Disabled inputs are excluded from form submission, sor.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
FormatSlackMessageininternal/delivery/engine.go:**bold**(Markdown) to*bold*(Slack mrkdwn) for all bold text```json→```(Slack doesn't support language-hinted code blocks)Updated test assertions in
internal/delivery/engine_test.goto match the corrected formatting (both unit tests and integration testTestDeliverSlack_Success).3. README line wrapping (FIXED)
Rewrapped the 83-character line in
README.mdto stay within the 80-column hard wrap requirement.Build
docker build .passes. All tests pass.Code Review: PR #47 — Slack target type (post-rework)
Policy Divergences
No policy violations found.
@sha256:✅.golangci.yml,Makefile, CI config, or test assertions ✅Previous Review Fixes Verified
Duplicate
name="url"form fields — FIXED. Both URL inputs now have:disabledAlpine.js bindings (:disabled="targetType !== 'http'"and:disabled="targetType !== 'slack'"). Disabled inputs are excluded from form submission, sor.FormValue("url")returns only the active input's value.Markdown formatting → Slack mrkdwn — FIXED.
FormatSlackMessageuses*bold*(single asterisks) for Slack mrkdwn. Code blocks use plain```without language hints. Tests explicitly assertNotContains "**Webhook Event Received**"andNotContains "```json".README line exceeding 80-column wrap — FIXED. All added/modified lines verified to be within 80 columns.
Requirements Checklist (issue #44)
SlackTargetConfigwithwebhook_urlfield{"text": "..."}JSON payloadjson.Indentwith 2-space indentation*bold*mrkdwn,_italic_for empty body:disabledbindings prevent form conflictsTest Coverage
SlackTargetConfig(exported type): tested viaTestParseSlackConfig_Valid,TestDeliverSlack_SuccessFormatSlackMessage(exported function): tested with JSON body, non-JSON body, empty body, large JSON truncationdeliverSlack(unexported): tested viaTestDeliverSlack_Success,_Failure,_InvalidConfigparseSlackConfig(unexported): tested viaTestParseSlackConfig_Valid,_Empty,_MissingWebhookURLprocessDeliveryrouting: tested viaTestProcessDelivery_RoutesToSlackBuild Result
docker build .passes (includesmake check: fmt-check, lint, test, build).Code Quality Notes
io.LimitReaderfor safetyVerdict: 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.