Give each event its own page and show bodies the same everywhere (closes #369)
check / check (push) Successful in 3m17s
check / check (push) Successful in 3m17s
Each event now has its own page at /hook/ID/events/EVENTID, behind the login, showing its details, its whole body and every delivery; a resubmitted copy links to its original's page. The recent events on the webhook page link there and expand to show their bodies, only the newest expanded on load. One renderer and one template show a body the same way in the recent events, the event log and the event's page: whole up to 32 KiB, cut there in the two lists with links to the event's page and the download; JSON pretty-printed unless that would grow it past four times plus 1 KiB; over 200 lines in a scrolling box; a body holding NUL or control characters treated as binary and never dumped raw. Model: opus-5-5
This commit was merged in pull request #465.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
{{define "delivery_attempts"}}
|
||||
<!-- A delivery's recorded attempts, as handlers.DeliveryView holds
|
||||
them: in the event log and on an event's own page. -->
|
||||
{{if .AttemptsOmitted}}
|
||||
<p class="text-xs text-gray-500">{{.AttemptsOmitted}} attempt{{if ne .AttemptsOmitted 1}}s{{end}} omitted between the first and last shown.</p>
|
||||
{{end}}
|
||||
{{range .Results}}
|
||||
<div class="rounded-md bg-white border border-gray-200 p-2">
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs">
|
||||
<span class="text-gray-500">Attempt {{.AttemptNum}}</span>
|
||||
<span class="{{if .Success}}text-green-600{{else}}text-red-600{{end}}">{{if .Success}}success{{else}}failure{{end}}</span>
|
||||
<span class="text-gray-500">Status: {{if .HasStatusCode}}{{.StatusCode}}{{else}}— (no response){{end}}</span>
|
||||
<span class="text-gray-500">Duration: {{.DurationMS}} ms</span>
|
||||
</div>
|
||||
{{if .Error}}
|
||||
<p class="mt-2 text-xs text-red-700 break-all">Error: {{.Error}}</p>
|
||||
{{end}}
|
||||
{{if .ResponseBody}}
|
||||
<pre class="mt-2 text-xs text-gray-700 overflow-x-auto whitespace-pre-wrap break-all">{{.ResponseBody}}</pre>
|
||||
{{end}}
|
||||
{{if .ResponseTruncated}}
|
||||
{{if .ResponseSizeKnown}}
|
||||
<p class="mt-1 text-xs text-gray-500">Response truncated for display: showing {{.ResponseShownBytes}} of {{.ResponseBytes}} bytes.</p>
|
||||
{{else}}
|
||||
<p class="mt-1 text-xs text-gray-500">Showing {{.ResponseShownBytes}} of the {{.ResponseBytes}} recorded bytes. The response reached the recording limit, so the remote may have sent more that was never stored.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="text-xs text-gray-500">No attempts recorded yet.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,15 @@
|
||||
{{define "event_body"}}
|
||||
<!-- An event's body, as handlers.BodyView describes it: the same in
|
||||
the recent events on the webhook page, the event log and the
|
||||
event's own page. -->
|
||||
{{if .Binary}}
|
||||
<p class="text-xs text-gray-500">This body is binary ({{.Size}} bytes) and is not shown. <a href="{{.EventURL}}/body" class="btn-small">Download the body</a></p>
|
||||
{{else if .Text}}
|
||||
<pre class="rounded-md border border-gray-200 bg-white p-2 text-xs text-gray-700 overflow-x-auto whitespace-pre-wrap break-all"{{if .Scroll}} style="max-height: 32rem; overflow-y: auto"{{end}}>{{.Text}}</pre>
|
||||
{{if .Cut}}
|
||||
<p class="mt-2 text-xs text-gray-500">Showing the first {{.ShownBytes}} of {{.Size}} bytes, unformatted. <a href="{{.EventURL}}" class="btn-small">Show the whole body</a> <a href="{{.EventURL}}/body" class="btn-small">Download the body</a></p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p class="text-xs text-gray-500">No body.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,87 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Event - {{.Webhook.Name}} - Webhooker{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="max-w-6xl mx-auto px-6 py-8">
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/hook/{{.Webhook.ID}}" class="btn-small">← Back to {{.Webhook.Name}}</a>
|
||||
<a href="/hook/{{.Webhook.ID}}/events" class="btn-small">Full Event Log</a>
|
||||
</div>
|
||||
<h1 class="text-2xl font-medium text-gray-900 mt-2">Event</h1>
|
||||
</div>
|
||||
|
||||
{{with .Event}}
|
||||
<div class="card p-4">
|
||||
<dl class="space-y-2 text-sm">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<dt class="w-32 flex-shrink-0 text-gray-500">ID</dt>
|
||||
<dd class="font-mono text-gray-900 break-all">{{.ID}}</dd>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<dt class="w-32 flex-shrink-0 text-gray-500">Received</dt>
|
||||
<dd class="text-gray-900">{{.CreatedAt.UTC.Format "2006-01-02 15:04:05"}} UTC</dd>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<dt class="w-32 flex-shrink-0 text-gray-500">Method</dt>
|
||||
<dd><span class="badge-info">{{.Method}}</span></dd>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<dt class="w-32 flex-shrink-0 text-gray-500">Content type</dt>
|
||||
<dd class="text-gray-900 break-all">{{.ContentType}}</dd>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<dt class="w-32 flex-shrink-0 text-gray-500">Body size</dt>
|
||||
<dd class="text-gray-900">{{.Body.Size}} bytes</dd>
|
||||
</div>
|
||||
{{if .ResubmittedFrom}}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<dt class="w-32 flex-shrink-0 text-gray-500">Resubmitted from</dt>
|
||||
<dd><a href="/hook/{{$.Webhook.ID}}/events/{{.ResubmittedFromID}}" class="btn-small font-mono">{{.ResubmittedFromID}}</a></dd>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .ResubmitCount}}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<dt class="w-32 flex-shrink-0 text-gray-500">Resubmitted</dt>
|
||||
<dd class="text-gray-900">as {{.ResubmitCount}} new event{{if ne .ResubmitCount 1}}s{{end}}</dd>
|
||||
</div>
|
||||
{{end}}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="card mt-6">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">Body</h2>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
{{template "event_body" .Body}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-6">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">Deliveries</h2>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
{{range .Deliveries}}
|
||||
<div class="p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<span class="text-sm text-gray-700">{{.Target.DisplayName}}</span>
|
||||
<span class="flex flex-wrap items-center gap-3">
|
||||
<span class="text-xs {{if eq .Status "delivered"}}text-green-600{{else if eq .Status "failed"}}text-red-600{{else if eq .Status "retrying"}}text-yellow-600{{else}}text-gray-400{{end}}">{{.Status}}</span>
|
||||
<span class="text-xs text-gray-400">{{.AttemptCount}} attempt{{if ne .AttemptCount 1}}s{{end}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 space-y-2">
|
||||
{{template "delivery_attempts" .}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="p-4 text-sm text-gray-500">No deliveries.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -225,26 +225,37 @@
|
||||
<a href="/hook/{{.Webhook.ID}}/events" class="btn-small">Full Event Log</a>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
{{range .Events}}
|
||||
<div class="p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="badge-info">{{.Method}}</span>
|
||||
<span class="text-sm text-gray-500 break-all">{{.ContentType}}</span>
|
||||
{{if .ResubmittedFromID}}
|
||||
<span class="text-xs text-gray-500" title="This event is a copy of {{.ResubmittedFromID}}">resubmitted copy</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs text-gray-400">
|
||||
<span title="Body size">{{.Size}}</span>
|
||||
{{if .ProcessingTime}}
|
||||
<span title="Processing time: how long the slowest delivery took, from being queued to its last attempt">{{.ProcessingTime}}</span>
|
||||
{{end}}
|
||||
{{if .Status}}
|
||||
<span class="font-medium {{.StatusClass}}" title="HTTP status from the HTTP target">{{.Status}}</span>
|
||||
{{end}}
|
||||
<span title="{{.ReceivedUTC}}">{{.Received}}</span>
|
||||
</div>
|
||||
<!-- Each row expands to show its body; only the newest
|
||||
starts expanded. -->
|
||||
{{range $i, $event := .Events}}
|
||||
<div class="p-4" x-data="collapsible"{{if eq $i 0}} data-open{{end}}>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<button type="button" class="btn-small flex-1 flex-wrap justify-between gap-3 text-left" @click="toggle">
|
||||
<span class="flex flex-wrap items-center gap-3">
|
||||
<span class="badge-info">{{.Method}}</span>
|
||||
<span class="text-sm text-gray-500 break-all">{{.ContentType}}</span>
|
||||
{{if .ResubmittedFromID}}
|
||||
<span class="text-xs text-gray-500" title="This event is a copy of {{.ResubmittedFromID}}">resubmitted copy</span>
|
||||
{{end}}
|
||||
</span>
|
||||
<span class="flex flex-wrap items-center gap-3 text-xs text-gray-400">
|
||||
<span title="Body size">{{.Size}}</span>
|
||||
{{if .ProcessingTime}}
|
||||
<span title="Processing time: how long the slowest delivery took, from being queued to its last attempt">{{.ProcessingTime}}</span>
|
||||
{{end}}
|
||||
{{if .Status}}
|
||||
<span class="font-medium {{.StatusClass}}" title="HTTP status from the HTTP target">{{.Status}}</span>
|
||||
{{end}}
|
||||
<span title="{{.ReceivedUTC}}">{{.Received}}</span>
|
||||
<svg class="w-4 h-4 text-gray-400 transition-transform" :class="caretClass" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<a href="{{.Body.EventURL}}" class="btn-small">Open</a>
|
||||
</div>
|
||||
<div x-show="open" x-cloak class="mt-3">
|
||||
{{template "event_body" .Body}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<div x-show="open" x-cloak class="mt-3 p-3 bg-gray-50 rounded-md">
|
||||
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="text-xs text-gray-500">
|
||||
{{if .ResubmittedFrom}}Resubmitted from event <span class="font-mono">{{.ResubmittedFromID}}</span>.{{end}}
|
||||
{{if .ResubmittedFrom}}Resubmitted from event <a href="/hook/{{$.Webhook.ID}}/events/{{.ResubmittedFromID}}" class="btn-small font-mono">{{.ResubmittedFromID}}</a>.{{end}}
|
||||
{{if .ResubmitCount}}Resubmitted as {{.ResubmitCount}} new event{{if ne .ResubmitCount 1}}s{{end}}.{{end}}
|
||||
</div>
|
||||
<form method="POST" action="/hook/{{$.Webhook.ID}}/events/{{.ID}}/resubmit" class="inline">
|
||||
@@ -53,10 +53,7 @@
|
||||
<button type="submit" class="btn-small" title="Submit this event again as a new event, to every currently active target">Resubmit</button>
|
||||
</form>
|
||||
</div>
|
||||
<pre class="text-xs text-gray-700 overflow-x-auto whitespace-pre-wrap break-all">{{.Body}}</pre>
|
||||
{{if .BodyTruncated}}
|
||||
<p class="mt-2 text-xs text-gray-500">Body truncated for display: showing {{.BodyShownBytes}} of {{.BodyBytes}} bytes. The stored body is unchanged — <a href="/hook/{{$.Webhook.ID}}/events/{{.ID}}/body" class="btn-small">download the full body</a>.</p>
|
||||
{{end}}
|
||||
{{template "event_body" .Body}}
|
||||
|
||||
{{if .Deliveries}}
|
||||
<div class="mt-4 border-t border-gray-200 pt-3">
|
||||
@@ -87,34 +84,7 @@
|
||||
</div>
|
||||
|
||||
<div x-show="open" x-cloak class="mt-2 space-y-2">
|
||||
{{if .AttemptsOmitted}}
|
||||
<p class="text-xs text-gray-500">{{.AttemptsOmitted}} attempt{{if ne .AttemptsOmitted 1}}s{{end}} omitted between the first and last shown.</p>
|
||||
{{end}}
|
||||
{{range .Results}}
|
||||
<div class="rounded-md bg-white border border-gray-200 p-2">
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs">
|
||||
<span class="text-gray-500">Attempt {{.AttemptNum}}</span>
|
||||
<span class="{{if .Success}}text-green-600{{else}}text-red-600{{end}}">{{if .Success}}success{{else}}failure{{end}}</span>
|
||||
<span class="text-gray-500">Status: {{if .HasStatusCode}}{{.StatusCode}}{{else}}— (no response){{end}}</span>
|
||||
<span class="text-gray-500">Duration: {{.DurationMS}} ms</span>
|
||||
</div>
|
||||
{{if .Error}}
|
||||
<p class="mt-2 text-xs text-red-700 break-all">Error: {{.Error}}</p>
|
||||
{{end}}
|
||||
{{if .ResponseBody}}
|
||||
<pre class="mt-2 text-xs text-gray-700 overflow-x-auto whitespace-pre-wrap break-all">{{.ResponseBody}}</pre>
|
||||
{{end}}
|
||||
{{if .ResponseTruncated}}
|
||||
{{if .ResponseSizeKnown}}
|
||||
<p class="mt-1 text-xs text-gray-500">Response truncated for display: showing {{.ResponseShownBytes}} of {{.ResponseBytes}} bytes.</p>
|
||||
{{else}}
|
||||
<p class="mt-1 text-xs text-gray-500">Showing {{.ResponseShownBytes}} of the {{.ResponseBytes}} recorded bytes. The response reached the recording limit, so the remote may have sent more that was never stored.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="text-xs text-gray-500">No attempts recorded yet.</p>
|
||||
{{end}}
|
||||
{{template "delivery_attempts" .}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user