From f06dd5702b2c29f996ec7e8de54832491970f12c Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Thu, 14 May 2026 17:10:56 -0400 Subject: [PATCH] feat(models): widen Site struct and DB schema for ping, port, dns, group monitor types Add Hostname, Port, Timeout, Method, Description, ParentID, AcceptedCodes, DNSResolveType, DNSServer, and IgnoreTLS fields. Refactor AddSite/UpdateSite to accept models.Site instead of individual params. Includes DB migrations for existing databases, per-monitor timeout/TLS in the engine, new type options in TUI forms, and TYPE column in the sites table. --- cmd/goupkeep/main.go | 19 +++++---- internal/models/models.go | 36 ++++++++++------ internal/monitor/monitor.go | 48 +++++++++++++++------- internal/store/postgres.go | 57 ++++++++++++++++++++------ internal/store/sqlite.go | 61 +++++++++++++++++++++------ internal/store/store.go | 4 +- internal/tui/tab_sites.go | 82 ++++++++++++++++++++++++++++++------- 7 files changed, 231 insertions(+), 76 deletions(-) diff --git a/cmd/goupkeep/main.go b/cmd/goupkeep/main.go index be8a9ab..0d8e7b9 100644 --- a/cmd/goupkeep/main.go +++ b/cmd/goupkeep/main.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "go-upkeep/internal/cluster" + "go-upkeep/internal/models" "go-upkeep/internal/monitor" "go-upkeep/internal/server" "go-upkeep/internal/store" @@ -166,14 +167,16 @@ func seedDemoData(s store.Store) { alertID = alerts[0].ID } - s.AddSite("Google", "https://www.google.com", "http", 30, alertID, true, 14, 2) - s.AddSite("GitHub", "https://github.com", "http", 30, alertID, true, 7, 3) - s.AddSite("Cloudflare DNS", "https://1.1.1.1", "http", 60, alertID, false, 7, 1) - s.AddSite("JSON Placeholder", "https://jsonplaceholder.typicode.com/posts/1", "http", 45, alertID, false, 7, 2) - s.AddSite("Nonexistent Site", "https://this-domain-does-not-exist-12345.com", "http", 30, alertID, false, 7, 3) - s.AddSite("Bad Port", "https://localhost:19999", "http", 30, 0, false, 7, 1) - s.AddSite("Backup Cron", "", "push", 300, alertID, false, 7, 0) - s.AddSite("DB Healthcheck", "", "push", 120, alertID, false, 7, 0) + s.AddSite(models.Site{Name: "Google", URL: "https://www.google.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 14, MaxRetries: 2}) + s.AddSite(models.Site{Name: "GitHub", URL: "https://github.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 7, MaxRetries: 3}) + s.AddSite(models.Site{Name: "Cloudflare DNS", URL: "https://1.1.1.1", Type: "http", Interval: 60, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 1}) + s.AddSite(models.Site{Name: "JSON Placeholder", URL: "https://jsonplaceholder.typicode.com/posts/1", Type: "http", Interval: 45, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 2}) + s.AddSite(models.Site{Name: "Nonexistent Site", URL: "https://this-domain-does-not-exist-12345.com", Type: "http", Interval: 30, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 3}) + s.AddSite(models.Site{Name: "Bad Port", URL: "https://localhost:19999", Type: "http", Interval: 30, ExpiryThreshold: 7, MaxRetries: 1}) + s.AddSite(models.Site{Name: "Backup Cron", Type: "push", Interval: 300, AlertID: alertID, ExpiryThreshold: 7}) + s.AddSite(models.Site{Name: "DB Healthcheck", Type: "push", Interval: 120, AlertID: alertID, ExpiryThreshold: 7}) + s.AddSite(models.Site{Name: "Gateway", Type: "ping", Interval: 30, AlertID: alertID, Hostname: "10.0.0.1", Timeout: 5, ExpiryThreshold: 7}) + s.AddSite(models.Site{Name: "SSH Server", Type: "port", Interval: 60, AlertID: alertID, Hostname: "10.0.0.1", Port: 22, Timeout: 5, ExpiryThreshold: 7}) } func isKeyAllowed(incomingKey ssh.PublicKey) bool { diff --git a/internal/models/models.go b/internal/models/models.go index 97354e5..f79c9c5 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -6,23 +6,33 @@ type Site struct { ID int Name string URL string - Type string // "http" or "push" - Token string // Secure Token + Type string // "http", "push", "ping", "port", "dns", "group" + Token string Interval int AlertID int CheckSSL bool ExpiryThreshold int - MaxRetries int - FailureCount int - Status string - StatusCode int - Latency time.Duration - CertExpiry time.Time - HasSSL bool - LastCheck time.Time - SentSSLWarning bool + Hostname string + Port int + Timeout int + Method string + Description string + ParentID int + AcceptedCodes string + DNSResolveType string + DNSServer string + IgnoreTLS bool + + FailureCount int + Status string + StatusCode int + Latency time.Duration + CertExpiry time.Time + HasSSL bool + LastCheck time.Time + SentSSLWarning bool } type AlertConfig struct { @@ -36,7 +46,7 @@ type User struct { ID int Username string PublicKey string - Role string + Role string } // Phase 5: Backup Structure @@ -44,4 +54,4 @@ type Backup struct { Sites []Site `json:"sites"` Alerts []AlertConfig `json:"alerts"` Users []User `json:"users"` -} \ No newline at end of file +} diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go index 40d99f1..a10cc89 100644 --- a/internal/monitor/monitor.go +++ b/internal/monitor/monitor.go @@ -135,19 +135,29 @@ func StartEngine() { }() } -func UpdateSiteConfig(id int, name, url, sType string, interval, alertID int, checkSSL bool, threshold, retries int) { +func UpdateSiteConfig(site models.Site) { Mutex.Lock() defer Mutex.Unlock() - if s, ok := LiveState[id]; ok { - s.Name = name - s.URL = url - s.Type = sType - s.Interval = interval - s.AlertID = alertID - s.CheckSSL = checkSSL - s.ExpiryThreshold = threshold - s.MaxRetries = retries - LiveState[id] = s + if s, ok := LiveState[site.ID]; ok { + s.Name = site.Name + s.URL = site.URL + s.Type = site.Type + s.Interval = site.Interval + s.AlertID = site.AlertID + s.CheckSSL = site.CheckSSL + s.ExpiryThreshold = site.ExpiryThreshold + s.MaxRetries = site.MaxRetries + s.Hostname = site.Hostname + s.Port = site.Port + s.Timeout = site.Timeout + s.Method = site.Method + s.Description = site.Description + s.ParentID = site.ParentID + s.AcceptedCodes = site.AcceptedCodes + s.DNSResolveType = site.DNSResolveType + s.DNSServer = site.DNSServer + s.IgnoreTLS = site.IgnoreTLS + LiveState[site.ID] = s } } @@ -194,10 +204,15 @@ func checkByID(id int) { if !exists { return } - if site.Type == "http" { + switch site.Type { + case "http": checkHTTP(site) - } else { + case "push": checkPush(site) + case "ping", "port", "dns": + AddLog(fmt.Sprintf("Monitor '%s' type '%s' not yet implemented", site.Name, site.Type)) + case "group": + // groups don't perform checks } } @@ -214,7 +229,12 @@ func checkPush(site models.Site) { func checkHTTP(site models.Site) { start := time.Now() - client := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify}}} + timeout := time.Duration(site.Timeout) * time.Second + if timeout <= 0 { + timeout = 5 * time.Second + } + skipTLS := insecureSkipVerify || site.IgnoreTLS + client := &http.Client{Timeout: timeout, Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTLS}}} resp, err := client.Get(site.URL) latency := time.Since(start) diff --git a/internal/store/postgres.go b/internal/store/postgres.go index e7135fa..34ce96b 100644 --- a/internal/store/postgres.go +++ b/internal/store/postgres.go @@ -37,7 +37,17 @@ func (p *PostgresStore) Init() error { alert_id INTEGER, check_ssl BOOLEAN DEFAULT FALSE, threshold INTEGER DEFAULT 7, - max_retries INTEGER DEFAULT 0 + max_retries INTEGER DEFAULT 0, + hostname TEXT DEFAULT '', + port INTEGER DEFAULT 0, + timeout INTEGER DEFAULT 0, + method TEXT DEFAULT 'GET', + description TEXT DEFAULT '', + parent_id INTEGER DEFAULT 0, + accepted_codes TEXT DEFAULT '200-299', + dns_resolve_type TEXT DEFAULT '', + dns_server TEXT DEFAULT '', + ignore_tls BOOLEAN DEFAULT FALSE );`, `CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, @@ -51,12 +61,29 @@ func (p *PostgresStore) Init() error { return err } } + + migrations := []string{ + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS hostname TEXT DEFAULT ''", + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS port INTEGER DEFAULT 0", + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS timeout INTEGER DEFAULT 0", + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS method TEXT DEFAULT 'GET'", + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS description TEXT DEFAULT ''", + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS parent_id INTEGER DEFAULT 0", + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS accepted_codes TEXT DEFAULT '200-299'", + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS dns_resolve_type TEXT DEFAULT ''", + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS dns_server TEXT DEFAULT ''", + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS ignore_tls BOOLEAN DEFAULT FALSE", + } + for _, m := range migrations { + p.db.Exec(m) + } + return nil } // ... [CRUD Methods are identical to Phase 4, keeping them concise here] ... func (p *PostgresStore) GetSites() []models.Site { - rows, err := p.db.Query("SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries FROM sites") + rows, err := p.db.Query("SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, FALSE) FROM sites") if err != nil { return []models.Site{} } @@ -64,25 +91,30 @@ func (p *PostgresStore) GetSites() []models.Site { var sites []models.Site for rows.Next() { var s models.Site - rows.Scan(&s.ID, &s.Name, &s.URL, &s.Type, &s.Token, &s.Interval, &s.AlertID, &s.CheckSSL, &s.ExpiryThreshold, &s.MaxRetries) + rows.Scan(&s.ID, &s.Name, &s.URL, &s.Type, &s.Token, &s.Interval, &s.AlertID, &s.CheckSSL, &s.ExpiryThreshold, &s.MaxRetries, + &s.Hostname, &s.Port, &s.Timeout, &s.Method, &s.Description, &s.ParentID, &s.AcceptedCodes, &s.DNSResolveType, &s.DNSServer, &s.IgnoreTLS) sites = append(sites, s) } return sites } -func (p *PostgresStore) AddSite(name, url, sType string, interval, alertID int, checkSSL bool, threshold, retries int) { +func (p *PostgresStore) AddSite(site models.Site) { token := "" - if sType == "push" { + if site.Type == "push" { token = generateToken() } - p.db.Exec("INSERT INTO sites (name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", name, url, sType, token, interval, alertID, checkSSL, threshold, retries) + p.db.Exec("INSERT INTO sites (name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries, hostname, port, timeout, method, description, parent_id, accepted_codes, dns_resolve_type, dns_server, ignore_tls) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)", + site.Name, site.URL, site.Type, token, site.Interval, site.AlertID, site.CheckSSL, site.ExpiryThreshold, site.MaxRetries, + site.Hostname, site.Port, site.Timeout, site.Method, site.Description, site.ParentID, site.AcceptedCodes, site.DNSResolveType, site.DNSServer, site.IgnoreTLS) } -func (p *PostgresStore) UpdateSite(id int, name, url, sType string, interval, alertID int, checkSSL bool, threshold, retries int) { +func (p *PostgresStore) UpdateSite(site models.Site) { var existingToken string - p.db.QueryRow("SELECT token FROM sites WHERE id=$1", id).Scan(&existingToken) - if sType == "push" && existingToken == "" { + p.db.QueryRow("SELECT token FROM sites WHERE id=$1", site.ID).Scan(&existingToken) + if site.Type == "push" && existingToken == "" { existingToken = generateToken() } - p.db.Exec("UPDATE sites SET name=$1, url=$2, type=$3, token=$4, interval=$5, alert_id=$6, check_ssl=$7, threshold=$8, max_retries=$9 WHERE id=$10", name, url, sType, existingToken, interval, alertID, checkSSL, threshold, retries, id) + p.db.Exec("UPDATE sites SET name=$1, url=$2, type=$3, token=$4, interval=$5, alert_id=$6, check_ssl=$7, threshold=$8, max_retries=$9, hostname=$10, port=$11, timeout=$12, method=$13, description=$14, parent_id=$15, accepted_codes=$16, dns_resolve_type=$17, dns_server=$18, ignore_tls=$19 WHERE id=$20", + site.Name, site.URL, site.Type, existingToken, site.Interval, site.AlertID, site.CheckSSL, site.ExpiryThreshold, site.MaxRetries, + site.Hostname, site.Port, site.Timeout, site.Method, site.Description, site.ParentID, site.AcceptedCodes, site.DNSResolveType, site.DNSServer, site.IgnoreTLS, site.ID) } func (p *PostgresStore) DeleteSite(id int) { p.db.Exec("DELETE FROM sites WHERE id=$1", id) } func (p *PostgresStore) GetAllAlerts() []models.AlertConfig { @@ -175,8 +207,9 @@ func (p *PostgresStore) ImportData(data models.Backup) error { tx.Exec("INSERT INTO alerts (id, name, type, settings) VALUES ($1, $2, $3, $4)", a.ID, a.Name, a.Type, string(jsonBytes)) } for _, st := range data.Sites { - tx.Exec("INSERT INTO sites (id, name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", - st.ID, st.Name, st.URL, st.Type, st.Token, st.Interval, st.AlertID, st.CheckSSL, st.ExpiryThreshold, st.MaxRetries) + tx.Exec("INSERT INTO sites (id, name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries, hostname, port, timeout, method, description, parent_id, accepted_codes, dns_resolve_type, dns_server, ignore_tls) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)", + st.ID, st.Name, st.URL, st.Type, st.Token, st.Interval, st.AlertID, st.CheckSSL, st.ExpiryThreshold, st.MaxRetries, + st.Hostname, st.Port, st.Timeout, st.Method, st.Description, st.ParentID, st.AcceptedCodes, st.DNSResolveType, st.DNSServer, st.IgnoreTLS) } tx.Exec("SELECT setval('sites_id_seq', (SELECT MAX(id) FROM sites))") diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index c653351..d55a35e 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -39,7 +39,17 @@ func (s *SQLiteStore) Init() error { alert_id INTEGER, check_ssl BOOLEAN DEFAULT 0, threshold INTEGER DEFAULT 7, - max_retries INTEGER DEFAULT 0 + max_retries INTEGER DEFAULT 0, + hostname TEXT DEFAULT '', + port INTEGER DEFAULT 0, + timeout INTEGER DEFAULT 0, + method TEXT DEFAULT 'GET', + description TEXT DEFAULT '', + parent_id INTEGER DEFAULT 0, + accepted_codes TEXT DEFAULT '200-299', + dns_resolve_type TEXT DEFAULT '', + dns_server TEXT DEFAULT '', + ignore_tls BOOLEAN DEFAULT 0 ); CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -48,7 +58,27 @@ func (s *SQLiteStore) Init() error { role TEXT DEFAULT 'user' );` _, err = s.db.Exec(createTables) - return err + if err != nil { + return err + } + + migrations := []string{ + "ALTER TABLE sites ADD COLUMN hostname TEXT DEFAULT ''", + "ALTER TABLE sites ADD COLUMN port INTEGER DEFAULT 0", + "ALTER TABLE sites ADD COLUMN timeout INTEGER DEFAULT 0", + "ALTER TABLE sites ADD COLUMN method TEXT DEFAULT 'GET'", + "ALTER TABLE sites ADD COLUMN description TEXT DEFAULT ''", + "ALTER TABLE sites ADD COLUMN parent_id INTEGER DEFAULT 0", + "ALTER TABLE sites ADD COLUMN accepted_codes TEXT DEFAULT '200-299'", + "ALTER TABLE sites ADD COLUMN dns_resolve_type TEXT DEFAULT ''", + "ALTER TABLE sites ADD COLUMN dns_server TEXT DEFAULT ''", + "ALTER TABLE sites ADD COLUMN ignore_tls BOOLEAN DEFAULT 0", + } + for _, m := range migrations { + s.db.Exec(m) + } + + return nil } func generateToken() string { @@ -60,7 +90,7 @@ func generateToken() string { } func (s *SQLiteStore) GetSites() []models.Site { - rows, err := s.db.Query("SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries FROM sites") + rows, err := s.db.Query("SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, 0) FROM sites") if err != nil { return []models.Site{} } @@ -68,25 +98,29 @@ func (s *SQLiteStore) GetSites() []models.Site { var sites []models.Site for rows.Next() { var st models.Site - rows.Scan(&st.ID, &st.Name, &st.URL, &st.Type, &st.Token, &st.Interval, &st.AlertID, &st.CheckSSL, &st.ExpiryThreshold, &st.MaxRetries) + rows.Scan(&st.ID, &st.Name, &st.URL, &st.Type, &st.Token, &st.Interval, &st.AlertID, &st.CheckSSL, &st.ExpiryThreshold, &st.MaxRetries, &st.Hostname, &st.Port, &st.Timeout, &st.Method, &st.Description, &st.ParentID, &st.AcceptedCodes, &st.DNSResolveType, &st.DNSServer, &st.IgnoreTLS) sites = append(sites, st) } return sites } -func (s *SQLiteStore) AddSite(name, url, sType string, interval, alertID int, checkSSL bool, threshold, retries int) { +func (s *SQLiteStore) AddSite(site models.Site) { token := "" - if sType == "push" { + if site.Type == "push" { token = generateToken() } - s.db.Exec("INSERT INTO sites (name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", name, url, sType, token, interval, alertID, checkSSL, threshold, retries) + s.db.Exec("INSERT INTO sites (name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries, hostname, port, timeout, method, description, parent_id, accepted_codes, dns_resolve_type, dns_server, ignore_tls) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + site.Name, site.URL, site.Type, token, site.Interval, site.AlertID, site.CheckSSL, site.ExpiryThreshold, site.MaxRetries, + site.Hostname, site.Port, site.Timeout, site.Method, site.Description, site.ParentID, site.AcceptedCodes, site.DNSResolveType, site.DNSServer, site.IgnoreTLS) } -func (s *SQLiteStore) UpdateSite(id int, name, url, sType string, interval, alertID int, checkSSL bool, threshold, retries int) { +func (s *SQLiteStore) UpdateSite(site models.Site) { var existingToken string - s.db.QueryRow("SELECT token FROM sites WHERE id=?", id).Scan(&existingToken) - if sType == "push" && existingToken == "" { + s.db.QueryRow("SELECT token FROM sites WHERE id=?", site.ID).Scan(&existingToken) + if site.Type == "push" && existingToken == "" { existingToken = generateToken() } - s.db.Exec("UPDATE sites SET name=?, url=?, type=?, token=?, interval=?, alert_id=?, check_ssl=?, threshold=?, max_retries=? WHERE id=?", name, url, sType, existingToken, interval, alertID, checkSSL, threshold, retries, id) + s.db.Exec("UPDATE sites SET name=?, url=?, type=?, token=?, interval=?, alert_id=?, check_ssl=?, threshold=?, max_retries=?, hostname=?, port=?, timeout=?, method=?, description=?, parent_id=?, accepted_codes=?, dns_resolve_type=?, dns_server=?, ignore_tls=? WHERE id=?", + site.Name, site.URL, site.Type, existingToken, site.Interval, site.AlertID, site.CheckSSL, site.ExpiryThreshold, site.MaxRetries, + site.Hostname, site.Port, site.Timeout, site.Method, site.Description, site.ParentID, site.AcceptedCodes, site.DNSResolveType, site.DNSServer, site.IgnoreTLS, site.ID) } func (s *SQLiteStore) DeleteSite(id int) { s.db.Exec("DELETE FROM sites WHERE id=?", id) @@ -198,8 +232,9 @@ func (s *SQLiteStore) ImportData(data models.Backup) error { tx.Exec("INSERT INTO alerts (id, name, type, settings) VALUES (?, ?, ?, ?)", a.ID, a.Name, a.Type, string(jsonBytes)) } for _, st := range data.Sites { - tx.Exec("INSERT INTO sites (id, name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - st.ID, st.Name, st.URL, st.Type, st.Token, st.Interval, st.AlertID, st.CheckSSL, st.ExpiryThreshold, st.MaxRetries) + tx.Exec("INSERT INTO sites (id, name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries, hostname, port, timeout, method, description, parent_id, accepted_codes, dns_resolve_type, dns_server, ignore_tls) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + st.ID, st.Name, st.URL, st.Type, st.Token, st.Interval, st.AlertID, st.CheckSSL, st.ExpiryThreshold, st.MaxRetries, + st.Hostname, st.Port, st.Timeout, st.Method, st.Description, st.ParentID, st.AcceptedCodes, st.DNSResolveType, st.DNSServer, st.IgnoreTLS) } return tx.Commit() diff --git a/internal/store/store.go b/internal/store/store.go index 6afbb07..e9c05ac 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -9,8 +9,8 @@ type Store interface { // Sites GetSites() []models.Site - AddSite(name, url, sType string, interval, alertID int, checkSSL bool, threshold, retries int) - UpdateSite(id int, name, url, sType string, interval, alertID int, checkSSL bool, threshold, retries int) + AddSite(site models.Site) + UpdateSite(site models.Site) DeleteSite(id int) // Alerts diff --git a/internal/tui/tab_sites.go b/internal/tui/tab_sites.go index e565bd3..2ac265b 100644 --- a/internal/tui/tab_sites.go +++ b/internal/tui/tab_sites.go @@ -35,18 +35,23 @@ var ( siteBorderStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#444")) - siteColWidths = []int{4, 16, 8, 9, 8, 22, 10, 6} + siteColWidths = []int{4, 14, 6, 8, 9, 8, 20, 10, 6} ) type siteFormData struct { - Name string - SiteType string - URL string - Interval string - AlertID string - CheckSSL bool - Threshold string - Retries string + Name string + SiteType string + URL string + Interval string + AlertID string + CheckSSL bool + Threshold string + Retries string + Hostname string + Port string + Timeout string + Description string + IgnoreTLS bool } func latencySparkline(latencies []time.Duration, width int) string { @@ -229,7 +234,8 @@ func (m Model) viewSitesTab() string { rows = append(rows, []string{ strconv.Itoa(site.ID), - m.zones.Mark(fmt.Sprintf("site-%d", i), limitStr(site.Name, 15)), + m.zones.Mark(fmt.Sprintf("site-%d", i), limitStr(site.Name, 13)), + site.Type, fmtStatus(site.Status), fmtLatency(site.Latency), fmtUptime(hist.TotalChecks, hist.UpChecks), @@ -242,7 +248,7 @@ func (m Model) viewSitesTab() string { t := table.New(). Border(lipgloss.RoundedBorder()). BorderStyle(siteBorderStyle). - Headers("ID", "NAME", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRY"). + Headers("ID", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRY"). Rows(rows...). StyleFunc(func(row, col int) lipgloss.Style { if row == table.HeaderRow { @@ -271,6 +277,8 @@ func (m *Model) initSiteHuhForm() tea.Cmd { Interval: "60", Threshold: "7", Retries: "0", + Timeout: "5", + Port: "0", } if m.editID > 0 { @@ -284,6 +292,11 @@ func (m *Model) initSiteHuhForm() tea.Cmd { m.siteFormData.CheckSSL = site.CheckSSL m.siteFormData.Threshold = strconv.Itoa(site.ExpiryThreshold) m.siteFormData.Retries = strconv.Itoa(site.MaxRetries) + m.siteFormData.Hostname = site.Hostname + m.siteFormData.Port = strconv.Itoa(site.Port) + m.siteFormData.Timeout = strconv.Itoa(site.Timeout) + m.siteFormData.Description = site.Description + m.siteFormData.IgnoreTLS = site.IgnoreTLS break } } @@ -314,6 +327,10 @@ func (m *Model) initSiteHuhForm() tea.Cmd { Options( huh.NewOption("HTTP/HTTPS", "http"), huh.NewOption("Push / Heartbeat", "push"), + huh.NewOption("Ping (ICMP)", "ping"), + huh.NewOption("TCP Port", "port"), + huh.NewOption("DNS", "dns"), + huh.NewOption("Group", "group"), ).Value(&m.siteFormData.SiteType), huh.NewInput().Title("URL"). Placeholder("https://example.com"). @@ -345,6 +362,22 @@ func (m *Model) initSiteHuhForm() tea.Cmd { Options(alertOpts...). Value(&m.siteFormData.AlertID), ).Title("Monitor Settings"), + huh.NewGroup( + huh.NewInput().Title("Hostname / IP"). + Placeholder("10.0.0.1"). + Description("Target for ping/port/DNS monitors"). + Value(&m.siteFormData.Hostname), + huh.NewInput().Title("Port"). + Placeholder("0"). + Description("Target port for TCP port monitors"). + Value(&m.siteFormData.Port), + huh.NewInput().Title("Timeout (seconds)"). + Placeholder("5"). + Value(&m.siteFormData.Timeout), + huh.NewInput().Title("Description"). + Placeholder("Optional description"). + Value(&m.siteFormData.Description), + ).Title("Connection"), huh.NewGroup( huh.NewConfirm().Title("Monitor SSL Certificate?"). Value(&m.siteFormData.CheckSSL), @@ -354,6 +387,8 @@ func (m *Model) initSiteHuhForm() tea.Cmd { huh.NewInput().Title("Max Retries Before Alert"). Placeholder("0"). Value(&m.siteFormData.Retries), + huh.NewConfirm().Title("Ignore TLS Errors?"). + Value(&m.siteFormData.IgnoreTLS), ).Title("Advanced"), ).WithTheme(huh.ThemeDracula()) @@ -366,6 +401,8 @@ func (m *Model) submitSiteForm() { alertID, _ := strconv.Atoi(d.AlertID) threshold, _ := strconv.Atoi(d.Threshold) retries, _ := strconv.Atoi(d.Retries) + port, _ := strconv.Atoi(d.Port) + timeout, _ := strconv.Atoi(d.Timeout) if interval < 1 { interval = 60 } @@ -373,11 +410,28 @@ func (m *Model) submitSiteForm() { threshold = 7 } + site := models.Site{ + ID: m.editID, + Name: d.Name, + URL: d.URL, + Type: d.SiteType, + Interval: interval, + AlertID: alertID, + CheckSSL: d.CheckSSL, + ExpiryThreshold: threshold, + MaxRetries: retries, + Hostname: d.Hostname, + Port: port, + Timeout: timeout, + Description: d.Description, + IgnoreTLS: d.IgnoreTLS, + } + if m.editID > 0 { - store.Get().UpdateSite(m.editID, d.Name, d.URL, d.SiteType, interval, alertID, d.CheckSSL, threshold, retries) - monitor.UpdateSiteConfig(m.editID, d.Name, d.URL, d.SiteType, interval, alertID, d.CheckSSL, threshold, retries) + store.Get().UpdateSite(site) + monitor.UpdateSiteConfig(site) } else { - store.Get().AddSite(d.Name, d.URL, d.SiteType, interval, alertID, d.CheckSSL, threshold, retries) + store.Get().AddSite(site) } m.state = stateDashboard }