audit: fix issues #1-#9 (mock data, notifier interface, retry/timeout, digest scheduling, real status, RPC retry, dedup, address edit UI)

- #1: remove mock alert events that poisoned production responses
- #2: add Notifier interface + Send implementations for all channels
- #3/#4: notification goroutines now carry a 30s context timeout and retry up to 3x with exponential backoff
- #5: schedule email digest in poller via DIGEST_INTERVAL_HOURS (default 24h)
- #6: SystemStatus queries real checkpoint data; returns starting/active/idle with latestBlock and lag
- #7: Ethereum RPC client retries on network errors, 429, and 5xx with 1s/2s backoff
- #8: alert_events dedup index + ON CONFLICT DO NOTHING to prevent duplicate events on poller restart
- #9: PATCH /v1/addresses/{id} for label editing; frontend address list gains inline edit and remove buttons
This commit is contained in:
KS Jannette
2026-03-01 07:49:08 -05:00
parent c80ed89c0a
commit 37ff644aa7
19 changed files with 530 additions and 122 deletions

View File

@@ -1,10 +1,67 @@
package handlers
import (
"log"
"net/http"
"time"
"github.com/kjannette/koin-ping/backend-go/internal/models"
)
// StatusHandler handles the system status endpoint.
type StatusHandler struct {
checkpoints *models.CheckpointModel
}
// NewStatusHandler creates a new StatusHandler.
func NewStatusHandler(checkpoints *models.CheckpointModel) *StatusHandler {
return &StatusHandler{checkpoints: checkpoints}
}
// GetStatus returns real-time system status derived from checkpoint data.
func (h *StatusHandler) GetStatus(w http.ResponseWriter, r *http.Request) {
block, checkedAt, err := h.checkpoints.GetLatestBlock(r.Context())
if err != nil {
log.Printf("Error querying latest block: %v", err)
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to get system status")
return
}
latestBlock := 0
lag := 0
status := "starting"
if checkedAt != nil {
lag = int(time.Since(*checkedAt).Seconds())
if lag > 600 { //nolint:mnd
status = "idle"
} else {
status = "active"
}
}
if block != nil {
latestBlock = *block
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": status,
"latestBlock": latestBlock,
"lag": lag,
"lastCheckedAt": checkedAtStr(checkedAt),
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}
func checkedAtStr(t *time.Time) string {
if t == nil {
return ""
}
return t.UTC().Format(time.RFC3339)
}
func HealthCheck(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "ok",
@@ -12,12 +69,3 @@ func HealthCheck(w http.ResponseWriter, r *http.Request) {
"service": "koin-ping-backend",
})
}
func SystemStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]interface{}{
"latestBlock": 0,
"lag": 0,
"status": "healthy",
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}