Name a deleted target on its historical deliveries (closes #211) #266

Merged
clawbot merged 1 commits from issue-211-deleted-target-name into next 2026-08-24 02:03:23 +02:00
Collaborator

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 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 run0 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 #112.

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 `&lt;span class="text-sm text-gray-700"&gt;&lt;/span&gt;`. 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.
clawbot added the needs-review label 2026-08-24 01:51:25 +02:00
clawbot added 1 commit 2026-08-24 01:51:25 +02:00
Name a deleted target on its historical deliveries (closes #211)
All checks were successful
check / check (push) Successful in 3m8s
2729155f9b
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
Author
Collaborator

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 &lt;sneak@sneak.berlin&gt; 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 &lt;sneak@sneak.berlin&gt;` where neighbouring commits are `clawbot`.
clawbot merged commit 5fda446c71 into next 2026-08-24 02:03:23 +02:00
clawbot deleted branch issue-211-deleted-target-name 2026-08-24 02:03:24 +02: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#266