going full courtside
This commit is contained in:
@@ -4,9 +4,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/domain"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/middleware"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/models"
|
||||
)
|
||||
@@ -46,44 +44,5 @@ func (h *AlertEventHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.Printf("Found %d alert events for user", len(events))
|
||||
|
||||
// MVP scaffolding: return mock data if DB is empty
|
||||
if len(events) == 0 {
|
||||
events = mockEvents(limit)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, events)
|
||||
}
|
||||
|
||||
func mockEvents(limit int) []domain.AlertEvent {
|
||||
label1 := "Treasury Wallet"
|
||||
label2 := "Cold Storage"
|
||||
|
||||
mocks := []domain.AlertEvent{
|
||||
{
|
||||
ID: 1,
|
||||
AlertRuleID: 1,
|
||||
Message: "Incoming transaction detected: 5.5 ETH received",
|
||||
AddressLabel: &label1,
|
||||
Timestamp: time.Now().Add(-2 * time.Hour),
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
AlertRuleID: 2,
|
||||
Message: "Balance dropped below threshold: Current balance 8.2 ETH",
|
||||
AddressLabel: &label1,
|
||||
Timestamp: time.Now().Add(-5 * time.Hour),
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
AlertRuleID: 3,
|
||||
Message: "Outgoing transaction detected: 2.0 ETH sent",
|
||||
AddressLabel: &label2,
|
||||
Timestamp: time.Now().Add(-24 * time.Hour),
|
||||
},
|
||||
}
|
||||
|
||||
if limit < len(mocks) {
|
||||
return mocks[:limit]
|
||||
}
|
||||
return mocks
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/domain"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/middleware"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/models"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/notifications"
|
||||
)
|
||||
|
||||
var emailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
|
||||
@@ -50,7 +51,7 @@ func (h *NotificationConfigHandler) UpdateConfig(w http.ResponseWriter, r *http.
|
||||
|
||||
var body struct {
|
||||
DiscordWebhookURL *string `json:"discord_webhook_url"`
|
||||
TelegramChatID *string `json:"telegram_chat_id"`
|
||||
SlackWebhookURL *string `json:"slack_webhook_url"`
|
||||
Email *string `json:"email"`
|
||||
NotificationEnabled *bool `json:"notification_enabled"`
|
||||
}
|
||||
@@ -62,7 +63,7 @@ func (h *NotificationConfigHandler) UpdateConfig(w http.ResponseWriter, r *http.
|
||||
|
||||
log.Printf("User %s updating notification config", userID)
|
||||
|
||||
if body.DiscordWebhookURL == nil && body.TelegramChatID == nil &&
|
||||
if body.DiscordWebhookURL == nil && body.SlackWebhookURL == nil &&
|
||||
body.Email == nil && body.NotificationEnabled == nil {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR",
|
||||
"At least one configuration field must be provided")
|
||||
@@ -76,6 +77,13 @@ func (h *NotificationConfigHandler) UpdateConfig(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
if body.SlackWebhookURL != nil && *body.SlackWebhookURL != "" &&
|
||||
!strings.HasPrefix(*body.SlackWebhookURL, "https://hooks.slack.com/") {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR",
|
||||
"Invalid Slack webhook URL format")
|
||||
return
|
||||
}
|
||||
|
||||
if body.Email != nil && *body.Email != "" && !emailRe.MatchString(*body.Email) {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR",
|
||||
"Invalid email address format")
|
||||
@@ -89,7 +97,7 @@ func (h *NotificationConfigHandler) UpdateConfig(w http.ResponseWriter, r *http.
|
||||
|
||||
cfg := domain.NotificationConfig{
|
||||
DiscordWebhookURL: body.DiscordWebhookURL,
|
||||
TelegramChatID: body.TelegramChatID,
|
||||
SlackWebhookURL: body.SlackWebhookURL,
|
||||
Email: body.Email,
|
||||
NotificationEnabled: enabled,
|
||||
}
|
||||
@@ -126,3 +134,50 @@ func (h *NotificationConfigHandler) DeleteConfig(w http.ResponseWriter, r *http.
|
||||
log.Println("Notification config deleted")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *NotificationConfigHandler) TestWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
_ = middleware.GetUserID(r.Context())
|
||||
|
||||
var body struct {
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if body.URL == "" {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "URL is required")
|
||||
return
|
||||
}
|
||||
|
||||
var ok bool
|
||||
var err error
|
||||
|
||||
switch body.Type {
|
||||
case "slack":
|
||||
if !strings.HasPrefix(body.URL, "https://hooks.slack.com/") {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid Slack webhook URL")
|
||||
return
|
||||
}
|
||||
ok, err = notifications.TestSlackWebhook(body.URL)
|
||||
case "discord":
|
||||
if !strings.HasPrefix(body.URL, "https://discord.com/api/webhooks/") {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid Discord webhook URL")
|
||||
return
|
||||
}
|
||||
ok, err = notifications.TestDiscordWebhook(body.URL)
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Type must be 'slack' or 'discord'")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Webhook test failed: %v", err)
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"success": ok})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/models"
|
||||
"github.com/kjannette/koin-ping/backend-go/internal/protocols/ethereum"
|
||||
)
|
||||
|
||||
func HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -13,11 +18,62 @@ func HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func SystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
type StatusHandler struct {
|
||||
eth ethereum.EthereumObserver
|
||||
checkpoints *models.CheckpointModel
|
||||
}
|
||||
|
||||
func NewStatusHandler(eth ethereum.EthereumObserver, checkpoints *models.CheckpointModel) *StatusHandler {
|
||||
return &StatusHandler{eth: eth, checkpoints: checkpoints}
|
||||
}
|
||||
|
||||
func (h *StatusHandler) SystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
status := "healthy"
|
||||
|
||||
if h.eth == nil {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"latestBlock": 0,
|
||||
"lag": 0,
|
||||
"trackedAddresses": 0,
|
||||
"status": "no_rpc",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
latestBlock, err := h.eth.GetLatestBlockNumber(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get latest block: %v", err)
|
||||
status = "degraded"
|
||||
latestBlock = 0
|
||||
}
|
||||
|
||||
lag := 0
|
||||
checkpoints, err := h.checkpoints.ListAll(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to list checkpoints: %v", err)
|
||||
} else if len(checkpoints) > 0 && latestBlock > 0 {
|
||||
minChecked := checkpoints[0].LastCheckedBlock
|
||||
for _, cp := range checkpoints[1:] {
|
||||
if cp.LastCheckedBlock < minChecked {
|
||||
minChecked = cp.LastCheckedBlock
|
||||
}
|
||||
}
|
||||
lag = latestBlock - minChecked
|
||||
}
|
||||
|
||||
if lag > 50 {
|
||||
status = "syncing"
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"latestBlock": 0,
|
||||
"lag": 0,
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"latestBlock": latestBlock,
|
||||
"lag": lag,
|
||||
"trackedAddresses": len(checkpoints),
|
||||
"status": status,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user