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

@@ -23,21 +23,51 @@ type ConfigField struct {
Value string
}
// deletedNameSuffix marks the name of a target that no longer
// exists. Deletes are soft and delivery history outlives the
// target, so the event log shows names of targets that are gone;
// an operator reading one needs to know it cannot be delivered
// to, replayed to, or configured.
const deletedNameSuffix = " (deleted)"
// TargetView is the display-safe projection of a target for
// the UI. It deliberately has no raw configuration field, so
// no template — present or future — can render the stored
// blob.
type TargetView struct {
ID string
Name string
ID string
Name string
// Deleted reports that this target's row is soft deleted.
// Only views built for historical display carry it set:
// every other projection is of a live row.
Deleted bool
Type database.TargetType
Active bool
Config []ConfigField
}
// DisplayName is the name to render, marked when the target has
// been deleted. Templates showing a name against historical data
// must use it rather than Name, which stays the stored name.
func (v TargetView) DisplayName() string {
if v.Deleted {
return v.Name + deletedNameSuffix
}
return v.Name
}
// NewTargetViews projects targets for rendering, replacing
// each stored configuration blob with named, display-safe
// fields.
//
// A soft-deleted row projects exactly as a live one does, minus
// the deleted marker on its name: masking is a property of the
// projection, not of the row's state, so a deleted target's
// credential is as unreachable from a template as a live
// target's.
func NewTargetViews(
targets []database.Target,
) []TargetView {
@@ -47,11 +77,12 @@ func NewTargetViews(
t := &targets[i]
views = append(views, TargetView{
ID: t.ID,
Name: t.Name,
Type: t.Type,
Active: t.Active,
Config: targetConfigFields(t),
ID: t.ID,
Name: t.Name,
Deleted: t.DeletedAt.Valid,
Type: t.Type,
Active: t.Active,
Config: targetConfigFields(t),
})
}