Add web auth, infinite-scroll, agent admission and review fixes across agent/server/web

This commit is contained in:
Stanislav Rossovskii
2026-05-28 23:45:12 +04:00
parent 37b1a1d6d6
commit 1026e9ebbe
75 changed files with 3021 additions and 916 deletions

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -15,6 +16,7 @@ import (
const (
MaxBatchEvents = 200
MaxBatchBytes = 1 << 20
)
type Client struct {
@@ -68,18 +70,81 @@ func (c *Client) SendEvents(ctx context.Context, events []scheduler.Event) (*Eve
return &EventBatchResponse{}, nil
}
if len(events) > MaxBatchEvents {
return nil, fmt.Errorf("batch exceeds %d events", MaxBatchEvents)
return nil, nonRetryablef("batch exceeds %d events", MaxBatchEvents)
}
wireEvents := make([]scheduler.Event, len(events))
for i, ev := range events {
wireEvents[i] = ev.WireEvent()
}
req := EventBatchRequest{AgentID: c.agentID, Events: wireEvents}
var resp EventBatchResponse
if err := c.post(ctx, "/api/v1/events", req, &resp); err != nil {
batches, err := c.splitEventBatches(req.Events)
if err != nil {
return nil, err
}
return &resp, nil
var total EventBatchResponse
for _, batch := range batches {
var resp EventBatchResponse
if err := c.post(ctx, "/api/v1/events", EventBatchRequest{AgentID: c.agentID, Events: batch}, &resp); err != nil {
return nil, err
}
total.Accepted += resp.Accepted
total.Deduplicated += resp.Deduplicated
}
return &total, nil
}
func (c *Client) splitEventBatches(events []scheduler.Event) ([][]scheduler.Event, error) {
overhead, err := c.eventBatchOverhead()
if err != nil {
return nil, err
}
batches := make([][]scheduler.Event, 0, 1)
current := make([]scheduler.Event, 0, len(events))
currentSize := overhead
for _, ev := range events {
eventSize, err := jsonSize(ev)
if err != nil {
return nil, err
}
nextSize := currentSize + eventSize
if len(current) > 0 {
nextSize++
}
if nextSize <= MaxBatchBytes {
current = append(current, ev)
currentSize = nextSize
continue
}
if len(current) == 0 {
return nil, nonRetryablef("single event batch exceeds %d bytes", MaxBatchBytes)
}
batches = append(batches, current)
current = []scheduler.Event{ev}
currentSize = overhead + eventSize
if currentSize > MaxBatchBytes {
return nil, nonRetryablef("single event batch exceeds %d bytes", MaxBatchBytes)
}
}
if len(current) > 0 {
batches = append(batches, current)
}
return batches, nil
}
func (c *Client) eventBatchOverhead() (int, error) {
size, err := jsonSize(EventBatchRequest{AgentID: c.agentID, Events: []scheduler.Event{}})
if err != nil {
return 0, err
}
return size, nil
}
func jsonSize(v any) (int, error) {
b, err := json.Marshal(v)
if err != nil {
return 0, err
}
return len(b), nil
}
func (c *Client) post(ctx context.Context, path string, body any, out any) error {
@@ -115,30 +180,28 @@ type HTTPError struct {
func (e *HTTPError) Error() string { return fmt.Sprintf("http %d: %s", e.Status, e.Body) }
type NonRetryableError struct{ Err error }
func (e *NonRetryableError) Error() string { return e.Err.Error() }
func (e *NonRetryableError) Unwrap() error { return e.Err }
func nonRetryablef(format string, args ...any) error {
return &NonRetryableError{Err: fmt.Errorf(format, args...)}
}
// Retryable reports whether the error should trigger a retry (network errors
// and 5xx are retryable; 4xx are dropped to avoid hot-loop on bad payload).
func Retryable(err error) bool {
if err == nil {
return false
}
var ne *NonRetryableError
if errors.As(err, &ne) {
return false
}
var he *HTTPError
if asErr(err, &he) {
if errors.As(err, &he) {
return he.Status >= 500
}
return true
}
func asErr(err error, target **HTTPError) bool {
for e := err; e != nil; {
if h, ok := e.(*HTTPError); ok {
*target = h
return true
}
u, ok := e.(interface{ Unwrap() error })
if !ok {
return false
}
e = u.Unwrap()
}
return false
}