Deleting a target soft deletes its row while its deliveries survive in the per-webhook database, so the event log kept the history and lost the label.
Change
loadTargetMap (internal/handlers/source_management.go) already loaded soft-deleted rows Unscoped() for the redactor half, then discarded them before building the view half. It now builds both halves from every loaded row.
delivery.TargetView gains Deleted (from DeletedAt.Valid) and DisplayName(), which renders name (deleted). The bare name is not restored: an operator debugging an old delivery needs to know the target is gone, not just what it was called. Name stays the stored name.
templates/source_logs.html renders DisplayName at both places a delivery's target is named (the collapsed status line and the deliveries block heading).
Masking is unchanged
Views still come from NewTargetViews, so a deleted target's configuration is masked by exactly the code that masks a live one's — TargetView has no raw config field, and nothing else was widened. TestHandleSourceLogs_MasksDeletedTargetConfig stores a Slack webhook URL on a target, deletes the target through the real delete handler, and asserts neither the credential path nor webhookUrl appears in the rendered page while the name does.
Scope of the widening — what I audited
The map is historical display only. Its view half reaches nothing but DeliveryView.Target on the event log page. Everything that could deliver to or re-activate a target resolves its own targets and is untouched:
receiver and resubmit fan-out: loadActiveTargets (internal/handlers/webhook.go), scoped and active = true.
delivery engine loadTargetMap (internal/delivery/engine.go): scoped. No file under internal/delivery other than the display projection target_config_view.go is touched, so nothing here anticipates #107.
replay: replayTarget (internal/handlers/delivery_replay.go) does its own Unscoped() lookup and still refuses a deleted target with its own banner.
source detail target list (source_management.go:452) and the target edit form (ownedTarget): scoped, so a deleted target stays off both. Confirmed live below.
Other views that show a target name against historical data
Searched every template and handler. templates/source_logs.html is the only place a name renders against delivery history, and both of its occurrences are fixed. target_edit.html renders a live target only. The replay and resubmit banners name no target. There is no JSON/API surface that projects a target name. A target_id naming no row at all — deleted or otherwise — still renders blank, but no code path hard deletes a target, so that state is not reachable through the app.
Verification
make check green, GOFLAGS=-count=1, lint ran uncached in Docker ([lint 3/3] RUN golangci-lint run — 0 issues, 48s, not CACHED); all 20 packages ok.
Both new handler tests were confirmed failing on unmodified next first: the rendered page contained : delivered and <span class="text-sm text-gray-700"></span>.
Live before/after on a running instance, same script against a build of unmodified next and a build of this branch: create a log target named NAMED-TARGET-211, POST a real event through the entrypoint, view the event log, delete the target through the UI, view it again.
and on the source detail page after the delete, occurrences of NAMED-TARGET-211 on detail page: 0 — the deleted target does not come back into the live target list.
Tests added: TestHandleSourceLogs_NamesDeletedTarget (before/after control on one source, deleting through the real handler), TestHandleSourceLogs_MasksDeletedTargetConfig, TestNewTargetViews_DeletedTarget, plus live-target Deleted/DisplayName assertions folded into TestNewTargetViews_Slack so the marker cannot reach a name that still exists.
Closes https://git.eeqj.de/sneak/webhooker/issues/211.
Deleting a target soft deletes its row while its deliveries survive in the per-webhook database, so the event log kept the history and lost the label.
## Change
- `loadTargetMap` (`internal/handlers/source_management.go`) already loaded soft-deleted rows `Unscoped()` for the redactor half, then discarded them before building the view half. It now builds both halves from every loaded row.
- `delivery.TargetView` gains `Deleted` (from `DeletedAt.Valid`) and `DisplayName()`, which renders `name (deleted)`. The bare name is not restored: an operator debugging an old delivery needs to know the target is gone, not just what it was called. `Name` stays the stored name.
- `templates/source_logs.html` renders `DisplayName` at both places a delivery's target is named (the collapsed status line and the deliveries block heading).
## Masking is unchanged
Views still come from `NewTargetViews`, so a deleted target's configuration is masked by exactly the code that masks a live one's — `TargetView` has no raw config field, and nothing else was widened. `TestHandleSourceLogs_MasksDeletedTargetConfig` stores a Slack webhook URL on a target, deletes the target through the real delete handler, and asserts neither the credential path nor `webhookUrl` appears in the rendered page while the name does.
## Scope of the widening — what I audited
The map is historical display only. Its view half reaches nothing but `DeliveryView.Target` on the event log page. Everything that could deliver to or re-activate a target resolves its own targets and is untouched:
- receiver and resubmit fan-out: `loadActiveTargets` (`internal/handlers/webhook.go`), scoped and `active = true`.
- delivery engine `loadTargetMap` (`internal/delivery/engine.go`): scoped. No file under `internal/delivery` other than the display projection `target_config_view.go` is touched, so nothing here anticipates https://git.eeqj.de/sneak/webhooker/issues/107.
- replay: `replayTarget` (`internal/handlers/delivery_replay.go`) does its own `Unscoped()` lookup and still refuses a deleted target with its own banner.
- source detail target list (`source_management.go:452`) and the target edit form (`ownedTarget`): scoped, so a deleted target stays off both. Confirmed live below.
## Other views that show a target name against historical data
Searched every template and handler. `templates/source_logs.html` is the only place a name renders against delivery history, and both of its occurrences are fixed. `target_edit.html` renders a live target only. The replay and resubmit banners name no target. There is no JSON/API surface that projects a target name. A `target_id` naming no row at all — deleted or otherwise — still renders blank, but no code path hard deletes a target, so that state is not reachable through the app.
## Verification
`make check` green, `GOFLAGS=-count=1`, lint ran uncached in Docker (`[lint 3/3] RUN golangci-lint run` — `0 issues`, 48s, not `CACHED`); all 20 packages `ok`.
Both new handler tests were confirmed failing on unmodified `next` first: the rendered page contained `: delivered` and `<span class="text-sm text-gray-700"></span>`.
Live before/after on a running instance, same script against a build of unmodified `next` and a build of this branch: create a `log` target named `NAMED-TARGET-211`, POST a real event through the entrypoint, view the event log, delete the target through the UI, view it again.
Unmodified `next`, after the delete:
```
occurrences of NAMED-TARGET-211: 0
<span class="text-xs text-green-600">
: delivered
<span class="text-sm text-gray-700"></span>
```
This branch, after the same delete:
```
occurrences of NAMED-TARGET-211: 2
NAMED-TARGET-211 (deleted): delivered
<span class="text-sm text-gray-700">NAMED-TARGET-211 (deleted)</span>
```
and on the source detail page after the delete, `occurrences of NAMED-TARGET-211 on detail page: 0` — the deleted target does not come back into the live target list.
Tests added: `TestHandleSourceLogs_NamesDeletedTarget` (before/after control on one source, deleting through the real handler), `TestHandleSourceLogs_MasksDeletedTargetConfig`, `TestNewTargetViews_DeletedTarget`, plus live-target `Deleted`/`DisplayName` assertions folded into `TestNewTargetViews_Slack` so the marker cannot reach a name that still exists.
`TODO.md` deliberately untouched per https://git.eeqj.de/sneak/webhooker/issues/112.
Deleting a target soft deletes its row while its deliveries survive
in the per-webhook database, so the event log kept rendering the
history and lost the label: every historical row read ": delivered"
and the deliveries block was headed by an empty name.
loadTargetMap already loaded soft-deleted rows Unscoped for the
redactor half, then discarded them before building the view half.
It now builds both halves from every loaded row, and TargetView
carries a Deleted flag with a DisplayName that renders
"name (deleted)" — an operator debugging an old delivery needs to
know the target is gone, not just what it was called.
The views still come from NewTargetViews, so a deleted target's
configuration is masked by exactly the code that masks a live
one's. The widening is confined to this map, which feeds only
DeliveryView.Target on the event log page: the source detail
target list, the target edit form, the receiver and resubmit fan-out
and the delivery engine each resolve targets through their own
scoped queries, and the replay path keeps refusing a deleted target.
clawbot
self-assigned this 2026-08-24 01:51:28 +02:00
Checked by execution from a clean clone: the blanking reproduces on the parent commit and is fixed at both render sites (marker appears exactly twice, matching the live control); no credential (Slack webhook URL, HTTP destination URL, Authorization: Bearer header) reaches the event log or the source detail page for a deleted target, and its masked rendering is identical to a live target's; the redactor half is unchanged and still redacts a deleted target's stored response body and error; the deleted target stays refused by the source detail list, the edit form (404), toggle, replay (?replay=target-deleted, no new delivery queued) and both fan-outs; a delete mid-retry renders name (deleted) with retrying and does not panic; both new handler tests fail on the parent. CI green on 2729155; make check green with GOFLAGS=-count=1 (88s, 20/20 packages ok, zero (cached), lint uncached in Docker, 0 issues); mergeable with next.
Two points worth recording rather than confirming:
The disclosed residual holds. Nothing hard-deletes a target row: the retention reaper hard-deletes only events, deliveries and delivery results; deleteWebhookResources soft-deletes targets while destroying the whole per-webhook event database, so no delivery survives to be orphaned; and the DELETE FROM targets in internal/database/event_db_isolation.go clears a stray legacy table inside the event database, not the config database. Unreachable as claimed.
The change is narrower than the PR body needs to claim: templates/source_logs.html reads only .Target.DisplayName, so the widened view's Config never reaches a template at all. The Unscoped() load itself was already present on the parent, so the query was not widened — only which loaded rows get projected.
Disclosures: no live HTTP instance was stood up; the harness drove the real handlers and real templates in-process, which is the same rendering path. Anomaly, not a defect: the landing commit is authored sneak <sneak@sneak.berlin> where neighbouring commits are clawbot.
PASS.
Checked by execution from a clean clone: the blanking reproduces on the parent commit and is fixed at both render sites (marker appears exactly twice, matching the live control); no credential (Slack webhook URL, HTTP destination URL, `Authorization: Bearer` header) reaches the event log or the source detail page for a deleted target, and its masked rendering is identical to a live target's; the redactor half is unchanged and still redacts a deleted target's stored response body and error; the deleted target stays refused by the source detail list, the edit form (404), toggle, replay (`?replay=target-deleted`, no new delivery queued) and both fan-outs; a delete mid-retry renders `name (deleted)` with `retrying` and does not panic; both new handler tests fail on the parent. CI green on `2729155`; `make check` green with `GOFLAGS=-count=1` (88s, 20/20 packages `ok`, zero `(cached)`, lint uncached in Docker, `0 issues`); mergeable with `next`.
Two points worth recording rather than confirming:
- The disclosed residual holds. Nothing hard-deletes a target row: the retention reaper hard-deletes only events, deliveries and delivery results; `deleteWebhookResources` soft-deletes targets while destroying the whole per-webhook event database, so no delivery survives to be orphaned; and the `DELETE FROM targets` in `internal/database/event_db_isolation.go` clears a stray legacy table inside the event database, not the config database. Unreachable as claimed.
- The change is narrower than the PR body needs to claim: `templates/source_logs.html` reads only `.Target.DisplayName`, so the widened view's `Config` never reaches a template at all. The `Unscoped()` load itself was already present on the parent, so the query was not widened — only which loaded rows get projected.
Disclosures: no live HTTP instance was stood up; the harness drove the real handlers and real templates in-process, which is the same rendering path. Anomaly, not a defect: the landing commit is authored `sneak <sneak@sneak.berlin>` where neighbouring commits are `clawbot`.
clawbot
merged commit 5fda446c71 into next2026-08-24 02:03:23 +02:00
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.
Closes #211.
Deleting a target soft deletes its row while its deliveries survive in the per-webhook database, so the event log kept the history and lost the label.
Change
loadTargetMap(internal/handlers/source_management.go) already loaded soft-deleted rowsUnscoped()for the redactor half, then discarded them before building the view half. It now builds both halves from every loaded row.delivery.TargetViewgainsDeleted(fromDeletedAt.Valid) andDisplayName(), which rendersname (deleted). The bare name is not restored: an operator debugging an old delivery needs to know the target is gone, not just what it was called.Namestays the stored name.templates/source_logs.htmlrendersDisplayNameat both places a delivery's target is named (the collapsed status line and the deliveries block heading).Masking is unchanged
Views still come from
NewTargetViews, so a deleted target's configuration is masked by exactly the code that masks a live one's —TargetViewhas no raw config field, and nothing else was widened.TestHandleSourceLogs_MasksDeletedTargetConfigstores a Slack webhook URL on a target, deletes the target through the real delete handler, and asserts neither the credential path norwebhookUrlappears in the rendered page while the name does.Scope of the widening — what I audited
The map is historical display only. Its view half reaches nothing but
DeliveryView.Targeton the event log page. Everything that could deliver to or re-activate a target resolves its own targets and is untouched:loadActiveTargets(internal/handlers/webhook.go), scoped andactive = true.loadTargetMap(internal/delivery/engine.go): scoped. No file underinternal/deliveryother than the display projectiontarget_config_view.gois touched, so nothing here anticipates #107.replayTarget(internal/handlers/delivery_replay.go) does its ownUnscoped()lookup and still refuses a deleted target with its own banner.source_management.go:452) and the target edit form (ownedTarget): scoped, so a deleted target stays off both. Confirmed live below.Other views that show a target name against historical data
Searched every template and handler.
templates/source_logs.htmlis the only place a name renders against delivery history, and both of its occurrences are fixed.target_edit.htmlrenders a live target only. The replay and resubmit banners name no target. There is no JSON/API surface that projects a target name. Atarget_idnaming no row at all — deleted or otherwise — still renders blank, but no code path hard deletes a target, so that state is not reachable through the app.Verification
make checkgreen,GOFLAGS=-count=1, lint ran uncached in Docker ([lint 3/3] RUN golangci-lint run—0 issues, 48s, notCACHED); all 20 packagesok.Both new handler tests were confirmed failing on unmodified
nextfirst: the rendered page contained: deliveredand<span class="text-sm text-gray-700"></span>.Live before/after on a running instance, same script against a build of unmodified
nextand a build of this branch: create alogtarget namedNAMED-TARGET-211, POST a real event through the entrypoint, view the event log, delete the target through the UI, view it again.Unmodified
next, after the delete:This branch, after the same delete:
and on the source detail page after the delete,
occurrences of NAMED-TARGET-211 on detail page: 0— the deleted target does not come back into the live target list.Tests added:
TestHandleSourceLogs_NamesDeletedTarget(before/after control on one source, deleting through the real handler),TestHandleSourceLogs_MasksDeletedTargetConfig,TestNewTargetViews_DeletedTarget, plus live-targetDeleted/DisplayNameassertions folded intoTestNewTargetViews_Slackso the marker cannot reach a name that still exists.TODO.mddeliberately untouched per #112.PASS.
Checked by execution from a clean clone: the blanking reproduces on the parent commit and is fixed at both render sites (marker appears exactly twice, matching the live control); no credential (Slack webhook URL, HTTP destination URL,
Authorization: Bearerheader) reaches the event log or the source detail page for a deleted target, and its masked rendering is identical to a live target's; the redactor half is unchanged and still redacts a deleted target's stored response body and error; the deleted target stays refused by the source detail list, the edit form (404), toggle, replay (?replay=target-deleted, no new delivery queued) and both fan-outs; a delete mid-retry rendersname (deleted)withretryingand does not panic; both new handler tests fail on the parent. CI green on2729155;make checkgreen withGOFLAGS=-count=1(88s, 20/20 packagesok, zero(cached), lint uncached in Docker,0 issues); mergeable withnext.Two points worth recording rather than confirming:
deleteWebhookResourcessoft-deletes targets while destroying the whole per-webhook event database, so no delivery survives to be orphaned; and theDELETE FROM targetsininternal/database/event_db_isolation.goclears a stray legacy table inside the event database, not the config database. Unreachable as claimed.templates/source_logs.htmlreads only.Target.DisplayName, so the widened view'sConfignever reaches a template at all. TheUnscoped()load itself was already present on the parent, so the query was not widened — only which loaded rows get projected.Disclosures: no live HTTP instance was stood up; the harness drove the real handlers and real templates in-process, which is the same rendering path. Anomaly, not a defect: the landing commit is authored
sneak <sneak@sneak.berlin>where neighbouring commits areclawbot.