Files
webhooker/templates/sources_list.html
clawbot 855b9de866
All checks were successful
check / check (push) Successful in 3m4s
Allow retention_days of 0 to mean retain forever (closes #79)
RetentionDays carried gorm:"default:30", so GORM substituted 30 for a
zero value while building the insert. A webhook could therefore never
be configured to keep its events indefinitely: the reaper's
retain-forever branch existed but was unreachable from the normal
create and edit flows.

Introduce database.RetentionForeverDays = 365 * 1000 as the sentinel
for "retain forever" and a Webhook.BeforeSave hook that rewrites any
non-positive RetentionDays to it. The rewrite has to live in the hook
rather than at the call sites: GORM applies the column default while
converting the model to insert values, which happens after BeforeSave,
so anything later loses that race. Putting it on the model also means
a future call site, such as the planned REST API, cannot bypass it.

The reaper now skips a webhook when Webhook.RetainsForever reports
true, which recognises the sentinel and keeps honouring the old <= 0
values for rows written before it existed. Without this the sentinel,
being positive, would have produced a cutoff a thousand years in the
past and a DELETE matching nothing on every sweep.

Form handling is shared by create and edit through parseRetentionDays
so the two cannot drift: an empty field keeps the previous behaviour
(default on create, unchanged on edit), 0 is honoured, and an
unparseable or negative value is a 400 that re-renders the form rather
than a silently substituted default.

The retention inputs drop max="365". That cap was not cosmetic: the
edit form pre-fills the stored value, so a retain-forever webhook
rendered 365000 into an input capped at 365 and browser validation
would have blocked saving any edit to it. min becomes 0 with a hint
explaining what 0 does, and the list and detail views render a
RetentionLabel of "forever" instead of a raw day count.

The 30-day default is consolidated into database.DefaultRetentionDays,
referenced from the handler and from the create form's pre-filled
value, with a test asserting it agrees with the struct tag that cannot
reference it.
2026-08-09 02:32:34 +00:00

50 lines
2.2 KiB
HTML

{{template "base" .}}
{{define "title"}}Sources - Webhooker{{end}}
{{define "content"}}
<div class="max-w-6xl mx-auto px-6 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-medium text-gray-900">Webhooks</h1>
<a href="/sources/new" class="btn-primary">
<svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
New Webhook
</a>
</div>
{{if .Webhooks}}
<div class="grid gap-4">
{{range .Webhooks}}
<a href="/source/{{.ID}}" class="card-elevated p-6 block">
<div class="flex justify-between items-start">
<div>
<h2 class="text-lg font-medium text-gray-900">{{.Name}}</h2>
{{if .Description}}
<p class="text-sm text-gray-500 mt-1">{{.Description}}</p>
{{end}}
</div>
<span class="badge-info">Retention: {{.RetentionLabel}}</span>
</div>
<div class="flex gap-6 mt-4 text-sm text-gray-500">
<span>{{.EntrypointCount}} entrypoint{{if ne .EntrypointCount 1}}s{{end}}</span>
<span>{{.TargetCount}} target{{if ne .TargetCount 1}}s{{end}}</span>
<span>{{.EventCount}} event{{if ne .EventCount 1}}s{{end}}</span>
</div>
</a>
{{end}}
</div>
{{else}}
<div class="card p-12 text-center">
<svg class="w-16 h-16 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
</svg>
<h2 class="text-lg font-medium text-gray-900 mb-2">No webhooks yet</h2>
<p class="text-gray-500 mb-6">Create your first webhook to start receiving and forwarding events.</p>
<a href="/sources/new" class="btn-primary">Create Webhook</a>
</div>
{{end}}
</div>
{{end}}