Offer archive expiry choices on the target forms, show plain units (closes #396)
check / check (push) Waiting to run

A database target's archive expiry was typed by hand as never or a raw duration such as 720h, and the target list showed it back raw. Adding or editing a database target now offers the new-webhook page's list of choices (never, 1h, 12h, 24h, 30d, 90d, 365d), defined once and shared by all three forms. The edit form starts on the stored expiry, or on the submitted one after a refused save; a stored value outside the choices is listed under its own value, so saving unchanged keeps it. The target list shows the expiry in plain units: "30 days", "12 hours", "never".

Model: opus-5-5
This commit was merged in pull request #479.
This commit is contained in:
2026-10-03 01:16:14 +02:00
parent f282c6363d
commit 3489d6909a
15 changed files with 366 additions and 52 deletions
+47 -23
View File
@@ -1,9 +1,9 @@
package delivery
import (
"encoding/json"
"fmt"
"strconv"
"time"
"sneak.berlin/go/webhooker/internal/database"
)
@@ -197,37 +197,61 @@ func maxRetriesField(t *database.Target) ConfigField {
}
}
// databaseConfigFields describes an archive target. Its
// configuration is optional, and an absent or empty expiry
// means the archive is kept forever. An expiry that is set
// but not a valid duration is reported as unavailable rather
// than echoed back.
// databaseConfigFields describes an archive target by its
// expiry in plain units, such as "30 days", or "never" when
// the archive is kept forever. An expiry that is set but not
// a valid duration is reported as unavailable rather than
// echoed back.
func databaseConfigFields(configJSON string) []ConfigField {
expiry := archiveExpiryNever
expiry, err := parseArchiveExpiry(configJSON)
if err != nil {
return unavailableConfigFields()
}
if configJSON != "" {
var cfg databaseTargetConfig
err := json.Unmarshal([]byte(configJSON), &cfg)
if err != nil {
return unavailableConfigFields()
}
if cfg.Expiry != "" {
if ValidateArchiveExpiry(cfg.Expiry) != nil {
return unavailableConfigFields()
}
expiry = cfg.Expiry
}
value := archiveExpiryNever
if expiry > 0 {
value = plainDuration(expiry)
}
return []ConfigField{{
Label: "Archive Expiry",
Value: expiry,
Value: value,
}}
}
// plainDuration writes a positive duration as a count of the
// largest whole unit it divides into: "30 days", "12 hours",
// "1 minute". A duration with a fraction of a second is
// written as Go writes it.
func plainDuration(d time.Duration) string {
const day = 24 * time.Hour
units := []struct {
size time.Duration
name string
}{
{day, "day"},
{time.Hour, "hour"},
{time.Minute, "minute"},
{time.Second, "second"},
}
for _, unit := range units {
if d%unit.size != 0 {
continue
}
count := int64(d / unit.size)
if count == 1 {
return "1 " + unit.name
}
return fmt.Sprintf("%d %ss", count, unit.name)
}
return d.String()
}
// MaskedWebhookURL returns the Slack webhook URL reduced to
// its scheme and host, with the path, query and any userinfo
// elided. The path segments are the credential, so none of