- #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
102 lines
2.5 KiB
Go
102 lines
2.5 KiB
Go
//loads environment-based configuration.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
defaultPort = 3001
|
|
defaultDBPort = 5432
|
|
defaultPollIntervalMS = 60000
|
|
minPollIntervalMS = 1000
|
|
defaultDigestIntervalHours = 24
|
|
)
|
|
|
|
type Config struct {
|
|
Port int
|
|
APIBasePath string
|
|
DatabaseURL string
|
|
DBHost string
|
|
DBPort int
|
|
DBUser string
|
|
DBPassword string
|
|
DBName string
|
|
FirebaseProjectID string
|
|
EthRPCURL string
|
|
PollIntervalMS int
|
|
NodeEnv string
|
|
ResendAPIKey string
|
|
EmailFrom string
|
|
DigestIntervalHours int
|
|
}
|
|
|
|
// Load reads configuration from environment variables and returns a Config.
|
|
func Load() (*Config, error) {
|
|
cfg := &Config{
|
|
Port: getEnvInt("PORT", defaultPort),
|
|
APIBasePath: getEnv("API_BASE_PATH", "/v1"),
|
|
DatabaseURL: os.Getenv("DATABASE_URL"),
|
|
DBHost: getEnv("DB_HOST", "localhost"),
|
|
DBPort: getEnvInt("DB_PORT", defaultDBPort),
|
|
DBUser: os.Getenv("DB_USER"),
|
|
DBPassword: os.Getenv("DB_PASSWORD"),
|
|
DBName: os.Getenv("DB_NAME"),
|
|
FirebaseProjectID: os.Getenv("FIREBASE_PROJECT_ID"),
|
|
EthRPCURL: os.Getenv("ETH_RPC_URL"),
|
|
PollIntervalMS: getEnvInt("POLL_INTERVAL_MS", defaultPollIntervalMS),
|
|
NodeEnv: getEnv("NODE_ENV", "development"),
|
|
ResendAPIKey: os.Getenv("RESEND_API_KEY"),
|
|
EmailFrom: getEnv("EMAIL_FROM", "Koin Ping <alerts@koinping.com>"),
|
|
DigestIntervalHours: getEnvInt("DIGEST_INTERVAL_HOURS", defaultDigestIntervalHours),
|
|
}
|
|
|
|
if cfg.PollIntervalMS < minPollIntervalMS {
|
|
return nil, fmt.Errorf("POLL_INTERVAL_MS must be >= 1000, got %d", cfg.PollIntervalMS) //nolint:err113
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func (c *Config) DSN() string {
|
|
if c.DatabaseURL != "" {
|
|
// Append sslmode=disable if not already specified.
|
|
if !strings.Contains(c.DatabaseURL, "sslmode=") {
|
|
sep := "?"
|
|
if strings.Contains(c.DatabaseURL, "?") {
|
|
sep = "&"
|
|
}
|
|
|
|
return c.DatabaseURL + sep + "sslmode=disable"
|
|
}
|
|
|
|
return c.DatabaseURL
|
|
}
|
|
|
|
return fmt.Sprintf(
|
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
|
c.DBHost, c.DBPort, c.DBUser, c.DBPassword, c.DBName,
|
|
)
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
|
|
return fallback
|
|
}
|
|
|
|
func getEnvInt(key string, fallback int) int {
|
|
if v := os.Getenv(key); v != "" {
|
|
if n, err := strconv.Atoi(v); err == nil {
|
|
return n
|
|
}
|
|
}
|
|
|
|
return fallback
|
|
}
|