diff options
| author | Fuwn <[email protected]> | 2026-01-23 20:54:50 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-23 20:54:50 -0800 |
| commit | 4f8955fb5b9e05a5d0e2f4811c13926a6cbbe4b3 (patch) | |
| tree | 4d1bcd871bb90e55f1aa11b16bc8763ac692af73 | |
| parent | feat: Add database maintenance with backup/reset modes and triggers (diff) | |
| download | kaze-4f8955fb5b9e05a5d0e2f4811c13926a6cbbe4b3.tar.xz kaze-4f8955fb5b9e05a5d0e2f4811c13926a6cbbe4b3.zip | |
feat: Switch to libsql driver for Turso compatibility
| -rw-r--r-- | cmd/kaze/main.go | 18 | ||||
| -rw-r--r-- | go.mod | 15 | ||||
| -rw-r--r-- | go.sum | 62 | ||||
| -rw-r--r-- | internal/config/config.go | 9 | ||||
| -rw-r--r-- | internal/storage/sqlite.go | 123 |
5 files changed, 105 insertions, 122 deletions
diff --git a/cmd/kaze/main.go b/cmd/kaze/main.go index ee12ae8..aaed1fa 100644 --- a/cmd/kaze/main.go +++ b/cmd/kaze/main.go @@ -8,6 +8,7 @@ import ( "os" "os/signal" "path/filepath" + "strings" "syscall" "time" @@ -24,6 +25,18 @@ var ( date = "unknown" ) +func maskDatabaseURL(url string) string { + if strings.Contains(url, "authToken=") { + idx := strings.Index(url, "authToken=") + end := strings.Index(url[idx:], "&") + if end == -1 { + return url[:idx] + "authToken=***" + } + return url[:idx] + "authToken=***" + url[idx+end:] + } + return url +} + func main() { // Parse flags configPath := flag.String("config", "config.yaml", "Path to configuration file") @@ -68,14 +81,15 @@ func main() { "groups", len(cfg.Groups), "incidents", len(cfg.Incidents)) - store, err := storage.NewWithMaintenance(cfg.Storage.Path, cfg.Storage.HistoryDays, cfg.Storage.Maintenance) + dbURL := cfg.Storage.GetDatabaseURL() + store, err := storage.NewWithMaintenance(dbURL, cfg.Storage.HistoryDays, cfg.Storage.Maintenance) if err != nil { logger.Error("failed to initialize storage", "error", err) os.Exit(1) } defer store.Close() logger.Info("initialized storage", - "path", cfg.Storage.Path, + "url", maskDatabaseURL(dbURL), "history_days", cfg.Storage.HistoryDays, "maintenance_mode", store.GetMaintenanceMode()) @@ -3,23 +3,18 @@ module github.com/Fuwn/kaze go 1.24.3 require ( + github.com/fsnotify/fsnotify v1.9.0 + github.com/go-ping/ping v1.2.0 + github.com/tursodatabase/go-libsql v0.0.0-20251219133454-43644db490ff gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.44.1 ) require ( - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-ping/ping v1.2.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect - modernc.org/libc v1.67.6 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect ) @@ -1,76 +1,38 @@ -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ= github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/tursodatabase/go-libsql v0.0.0-20251219133454-43644db490ff h1:Hvxz9W8fWpSg9xkiq8/q+3cVJo+MmLMfkjdS/u4nWFY= +github.com/tursodatabase/go-libsql v0.0.0-20251219133454-43644db490ff/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= -modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= -modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= -modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= -modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= -modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= -modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= -modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= -modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.44.1 h1:qybx/rNpfQipX/t47OxbHmkkJuv2JWifCMH8SVUiDas= -modernc.org/sqlite v1.44.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= -modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/internal/config/config.go b/internal/config/config.go index 250827b..3ac03ea 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -74,10 +74,19 @@ type ServerConfig struct { // StorageConfig contains database settings type StorageConfig struct { Path string `yaml:"path"` + URL string `yaml:"url,omitempty"` // libsql:// or file:// URL (overrides path, supports $ENV_VAR) HistoryDays int `yaml:"history_days"` Maintenance MaintenanceConfig `yaml:"maintenance,omitempty"` } +func (s *StorageConfig) GetDatabaseURL() string { + url := s.URL + if url == "" { + url = s.Path + } + return os.ExpandEnv(url) +} + // MaintenanceConfig controls database maintenance/pruning behavior type MaintenanceConfig struct { // Mode: "never" (default), "backup" (rename with epoch suffix), "reset" (delete in place) diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go index db0ed52..5fcc9b3 100644 --- a/internal/storage/sqlite.go +++ b/internal/storage/sqlite.go @@ -8,7 +8,7 @@ import ( "time" "github.com/Fuwn/kaze/internal/config" - _ "modernc.org/sqlite" + _ "github.com/tursodatabase/go-libsql" ) // Storage handles all database operations @@ -107,22 +107,27 @@ func NewWithMaintenance(dbPath string, historyDays int, maintenanceCfg config.Ma return s, nil } -func openDB(dbPath string) (*sql.DB, error) { - dsn := fmt.Sprintf("%s?_txlock=immediate&_busy_timeout=5000&_journal_mode=WAL&_synchronous=NORMAL", dbPath) +func openDB(dbURL string) (*sql.DB, error) { + isRemote := strings.HasPrefix(dbURL, "libsql://") || strings.HasPrefix(dbURL, "https://") - db, err := sql.Open("sqlite", dsn) + dsn := dbURL + if !isRemote && !strings.HasPrefix(dbURL, "file:") { + dsn = "file:" + dbURL + } + + if !isRemote { + dsn = dsn + "?_txlock=immediate&_busy_timeout=5000&_journal_mode=WAL&_synchronous=NORMAL" + } + + db, err := sql.Open("libsql", dsn) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } - db.SetMaxOpenConns(1) - db.SetMaxIdleConns(1) - db.SetConnMaxLifetime(0) - - var journalMode string - if err := db.QueryRow("PRAGMA journal_mode").Scan(&journalMode); err != nil { - db.Close() - return nil, fmt.Errorf("failed to check journal mode: %w", err) + if !isRemote { + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + db.SetConnMaxLifetime(0) } if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil { @@ -133,55 +138,53 @@ func openDB(dbPath string) (*sql.DB, error) { return db, nil } -// migrate creates the database schema func (s *Storage) migrate() error { - schema := ` - CREATE TABLE IF NOT EXISTS check_results ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - monitor_name TEXT NOT NULL, - timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - status TEXT NOT NULL CHECK(status IN ('up', 'down', 'degraded')), - response_time_ms INTEGER NOT NULL DEFAULT 0, - status_code INTEGER DEFAULT 0, - error_message TEXT, - ssl_expiry DATETIME, - ssl_days_left INTEGER DEFAULT 0 - ); - - CREATE INDEX IF NOT EXISTS idx_check_results_monitor_time - ON check_results(monitor_name, timestamp DESC); - - CREATE INDEX IF NOT EXISTS idx_check_results_timestamp - ON check_results(timestamp); - - CREATE TABLE IF NOT EXISTS daily_stats ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - monitor_name TEXT NOT NULL, - date DATE NOT NULL, - success_count INTEGER NOT NULL DEFAULT 0, - failure_count INTEGER NOT NULL DEFAULT 0, - total_checks INTEGER NOT NULL DEFAULT 0, - avg_response_ms REAL DEFAULT 0, - uptime_percent REAL DEFAULT 0, - UNIQUE(monitor_name, date) - ); - - CREATE INDEX IF NOT EXISTS idx_daily_stats_monitor_date - ON daily_stats(monitor_name, date DESC); - - CREATE TABLE IF NOT EXISTS monitor_state ( - monitor_name TEXT PRIMARY KEY, - current_status TEXT NOT NULL DEFAULT 'unknown', - last_check DATETIME, - last_response_time_ms INTEGER DEFAULT 0, - last_error TEXT, - ssl_expiry DATETIME, - ssl_days_left INTEGER DEFAULT 0 - ); - ` - - _, err := s.db.Exec(schema) - return err + statements := []string{ + `CREATE TABLE IF NOT EXISTS check_results ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + monitor_name TEXT NOT NULL, + timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + status TEXT NOT NULL CHECK(status IN ('up', 'down', 'degraded')), + response_time_ms INTEGER NOT NULL DEFAULT 0, + status_code INTEGER DEFAULT 0, + error_message TEXT, + ssl_expiry DATETIME, + ssl_days_left INTEGER DEFAULT 0 + )`, + `CREATE INDEX IF NOT EXISTS idx_check_results_monitor_time + ON check_results(monitor_name, timestamp DESC)`, + `CREATE INDEX IF NOT EXISTS idx_check_results_timestamp + ON check_results(timestamp)`, + `CREATE TABLE IF NOT EXISTS daily_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + monitor_name TEXT NOT NULL, + date DATE NOT NULL, + success_count INTEGER NOT NULL DEFAULT 0, + failure_count INTEGER NOT NULL DEFAULT 0, + total_checks INTEGER NOT NULL DEFAULT 0, + avg_response_ms REAL DEFAULT 0, + uptime_percent REAL DEFAULT 0, + UNIQUE(monitor_name, date) + )`, + `CREATE INDEX IF NOT EXISTS idx_daily_stats_monitor_date + ON daily_stats(monitor_name, date DESC)`, + `CREATE TABLE IF NOT EXISTS monitor_state ( + monitor_name TEXT PRIMARY KEY, + current_status TEXT NOT NULL DEFAULT 'unknown', + last_check DATETIME, + last_response_time_ms INTEGER DEFAULT 0, + last_error TEXT, + ssl_expiry DATETIME, + ssl_days_left INTEGER DEFAULT 0 + )`, + } + + for _, stmt := range statements { + if _, err := s.db.Exec(stmt); err != nil { + return fmt.Errorf("migration failed: %w", err) + } + } + return nil } // SaveCheckResult saves a check result and updates monitor state |