release: 2026.05.1 — distributed probing, config-as-code, TUI polish #15

Merged
lerko merged 47 commits from develop into main 2026-05-16 20:03:54 +00:00
10 changed files with 199 additions and 77 deletions
Showing only changes of commit 2f8de35d4b - Show all commits
+1
View File
@@ -24,6 +24,7 @@ type Site struct {
DNSResolveType string DNSResolveType string
DNSServer string DNSServer string
IgnoreTLS bool IgnoreTLS bool
Paused bool
FailureCount int FailureCount int
Status string Status string
+24 -2
View File
@@ -162,6 +162,7 @@ func UpdateSiteConfig(site models.Site) {
s.DNSResolveType = site.DNSResolveType s.DNSResolveType = site.DNSResolveType
s.DNSServer = site.DNSServer s.DNSServer = site.DNSServer
s.IgnoreTLS = site.IgnoreTLS s.IgnoreTLS = site.IgnoreTLS
s.Paused = site.Paused
LiveState[site.ID] = s LiveState[site.ID] = s
} }
} }
@@ -173,10 +174,26 @@ func RemoveSite(id int) {
RemoveHistory(id) RemoveHistory(id)
} }
func ToggleSitePause(id int) bool {
Mutex.Lock()
defer Mutex.Unlock()
site, ok := LiveState[id]
if !ok {
return false
}
site.Paused = !site.Paused
LiveState[id] = site
if site.Paused {
AddLog(fmt.Sprintf("Monitor '%s' paused", site.Name))
} else {
AddLog(fmt.Sprintf("Monitor '%s' resumed", site.Name))
}
return site.Paused
}
func monitorRoutine(id int) { func monitorRoutine(id int) {
checkByID(id) checkByID(id)
for { for {
// If paused, just sleep loop to keep goroutine alive but idle
if !IsEngineActive() { if !IsEngineActive() {
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
continue continue
@@ -189,6 +206,11 @@ func monitorRoutine(id int) {
return return
} }
if site.Paused {
time.Sleep(5 * time.Second)
continue
}
interval := site.Interval interval := site.Interval
if interval < 5 { if interval < 5 {
interval = 5 interval = 5
@@ -206,7 +228,7 @@ func checkByID(id int) {
Mutex.RLock() Mutex.RLock()
site, exists := LiveState[id] site, exists := LiveState[id]
Mutex.RUnlock() Mutex.RUnlock()
if !exists { if !exists || site.Paused {
return return
} }
switch site.Type { switch site.Type {
+92 -13
View File
@@ -148,7 +148,6 @@ func renderStatusPage(w http.ResponseWriter, title string) {
<html> <html>
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<meta http-equiv="refresh" content="5">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<style> <style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #1a1b26; color: #a9b1d6; padding: 20px; margin: 0; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #1a1b26; color: #a9b1d6; padding: 20px; margin: 0; }
@@ -162,26 +161,106 @@ func renderStatusPage(w http.ResponseWriter, title string) {
.UP { background: #9ece6a; color: #1a1b26; } .UP { background: #9ece6a; color: #1a1b26; }
.DOWN { background: #f7768e; color: #1a1b26; } .DOWN { background: #f7768e; color: #1a1b26; }
.PENDING { background: #e0af68; color: #1a1b26; } .PENDING { background: #e0af68; color: #1a1b26; }
.SSLEXP { background: #e0af68; color: #1a1b26; } .SSL-EXP { background: #e0af68; color: #1a1b26; }
.PAUSED { background: #565f89; color: #c0caf5; }
.summary { display: flex; justify-content: center; gap: 16px; margin-bottom: 24px; font-size: 0.95em; font-weight: 600; }
.summary span { padding: 4px 12px; border-radius: 6px; }
.summary .s-up { color: #9ece6a; }
.summary .s-down { color: #f7768e; }
.summary .s-paused { color: #565f89; }
.summary .s-total { color: #7aa2f7; }
.stale-bar { text-align: center; font-size: 0.8em; color: #565f89; margin-bottom: 16px; transition: color 0.3s; }
.stale-bar.warn { color: #e0af68; }
.stale-bar.error { color: #f7768e; }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>{{.Title}}</h1> <h1>{{.Title}}</h1>
{{range .Sites}} <div id="summary" class="summary"></div>
<div class="card"> <div id="stale" class="stale-bar"></div>
<div class="info"> <div id="cards"></div>
<div class="name">{{.Name}}</div>
<div class="meta">{{.Type}} | {{if eq .Type "http"}}{{.URL}}{{else}}Heartbeat Monitor{{end}}</div>
<div class="meta" style="margin-top:4px;">Last Check: {{.LastCheck.Format "15:04:05"}}</div>
</div>
<div class="status {{.Status}}">{{.Status}}</div>
</div>
{{end}}
<div style="text-align: center; margin-top: 40px; color: #565f89; font-size: 0.8em;">Powered by Go-Upkeep</div> <div style="text-align: center; margin-top: 40px; color: #565f89; font-size: 0.8em;">Powered by Go-Upkeep</div>
</div> </div>
<script> <script>
setTimeout(function(){ window.location.reload(1); }, 5000); var lastUpdate = null;
function cssClass(status) {
return status.replace(/\s+/g, '-');
}
function renderSummary(sites) {
var up = 0, down = 0, paused = 0, total = sites.length;
for (var i = 0; i < sites.length; i++) {
if (sites[i].Paused) { paused++; continue; }
if (sites[i].Status === 'UP') up++;
else if (sites[i].Status === 'DOWN') down++;
}
var el = document.getElementById('summary');
var parts = ['<span class="s-total">' + up + '/' + total + ' UP</span>'];
if (down > 0) parts.push('<span class="s-down">' + down + ' DOWN</span>');
if (paused > 0) parts.push('<span class="s-paused">' + paused + ' PAUSED</span>');
el.innerHTML = parts.join('<span style="color:#383838">·</span>');
}
function renderStale() {
var el = document.getElementById('stale');
if (!lastUpdate) { el.textContent = ''; return; }
var ago = Math.round((Date.now() - lastUpdate) / 1000);
el.className = 'stale-bar';
if (ago < 10) {
el.textContent = 'Updated just now';
} else if (ago < 30) {
el.textContent = 'Updated ' + ago + 's ago';
el.className = 'stale-bar warn';
} else {
el.textContent = 'Stale — last update ' + ago + 's ago';
el.className = 'stale-bar error';
}
}
function render(sites) {
var c = document.getElementById('cards');
var html = '';
sites.sort(function(a, b) {
if (a.Status !== b.Status) {
if (a.Status === 'DOWN') return -1;
if (b.Status === 'DOWN') return 1;
}
return a.Name < b.Name ? -1 : a.Name > b.Name ? 1 : 0;
});
renderSummary(sites);
for (var i = 0; i < sites.length; i++) {
var s = sites[i];
var st = s.Paused ? 'PAUSED' : s.Status;
var cls = cssClass(st);
var meta = s.Type + ' | ' + (s.Type === 'http' ? s.URL : 'Heartbeat Monitor');
var lc = s.LastCheck ? new Date(s.LastCheck).toLocaleTimeString('en-GB', {hour12: false}) : '—';
html += '<div class="card"><div class="info">' +
'<div class="name">' + s.Name + '</div>' +
'<div class="meta">' + meta + '</div>' +
'<div class="meta" style="margin-top:4px;">Last Check: ' + lc + '</div>' +
'</div><div class="status ' + cls + '">' + st + '</div></div>';
}
c.innerHTML = html;
}
function refresh() {
fetch('/status/json')
.then(function(r) { return r.json(); })
.then(function(data) {
var sites = [];
for (var k in data) sites.push(data[k]);
lastUpdate = Date.now();
render(sites);
})
.catch(function() {});
renderStale();
setTimeout(refresh, 5000);
}
setInterval(renderStale, 1000);
refresh();
</script> </script>
</body> </body>
</html>` </html>`
+14 -9
View File
@@ -47,7 +47,8 @@ func (p *PostgresStore) Init() error {
accepted_codes TEXT DEFAULT '200-299', accepted_codes TEXT DEFAULT '200-299',
dns_resolve_type TEXT DEFAULT '', dns_resolve_type TEXT DEFAULT '',
dns_server TEXT DEFAULT '', dns_server TEXT DEFAULT '',
ignore_tls BOOLEAN DEFAULT FALSE ignore_tls BOOLEAN DEFAULT FALSE,
paused BOOLEAN DEFAULT FALSE
);`, );`,
`CREATE TABLE IF NOT EXISTS users ( `CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@@ -73,6 +74,7 @@ func (p *PostgresStore) Init() error {
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS dns_resolve_type TEXT DEFAULT ''", "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 dns_server TEXT DEFAULT ''",
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS ignore_tls BOOLEAN DEFAULT FALSE", "ALTER TABLE sites ADD COLUMN IF NOT EXISTS ignore_tls BOOLEAN DEFAULT FALSE",
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS paused BOOLEAN DEFAULT FALSE",
} }
for _, m := range migrations { for _, m := range migrations {
p.db.Exec(m) p.db.Exec(m)
@@ -83,7 +85,7 @@ func (p *PostgresStore) Init() error {
// ... [CRUD Methods are identical to Phase 4, keeping them concise here] ... // ... [CRUD Methods are identical to Phase 4, keeping them concise here] ...
func (p *PostgresStore) GetSites() []models.Site { 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, 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") 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), COALESCE(paused, FALSE) FROM sites")
if err != nil { if err != nil {
return []models.Site{} return []models.Site{}
} }
@@ -92,7 +94,7 @@ func (p *PostgresStore) GetSites() []models.Site {
for rows.Next() { for rows.Next() {
var s models.Site 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) &s.Hostname, &s.Port, &s.Timeout, &s.Method, &s.Description, &s.ParentID, &s.AcceptedCodes, &s.DNSResolveType, &s.DNSServer, &s.IgnoreTLS, &s.Paused)
sites = append(sites, s) sites = append(sites, s)
} }
return sites return sites
@@ -102,9 +104,9 @@ func (p *PostgresStore) AddSite(site models.Site) {
if site.Type == "push" { if site.Type == "push" {
token = generateToken() token = generateToken()
} }
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)", 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, paused) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)",
site.Name, site.URL, site.Type, token, site.Interval, site.AlertID, site.CheckSSL, site.ExpiryThreshold, site.MaxRetries, 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) site.Hostname, site.Port, site.Timeout, site.Method, site.Description, site.ParentID, site.AcceptedCodes, site.DNSResolveType, site.DNSServer, site.IgnoreTLS, site.Paused)
} }
func (p *PostgresStore) UpdateSite(site models.Site) { func (p *PostgresStore) UpdateSite(site models.Site) {
var existingToken string var existingToken string
@@ -112,9 +114,12 @@ func (p *PostgresStore) UpdateSite(site models.Site) {
if site.Type == "push" && existingToken == "" { if site.Type == "push" && existingToken == "" {
existingToken = generateToken() 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, 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", 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, paused=$20 WHERE id=$21",
site.Name, site.URL, site.Type, existingToken, site.Interval, site.AlertID, site.CheckSSL, site.ExpiryThreshold, site.MaxRetries, 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) site.Hostname, site.Port, site.Timeout, site.Method, site.Description, site.ParentID, site.AcceptedCodes, site.DNSResolveType, site.DNSServer, site.IgnoreTLS, site.Paused, site.ID)
}
func (p *PostgresStore) UpdateSitePaused(id int, paused bool) {
p.db.Exec("UPDATE sites SET paused=$1 WHERE id=$2", paused, id)
} }
func (p *PostgresStore) DeleteSite(id int) { p.db.Exec("DELETE FROM sites WHERE id=$1", id) } func (p *PostgresStore) DeleteSite(id int) { p.db.Exec("DELETE FROM sites WHERE id=$1", id) }
func (p *PostgresStore) GetAllAlerts() []models.AlertConfig { func (p *PostgresStore) GetAllAlerts() []models.AlertConfig {
@@ -207,9 +212,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)) 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 { for _, st := range data.Sites {
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)", 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, paused) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)",
st.ID, st.Name, st.URL, st.Type, st.Token, st.Interval, st.AlertID, st.CheckSSL, st.ExpiryThreshold, st.MaxRetries, 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) st.Hostname, st.Port, st.Timeout, st.Method, st.Description, st.ParentID, st.AcceptedCodes, st.DNSResolveType, st.DNSServer, st.IgnoreTLS, st.Paused)
} }
tx.Exec("SELECT setval('sites_id_seq', (SELECT MAX(id) FROM sites))") tx.Exec("SELECT setval('sites_id_seq', (SELECT MAX(id) FROM sites))")
+14 -9
View File
@@ -49,7 +49,8 @@ func (s *SQLiteStore) Init() error {
accepted_codes TEXT DEFAULT '200-299', accepted_codes TEXT DEFAULT '200-299',
dns_resolve_type TEXT DEFAULT '', dns_resolve_type TEXT DEFAULT '',
dns_server TEXT DEFAULT '', dns_server TEXT DEFAULT '',
ignore_tls BOOLEAN DEFAULT 0 ignore_tls BOOLEAN DEFAULT 0,
paused BOOLEAN DEFAULT 0
); );
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -73,6 +74,7 @@ func (s *SQLiteStore) Init() error {
"ALTER TABLE sites ADD COLUMN dns_resolve_type TEXT DEFAULT ''", "ALTER TABLE sites ADD COLUMN dns_resolve_type TEXT DEFAULT ''",
"ALTER TABLE sites ADD COLUMN dns_server TEXT DEFAULT ''", "ALTER TABLE sites ADD COLUMN dns_server TEXT DEFAULT ''",
"ALTER TABLE sites ADD COLUMN ignore_tls BOOLEAN DEFAULT 0", "ALTER TABLE sites ADD COLUMN ignore_tls BOOLEAN DEFAULT 0",
"ALTER TABLE sites ADD COLUMN paused BOOLEAN DEFAULT 0",
} }
for _, m := range migrations { for _, m := range migrations {
s.db.Exec(m) s.db.Exec(m)
@@ -90,7 +92,7 @@ func generateToken() string {
} }
func (s *SQLiteStore) GetSites() []models.Site { 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, 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") 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), COALESCE(paused, 0) FROM sites")
if err != nil { if err != nil {
return []models.Site{} return []models.Site{}
} }
@@ -98,7 +100,7 @@ func (s *SQLiteStore) GetSites() []models.Site {
var sites []models.Site var sites []models.Site
for rows.Next() { for rows.Next() {
var st models.Site 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, &st.Hostname, &st.Port, &st.Timeout, &st.Method, &st.Description, &st.ParentID, &st.AcceptedCodes, &st.DNSResolveType, &st.DNSServer, &st.IgnoreTLS) 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, &st.Paused)
sites = append(sites, st) sites = append(sites, st)
} }
return sites return sites
@@ -108,9 +110,9 @@ func (s *SQLiteStore) AddSite(site models.Site) {
if site.Type == "push" { if site.Type == "push" {
token = generateToken() token = generateToken()
} }
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 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, paused) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
site.Name, site.URL, site.Type, token, site.Interval, site.AlertID, site.CheckSSL, site.ExpiryThreshold, site.MaxRetries, 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) site.Hostname, site.Port, site.Timeout, site.Method, site.Description, site.ParentID, site.AcceptedCodes, site.DNSResolveType, site.DNSServer, site.IgnoreTLS, site.Paused)
} }
func (s *SQLiteStore) UpdateSite(site models.Site) { func (s *SQLiteStore) UpdateSite(site models.Site) {
var existingToken string var existingToken string
@@ -118,9 +120,12 @@ func (s *SQLiteStore) UpdateSite(site models.Site) {
if site.Type == "push" && existingToken == "" { if site.Type == "push" && existingToken == "" {
existingToken = generateToken() existingToken = generateToken()
} }
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=?", 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=?, paused=? WHERE id=?",
site.Name, site.URL, site.Type, existingToken, site.Interval, site.AlertID, site.CheckSSL, site.ExpiryThreshold, site.MaxRetries, 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) site.Hostname, site.Port, site.Timeout, site.Method, site.Description, site.ParentID, site.AcceptedCodes, site.DNSResolveType, site.DNSServer, site.IgnoreTLS, site.Paused, site.ID)
}
func (s *SQLiteStore) UpdateSitePaused(id int, paused bool) {
s.db.Exec("UPDATE sites SET paused=? WHERE id=?", paused, id)
} }
func (s *SQLiteStore) DeleteSite(id int) { func (s *SQLiteStore) DeleteSite(id int) {
s.db.Exec("DELETE FROM sites WHERE id=?", id) s.db.Exec("DELETE FROM sites WHERE id=?", id)
@@ -232,9 +237,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)) tx.Exec("INSERT INTO alerts (id, name, type, settings) VALUES (?, ?, ?, ?)", a.ID, a.Name, a.Type, string(jsonBytes))
} }
for _, st := range data.Sites { for _, st := range data.Sites {
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 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, paused) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
st.ID, st.Name, st.URL, st.Type, st.Token, st.Interval, st.AlertID, st.CheckSSL, st.ExpiryThreshold, st.MaxRetries, 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) st.Hostname, st.Port, st.Timeout, st.Method, st.Description, st.ParentID, st.AcceptedCodes, st.DNSResolveType, st.DNSServer, st.IgnoreTLS, st.Paused)
} }
return tx.Commit() return tx.Commit()
+1
View File
@@ -11,6 +11,7 @@ type Store interface {
GetSites() []models.Site GetSites() []models.Site
AddSite(site models.Site) AddSite(site models.Site)
UpdateSite(site models.Site) UpdateSite(site models.Site)
UpdateSitePaused(id int, paused bool)
DeleteSite(id int) DeleteSite(id int)
// Alerts // Alerts
+9 -13
View File
@@ -26,8 +26,6 @@ var (
alertBorderStyle = lipgloss.NewStyle(). alertBorderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#444")) Foreground(lipgloss.Color("#444"))
alertColWidths = []int{4, 16, 10, 36}
) )
type alertFormData struct { type alertFormData struct {
@@ -120,27 +118,25 @@ func (m Model) viewAlertsTab() string {
}) })
} }
tableWidth := m.termWidth - 6
if tableWidth < 40 {
tableWidth = 40
}
t := table.New(). t := table.New().
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderStyle(alertBorderStyle). BorderStyle(alertBorderStyle).
Width(tableWidth).
Headers("ID", "NAME", "TYPE", "CONFIG"). Headers("ID", "NAME", "TYPE", "CONFIG").
Rows(rows...). Rows(rows...).
StyleFunc(func(row, col int) lipgloss.Style { StyleFunc(func(row, col int) lipgloss.Style {
if row == table.HeaderRow { if row == table.HeaderRow {
s := alertHeaderStyle return alertHeaderStyle
if col < len(alertColWidths) {
s = s.Width(alertColWidths[col])
} }
return s
}
s := alertCellStyle
if row == selectedVisual { if row == selectedVisual {
s = alertSelectedStyle return alertSelectedStyle
} }
if col < len(alertColWidths) { return alertCellStyle
s = s.Width(alertColWidths[col])
}
return s
}) })
return "\n" + t.Render() return "\n" + t.Render()
+14 -15
View File
@@ -34,8 +34,6 @@ var (
siteBorderStyle = lipgloss.NewStyle(). siteBorderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#444")) Foreground(lipgloss.Color("#444"))
siteColWidths = []int{4, 14, 6, 8, 9, 8, 20, 10, 6}
) )
type siteFormData struct { type siteFormData struct {
@@ -195,7 +193,10 @@ func fmtRetries(site models.Site) string {
return s return s
} }
func fmtStatus(status string) string { func fmtStatus(status string, paused bool) string {
if paused {
return warnStyle.Render("PAUSED")
}
switch { switch {
case status == "DOWN" || status == "SSL EXP": case status == "DOWN" || status == "SSL EXP":
return dangerStyle.Render(status) return dangerStyle.Render(status)
@@ -236,7 +237,7 @@ func (m Model) viewSitesTab() string {
strconv.Itoa(site.ID), strconv.Itoa(site.ID),
m.zones.Mark(fmt.Sprintf("site-%d", i), limitStr(site.Name, 13)), m.zones.Mark(fmt.Sprintf("site-%d", i), limitStr(site.Name, 13)),
site.Type, site.Type,
fmtStatus(site.Status), fmtStatus(site.Status, site.Paused),
fmtLatency(site.Latency), fmtLatency(site.Latency),
fmtUptime(hist.TotalChecks, hist.UpChecks), fmtUptime(hist.TotalChecks, hist.UpChecks),
spark, spark,
@@ -245,27 +246,25 @@ func (m Model) viewSitesTab() string {
}) })
} }
tableWidth := m.termWidth - 6
if tableWidth < 40 {
tableWidth = 40
}
t := table.New(). t := table.New().
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderStyle(siteBorderStyle). BorderStyle(siteBorderStyle).
Width(tableWidth).
Headers("ID", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRY"). Headers("ID", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRY").
Rows(rows...). Rows(rows...).
StyleFunc(func(row, col int) lipgloss.Style { StyleFunc(func(row, col int) lipgloss.Style {
if row == table.HeaderRow { if row == table.HeaderRow {
s := siteHeaderStyle return siteHeaderStyle
if col < len(siteColWidths) {
s = s.Width(siteColWidths[col])
} }
return s
}
s := siteCellStyle
if row == selectedVisual { if row == selectedVisual {
s = siteSelectedStyle return siteSelectedStyle
} }
if col < len(siteColWidths) { return siteCellStyle
s = s.Width(siteColWidths[col])
}
return s
}) })
return "\n" + t.Render() return "\n" + t.Render()
+9 -13
View File
@@ -26,8 +26,6 @@ var (
userBorderStyle = lipgloss.NewStyle(). userBorderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#444")) Foreground(lipgloss.Color("#444"))
userColWidths = []int{4, 16, 10, 44}
) )
type userFormData struct { type userFormData struct {
@@ -73,27 +71,25 @@ func (m Model) viewUsersTab() string {
}) })
} }
tableWidth := m.termWidth - 6
if tableWidth < 40 {
tableWidth = 40
}
t := table.New(). t := table.New().
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderStyle(userBorderStyle). BorderStyle(userBorderStyle).
Width(tableWidth).
Headers("ID", "USERNAME", "ROLE", "PUBLIC KEY"). Headers("ID", "USERNAME", "ROLE", "PUBLIC KEY").
Rows(rows...). Rows(rows...).
StyleFunc(func(row, col int) lipgloss.Style { StyleFunc(func(row, col int) lipgloss.Style {
if row == table.HeaderRow { if row == table.HeaderRow {
s := userHeaderStyle return userHeaderStyle
if col < len(userColWidths) {
s = s.Width(userColWidths[col])
} }
return s
}
s := userCellStyle
if row == selectedVisual { if row == selectedVisual {
s = userSelectedStyle return userSelectedStyle
} }
if col < len(userColWidths) { return userCellStyle
s = s.Width(userColWidths[col])
}
return s
}) })
return "\n" + t.Render() return "\n" + t.Render()
+21 -3
View File
@@ -19,7 +19,7 @@ import (
) )
var ( var (
subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"}) subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#9ca0b0", Dark: "#565f89"})
specialStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}) specialStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"})
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F0E442", Dark: "#F0E442"}) warnStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F0E442", Dark: "#F0E442"})
dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F25D94", Dark: "#F25D94"}) dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F25D94", Dark: "#F25D94"})
@@ -48,6 +48,8 @@ type Model struct {
cursor int cursor int
tableOffset int tableOffset int
maxTableRows int maxTableRows int
termWidth int
termHeight int
editID int editID int
editToken string editToken string
@@ -126,6 +128,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.termWidth = msg.Width
m.termHeight = msg.Height
m.maxTableRows = msg.Height - 12 m.maxTableRows = msg.Height - 12
if m.maxTableRows < 1 { if m.maxTableRows < 1 {
m.maxTableRows = 1 m.maxTableRows = 1
@@ -255,6 +259,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.state = stateFormUser m.state = stateFormUser
return m, m.initUserHuhForm() return m, m.initUserHuhForm()
} }
case "p":
if m.currentTab == 0 && len(m.sites) > 0 {
site := m.sites[m.cursor]
monitor.ToggleSitePause(site.ID)
site.Paused = !site.Paused
if store.Get() != nil {
store.Get().UpdateSitePaused(site.ID, site.Paused)
}
m.refreshData()
}
case "d", "backspace": case "d", "backspace":
if m.currentTab == 1 && len(m.alerts) > 0 { if m.currentTab == 1 && len(m.alerts) > 0 {
store.Get().DeleteAlert(m.alerts[m.cursor].ID) store.Get().DeleteAlert(m.alerts[m.cursor].ID)
@@ -476,11 +490,15 @@ func (m Model) viewDashboard() string {
} }
} }
footer := subtleStyle.Render("\n[n] New [e/Enter] Edit [d] Delete [Tab/Click] Switch [Ctrl+L] Clear [q] Quit") footer := subtleStyle.Render("\n[n] New [e/Enter] Edit [d] Delete [p] Pause [Tab/Click] Switch [Ctrl+L] Clear [q] Quit")
if m.currentTab == 3 { if m.currentTab == 3 {
footer = subtleStyle.Render("\n[n] Add User [d] Revoke [Tab/Click] Switch [Ctrl+L] Clear [q] Quit") footer = subtleStyle.Render("\n[n] Add User [d] Revoke [Tab/Click] Switch [Ctrl+L] Clear [q] Quit")
} }
return lipgloss.NewStyle().Padding(1, 2).Render(header + "\n" + content + "\n" + footer) s := lipgloss.NewStyle().Padding(1, 2)
if m.termHeight > 0 {
s = s.MaxHeight(m.termHeight)
}
return s.Render(header + "\n" + content + "\n" + footer)
} }
func limitStr(text string, max int) string { func limitStr(text string, max int) string {