All checks were successful
check / check (push) Successful in 2m55s
delivery_results stored status_code, response_body, error, duration and attempt_num, and no template rendered any of it, so a failure read as "target: failed" and diagnosing it meant opening the per-webhook SQLite file by hand. An expanded delivery now lists its attempts with attempt number, status code, duration, error and response body. The body is bounded in the query rather than read whole and truncated in Go (#135), and a body the engine itself cut is no longer presented as complete. The response body and error are untrusted remote content, so target credentials are removed before rendering. Two cases needed care: a secret severed by the 4096-byte cut matches nothing as a whole string, and the engine's io.LimitReader cuts at the same constant the renderer uses, so the guard keys on the body reaching the cap rather than on the stored size exceeding it. Empty secrets are filtered where the secret list is built, because an empty string passed to strings.ReplaceAll inserts the marker at every byte boundary. loadTargetMap builds the redactor half unscoped, so a soft-deleted target's historical deliveries still render redacted. Also regenerates static/css/tailwind.css, which had drifted from the templates: hover:text-red-700, text-red-500, underline and w-28 were in use but absent from the served stylesheet (#236).
131 lines
8.6 KiB
HTML
131 lines
8.6 KiB
HTML
{{template "base" .}}
|
|
|
|
{{define "title"}}Event Log - {{.Webhook.Name}} - Webhooker{{end}}
|
|
|
|
{{define "content"}}
|
|
<div class="max-w-6xl mx-auto px-6 py-8">
|
|
<div class="mb-6">
|
|
<a href="/source/{{.Webhook.ID}}" class="text-sm text-primary-600 hover:text-primary-700">← Back to {{.Webhook.Name}}</a>
|
|
<div class="flex justify-between items-center mt-2">
|
|
<h1 class="text-2xl font-medium text-gray-900">Event Log</h1>
|
|
<span class="text-sm text-gray-500">{{.TotalEvents}} total event{{if ne .TotalEvents 1}}s{{end}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{{if .ReplayMessage}}
|
|
<div class="{{if .ReplayQueued}}alert-success{{else}}alert-error{{end}}">{{.ReplayMessage}}</div>
|
|
{{end}}
|
|
|
|
<div class="card">
|
|
<div class="divide-y divide-gray-100">
|
|
{{range .Events}}
|
|
<div class="p-4" x-data="{ open: false }">
|
|
<div class="flex items-center justify-between cursor-pointer" @click="open = !open">
|
|
<div class="flex items-center gap-3">
|
|
<span class="badge-info">{{.Method}}</span>
|
|
<span class="text-sm font-mono text-gray-700">{{.ID}}</span>
|
|
<span class="text-sm text-gray-500">{{.ContentType}}</span>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
{{range .Deliveries}}
|
|
<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}}">
|
|
{{.Target.Name}}: {{.Status}}
|
|
</span>
|
|
{{end}}
|
|
<span class="text-xs text-gray-400">{{.CreatedAt.Format "2006-01-02 15:04:05"}}</span>
|
|
<svg class="w-4 h-4 text-gray-400 transition-transform" :class="{ 'rotate-180': open }" 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>
|
|
</div>
|
|
</div>
|
|
|
|
<div x-show="open" x-cloak class="mt-3 p-3 bg-gray-50 rounded-md">
|
|
<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="/source/{{$.Webhook.ID}}/logs/{{.ID}}/body" class="text-primary-600 hover:text-primary-700 underline">download the full body</a>.</p>
|
|
{{end}}
|
|
|
|
{{if .Deliveries}}
|
|
<div class="mt-4 border-t border-gray-200 pt-3">
|
|
<h3 class="text-xs font-medium uppercase tracking-wide text-gray-500">Deliveries</h3>
|
|
<div class="mt-2 divide-y divide-gray-200">
|
|
{{range .Deliveries}}
|
|
<div class="py-2" x-data="{ attempts: false }">
|
|
<div class="flex items-center justify-between cursor-pointer" @click="attempts = !attempts">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm text-gray-700">{{.Target.Name}}</span>
|
|
<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>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
{{if .Status.Terminal}}
|
|
<form method="POST" action="/source/{{$.Webhook.ID}}/deliveries/{{.ID}}/replay" class="inline" @click.stop>
|
|
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
|
<input type="hidden" name="page" value="{{$.Page}}">
|
|
<button type="submit" class="text-xs text-primary-600 hover:text-primary-700" title="Send this event to the target again">Replay</button>
|
|
</form>
|
|
{{end}}
|
|
<span class="text-xs text-gray-400">{{.AttemptCount}} attempt{{if ne .AttemptCount 1}}s{{end}}</span>
|
|
<svg class="w-3 h-3 text-gray-400 transition-transform" :class="{ 'rotate-180': attempts }" 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>
|
|
</div>
|
|
</div>
|
|
|
|
<div x-show="attempts" 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}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{else}}
|
|
<div class="p-12 text-center text-sm text-gray-500">No events recorded yet.</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{{if or .HasPrev .HasNext}}
|
|
<div class="flex justify-center gap-2 mt-6">
|
|
{{if .HasPrev}}
|
|
<a href="/source/{{.Webhook.ID}}/logs?page={{.PrevPage}}" class="btn-secondary text-sm">← Previous</a>
|
|
{{end}}
|
|
<span class="inline-flex items-center px-4 py-2 text-sm text-gray-500">Page {{.Page}} of {{.TotalPages}}</span>
|
|
{{if .HasNext}}
|
|
<a href="/source/{{.Webhook.ID}}/logs?page={{.NextPage}}" class="btn-secondary text-sm">Next →</a>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|