package store import ( "fmt" ) // WebhookDelivery is one persisted inbound webhook hit. Recorded after the // handler decides what to do so the row reflects the final outcome. type WebhookDelivery struct { ID int64 `json:"id"` TargetType string `json:"target_type"` // "trigger" today; legacy rows may carry "project" or "site" TargetID string `json:"target_id"` TargetName string `json:"target_name"` ReceivedAt string `json:"received_at"` SourceIP string `json:"source_ip"` SignatureState string `json:"signature_state"` // "valid" / "invalid" / "missing" / "unconfigured" StatusCode int `json:"status_code"` Outcome string `json:"outcome"` // "deploy" / "skip" / "rejected" / etc. Detail string `json:"detail"` BodySize int `json:"body_size"` } // InsertWebhookDelivery persists a single webhook delivery record. Best-effort // — failures here must not block the actual webhook handler, so callers // should log and continue rather than propagate. func (s *Store) InsertWebhookDelivery(d WebhookDelivery) error { _, err := s.db.Exec( `INSERT INTO webhook_deliveries (target_type, target_id, target_name, source_ip, signature_state, status_code, outcome, detail, body_size) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, d.TargetType, d.TargetID, d.TargetName, d.SourceIP, d.SignatureState, d.StatusCode, d.Outcome, d.Detail, d.BodySize, ) if err != nil { return fmt.Errorf("insert webhook delivery: %w", err) } return nil } // ListWebhookDeliveriesByTarget returns the most recent N deliveries for // a specific target. Used by the trigger detail panel after the legacy // project / site detail pages were removed. func (s *Store) ListWebhookDeliveriesByTarget(targetType, targetID string, limit int) ([]WebhookDelivery, error) { if limit <= 0 || limit > 200 { limit = 50 } rows, err := s.db.Query( `SELECT id, target_type, target_id, target_name, received_at, source_ip, signature_state, status_code, outcome, detail, body_size FROM webhook_deliveries WHERE target_type = ? AND target_id = ? ORDER BY id DESC LIMIT ?`, targetType, targetID, limit, ) if err != nil { return nil, fmt.Errorf("query webhook deliveries: %w", err) } defer rows.Close() out := []WebhookDelivery{} for rows.Next() { var d WebhookDelivery if err := rows.Scan(&d.ID, &d.TargetType, &d.TargetID, &d.TargetName, &d.ReceivedAt, &d.SourceIP, &d.SignatureState, &d.StatusCode, &d.Outcome, &d.Detail, &d.BodySize); err != nil { return nil, fmt.Errorf("scan webhook delivery: %w", err) } out = append(out, d) } return out, rows.Err() } // PruneWebhookDeliveriesBefore deletes rows older than the given timestamp. // Returns the number of rows removed. Run on the same daily cron as the // event log to keep the table from growing without bound. func (s *Store) PruneWebhookDeliveriesBefore(beforeTS string) (int64, error) { res, err := s.db.Exec(`DELETE FROM webhook_deliveries WHERE received_at < ?`, beforeTS) if err != nil { return 0, fmt.Errorf("prune webhook deliveries: %w", err) } n, _ := res.RowsAffected() return n, nil }