feat(receiver): storage layer and webhook payload compat

Dual-dialect store (SQLite via modernc.org, MySQL via go-sql-driver,
both pure Go) with order-tolerant start/complete upserts, filtered and
paginated listing, and aggregate queries (per-day, top domains, query
types, statuses, duration percentiles, top clients). Sender gains
optional EXPLOREDNS_WEBHOOK_TOKEN bearer auth; a round-trip test pins
receiver structs byte-compatible with the sender payloads.

Note: go directive moves to 1.25.0, required by modernc.org/sqlite.
CI reads the version from go.mod so GOTOOLCHAIN=local stays satisfied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gary Hansen
2026-07-08 02:43:50 +10:00
co-authored by Claude Fable 5
parent d96f25b1d6
commit e8f56a1ef4
11 changed files with 1468 additions and 23 deletions
+39
View File
@@ -0,0 +1,39 @@
package store
import (
"encoding/json"
"time"
)
// StartEvent and CompleteEvent are the receiver-side decodings of the
// webhook payloads posted by the main app. Field names and JSON tags must
// stay in sync with webhookStartEvent/webhookCompleteEvent in
// web/api/webhook.go; a compatibility test in web/api enforces this.
// StartEvent mirrors web/api webhookStartEvent.
type StartEvent struct {
Event string `json:"event"`
ID string `json:"id"`
Domain string `json:"domain"`
QueryType string `json:"query_type"`
AllRoots bool `json:"all_roots"`
ClientIP string `json:"client_ip"`
StartedAt time.Time `json:"started_at"`
}
// CompleteEvent mirrors web/api webhookCompleteEvent. Summary is kept as
// raw JSON: the receiver stores it verbatim and never interprets it.
type CompleteEvent struct {
Event string `json:"event"`
ID string `json:"id"`
Domain string `json:"domain"`
QueryType string `json:"query_type"`
ClientIP string `json:"client_ip"`
StartedAt time.Time `json:"started_at"`
DoneAt time.Time `json:"done_at"`
DurationMS int64 `json:"duration_ms"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
ResultCount int `json:"result_count"`
Summary json.RawMessage `json:"summary"`
}