check / check (push) Waiting to run
Each row of the recent events on the webhook page links to the
event's own page, /hook/{id}/events/{eventID}, and expands to show its
body; only the newest starts expanded. The event's page shows its
details, its whole body and every delivery with its attempts.
One renderer, newBodyView with templates/event_body.html, shows a body
in all three places: whole up to 32 KiB, cut there in the lists with a
link to the event's page, JSON pretty-printed, a body of more than 200
lines or 32 KiB in a scrolling box, and a body that is not text left
out beside its download link. A resubmitted copy links to its
original's page.
Model: opus-5-5
118 lines
3.5 KiB
Go
118 lines
3.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// eventLogColumns is the event log's projection. The casts to
|
|
// blob are load-bearing: they make substr and length count
|
|
// bytes rather than characters, so the cap bounds the page in
|
|
// bytes whatever the payload's encoding. Cutting in SQLite
|
|
// rather than in Go is the point of the projection — an
|
|
// oversized body never becomes a Go string at all.
|
|
const eventLogColumns = "id, created_at, method, content_type, " +
|
|
"resubmitted_from_id, " +
|
|
"substr(cast(body as blob), 1, ?) AS body, " +
|
|
"length(cast(body as blob)) AS body_bytes"
|
|
|
|
// eventColumns is eventLogColumns for the event's own page, which
|
|
// shows the whole body.
|
|
const eventColumns = "id, created_at, method, content_type, " +
|
|
"resubmitted_from_id, " +
|
|
"cast(body as blob) AS body, " +
|
|
"length(cast(body as blob)) AS body_bytes"
|
|
|
|
// EventLogView is the display-safe projection of an event for
|
|
// the event log page and the event's own page, alongside
|
|
// DeliveryView and TargetView.
|
|
type EventLogView struct {
|
|
ID string
|
|
CreatedAt time.Time
|
|
Method string
|
|
ContentType string
|
|
|
|
Body BodyView
|
|
|
|
// ResubmittedFromID names the event this one was copied
|
|
// from, empty for an event that arrived on the receiver.
|
|
ResubmittedFromID string
|
|
|
|
// ResubmitCount is how many events have been resubmitted
|
|
// from this one. Both directions are shown, because after
|
|
// a few resubmits of one captured event the log is
|
|
// otherwise a row of identical bodies with nothing saying
|
|
// which came from which.
|
|
ResubmitCount int
|
|
|
|
Deliveries []DeliveryView
|
|
}
|
|
|
|
// ResubmittedFrom reports that this event is a copy of another.
|
|
func (v EventLogView) ResubmittedFrom() bool {
|
|
return v.ResubmittedFromID != ""
|
|
}
|
|
|
|
// eventLogRow is one row of the event log projection, or of
|
|
// eventColumns. In the event log its body column arrives
|
|
// already cut to the cap by SQLite, with the true size beside
|
|
// it.
|
|
type eventLogRow struct {
|
|
ID string
|
|
CreatedAt time.Time
|
|
Method string
|
|
ContentType string
|
|
ResubmittedFromID *string
|
|
Body []byte
|
|
BodyBytes int64
|
|
}
|
|
|
|
// view projects a loaded row of the webhook's events for
|
|
// rendering.
|
|
func (r *eventLogRow) view(webhookID string) EventLogView {
|
|
var from string
|
|
if r.ResubmittedFromID != nil {
|
|
from = *r.ResubmittedFromID
|
|
}
|
|
|
|
return EventLogView{
|
|
ID: r.ID,
|
|
CreatedAt: r.CreatedAt,
|
|
Method: r.Method,
|
|
ContentType: r.ContentType,
|
|
Body: newBodyView(
|
|
"/hook/"+webhookID+"/events/"+r.ID, r.Body, r.BodyBytes,
|
|
),
|
|
ResubmittedFromID: from,
|
|
}
|
|
}
|
|
|
|
// trimPartialRune drops a trailing UTF-8 sequence that the
|
|
// byte-wise cut left incomplete, so a multi-byte rune severed
|
|
// at the cap does not surface as a mojibake tail.
|
|
//
|
|
// Bytes that are merely invalid UTF-8 are left exactly as
|
|
// stored: this service receives binary payloads, and rewriting
|
|
// them would misreport what was delivered. The distinction is
|
|
// utf8.FullRune's — it reports a complete sequence for an
|
|
// invalid encoding too, since that decodes to a width-1 error
|
|
// rune, so only a valid prefix still waiting for its
|
|
// continuation bytes is removed. A tail with no rune start in
|
|
// its last utf8.UTFMax bytes cannot be an incomplete sequence
|
|
// either, and is likewise left alone.
|
|
func trimPartialRune(b []byte) []byte {
|
|
for i := len(b) - 1; i >= 0 && len(b)-i <= utf8.UTFMax; i-- {
|
|
if !utf8.RuneStart(b[i]) {
|
|
continue
|
|
}
|
|
|
|
if utf8.FullRune(b[i:]) {
|
|
return b
|
|
}
|
|
|
|
return b[:i]
|
|
}
|
|
|
|
return b
|
|
}
|