Name a deleted target on its historical deliveries (closes #211)
All checks were successful
check / check (push) Successful in 3m8s

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.
This commit is contained in:
2026-08-23 23:49:34 +00:00
parent fd5966f807
commit 2729155f9b
5 changed files with 256 additions and 31 deletions

View File

@@ -860,11 +860,16 @@ func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
//
// The load is Unscoped because deleting a target only soft
// deletes the row while its deliveries survive in the
// per-webhook database: a scoped load leaves those deliveries
// with a zero redactor, which renders their response bodies
// unredacted. Only the redactor half of the map is built from
// deleted rows. The view half, which is what the page lists,
// stays scoped.
// per-webhook database. Both halves of the map need those rows:
// a scoped load leaves an old delivery with a zero redactor,
// which renders its response bodies unredacted, and with a zero
// view, which renders its target as a blank name.
//
// This map is historical display only. It is built for the event
// log page and reaches nothing but DeliveryView.Target: the
// target list on the source detail page, the edit form and the
// replay path each resolve targets themselves, and a deleted row
// is refused there as before.
func (h *Handlers) loadTargetMap(
webhookID string,
) (map[string]eventLogTarget, error) {
@@ -880,21 +885,18 @@ func (h *Handlers) loadTargetMap(
targetMap := make(
map[string]eventLogTarget, len(targets),
)
live := make([]database.Target, 0, len(targets))
for i := range targets {
targetMap[targets[i].ID] = eventLogTarget{
Redactor: delivery.NewRedactor(&targets[i]),
}
if !targets[i].DeletedAt.Valid {
live = append(live, targets[i])
}
}
// The views come from NewTargetViews rather than being
// rebuilt here, so the masking rules stay in one place.
for _, v := range delivery.NewTargetViews(live) {
// rebuilt here, so the masking rules stay in one place and a
// deleted target's configuration is masked by the same code
// that masks a live one's.
for _, v := range delivery.NewTargetViews(targets) {
entry := targetMap[v.ID]
entry.View = v
targetMap[v.ID] = entry