feat(tui,status): add per-site pause, fix viewport, polish status page
Per-site pause: [p] key toggles pause for selected monitor in TUI. Paused monitors skip checks, persist to DB, show on status page. Status page: replace full-page reload with fetch-based DOM updates to eliminate scroll-jump on refresh. Add summary bar (UP/DOWN/PAUSED counts), stale-data indicator, and fix SSL EXP CSS class bug. TUI: constrain tables to terminal width via lipgloss .Width() to prevent row wrapping that pushed header off-screen. Add MaxHeight safety net. Bump subtle style from #383838 to #565f89 for readability on dark terminals.
This commit is contained in:
@@ -24,6 +24,7 @@ type Site struct {
|
||||
DNSResolveType string
|
||||
DNSServer string
|
||||
IgnoreTLS bool
|
||||
Paused bool
|
||||
|
||||
FailureCount int
|
||||
Status string
|
||||
|
||||
@@ -162,6 +162,7 @@ func UpdateSiteConfig(site models.Site) {
|
||||
s.DNSResolveType = site.DNSResolveType
|
||||
s.DNSServer = site.DNSServer
|
||||
s.IgnoreTLS = site.IgnoreTLS
|
||||
s.Paused = site.Paused
|
||||
LiveState[site.ID] = s
|
||||
}
|
||||
}
|
||||
@@ -173,10 +174,26 @@ func RemoveSite(id int) {
|
||||
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) {
|
||||
checkByID(id)
|
||||
for {
|
||||
// If paused, just sleep loop to keep goroutine alive but idle
|
||||
if !IsEngineActive() {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
@@ -189,6 +206,11 @@ func monitorRoutine(id int) {
|
||||
return
|
||||
}
|
||||
|
||||
if site.Paused {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
interval := site.Interval
|
||||
if interval < 5 {
|
||||
interval = 5
|
||||
@@ -206,7 +228,7 @@ func checkByID(id int) {
|
||||
Mutex.RLock()
|
||||
site, exists := LiveState[id]
|
||||
Mutex.RUnlock()
|
||||
if !exists {
|
||||
if !exists || site.Paused {
|
||||
return
|
||||
}
|
||||
switch site.Type {
|
||||
|
||||
+92
-13
@@ -148,7 +148,6 @@ func renderStatusPage(w http.ResponseWriter, title string) {
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Title}}</title>
|
||||
<meta http-equiv="refresh" content="5">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
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; }
|
||||
.DOWN { background: #f7768e; 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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{{.Title}}</h1>
|
||||
{{range .Sites}}
|
||||
<div class="card">
|
||||
<div class="info">
|
||||
<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 id="summary" class="summary"></div>
|
||||
<div id="stale" class="stale-bar"></div>
|
||||
<div id="cards"></div>
|
||||
<div style="text-align: center; margin-top: 40px; color: #565f89; font-size: 0.8em;">Powered by Go-Upkeep</div>
|
||||
</div>
|
||||
<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>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
@@ -47,7 +47,8 @@ func (p *PostgresStore) Init() error {
|
||||
accepted_codes TEXT DEFAULT '200-299',
|
||||
dns_resolve_type 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 (
|
||||
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_server TEXT DEFAULT ''",
|
||||
"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 {
|
||||
p.db.Exec(m)
|
||||
@@ -83,7 +85,7 @@ func (p *PostgresStore) Init() error {
|
||||
|
||||
// ... [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, 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 {
|
||||
return []models.Site{}
|
||||
}
|
||||
@@ -92,7 +94,7 @@ func (p *PostgresStore) GetSites() []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,
|
||||
&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)
|
||||
}
|
||||
return sites
|
||||
@@ -102,9 +104,9 @@ func (p *PostgresStore) AddSite(site models.Site) {
|
||||
if site.Type == "push" {
|
||||
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.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) {
|
||||
var existingToken string
|
||||
@@ -112,9 +114,12 @@ func (p *PostgresStore) UpdateSite(site models.Site) {
|
||||
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, 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.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) 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))
|
||||
}
|
||||
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.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))")
|
||||
|
||||
@@ -49,7 +49,8 @@ func (s *SQLiteStore) Init() error {
|
||||
accepted_codes TEXT DEFAULT '200-299',
|
||||
dns_resolve_type 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 (
|
||||
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_server TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN ignore_tls BOOLEAN DEFAULT 0",
|
||||
"ALTER TABLE sites ADD COLUMN paused BOOLEAN DEFAULT 0",
|
||||
}
|
||||
for _, m := range migrations {
|
||||
s.db.Exec(m)
|
||||
@@ -90,7 +92,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, 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 {
|
||||
return []models.Site{}
|
||||
}
|
||||
@@ -98,7 +100,7 @@ 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, &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)
|
||||
}
|
||||
return sites
|
||||
@@ -108,9 +110,9 @@ func (s *SQLiteStore) AddSite(site models.Site) {
|
||||
if site.Type == "push" {
|
||||
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.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) {
|
||||
var existingToken string
|
||||
@@ -118,9 +120,12 @@ func (s *SQLiteStore) UpdateSite(site models.Site) {
|
||||
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=?, 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.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) {
|
||||
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))
|
||||
}
|
||||
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.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()
|
||||
|
||||
@@ -11,6 +11,7 @@ type Store interface {
|
||||
GetSites() []models.Site
|
||||
AddSite(site models.Site)
|
||||
UpdateSite(site models.Site)
|
||||
UpdateSitePaused(id int, paused bool)
|
||||
DeleteSite(id int)
|
||||
|
||||
// Alerts
|
||||
|
||||
@@ -26,8 +26,6 @@ var (
|
||||
|
||||
alertBorderStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#444"))
|
||||
|
||||
alertColWidths = []int{4, 16, 10, 36}
|
||||
)
|
||||
|
||||
type alertFormData struct {
|
||||
@@ -120,27 +118,25 @@ func (m Model) viewAlertsTab() string {
|
||||
})
|
||||
}
|
||||
|
||||
tableWidth := m.termWidth - 6
|
||||
if tableWidth < 40 {
|
||||
tableWidth = 40
|
||||
}
|
||||
|
||||
t := table.New().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderStyle(alertBorderStyle).
|
||||
Width(tableWidth).
|
||||
Headers("ID", "NAME", "TYPE", "CONFIG").
|
||||
Rows(rows...).
|
||||
StyleFunc(func(row, col int) lipgloss.Style {
|
||||
if row == table.HeaderRow {
|
||||
s := alertHeaderStyle
|
||||
if col < len(alertColWidths) {
|
||||
s = s.Width(alertColWidths[col])
|
||||
}
|
||||
return s
|
||||
return alertHeaderStyle
|
||||
}
|
||||
s := alertCellStyle
|
||||
if row == selectedVisual {
|
||||
s = alertSelectedStyle
|
||||
return alertSelectedStyle
|
||||
}
|
||||
if col < len(alertColWidths) {
|
||||
s = s.Width(alertColWidths[col])
|
||||
}
|
||||
return s
|
||||
return alertCellStyle
|
||||
})
|
||||
|
||||
return "\n" + t.Render()
|
||||
|
||||
+14
-15
@@ -34,8 +34,6 @@ var (
|
||||
|
||||
siteBorderStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#444"))
|
||||
|
||||
siteColWidths = []int{4, 14, 6, 8, 9, 8, 20, 10, 6}
|
||||
)
|
||||
|
||||
type siteFormData struct {
|
||||
@@ -195,7 +193,10 @@ func fmtRetries(site models.Site) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func fmtStatus(status string) string {
|
||||
func fmtStatus(status string, paused bool) string {
|
||||
if paused {
|
||||
return warnStyle.Render("PAUSED")
|
||||
}
|
||||
switch {
|
||||
case status == "DOWN" || status == "SSL EXP":
|
||||
return dangerStyle.Render(status)
|
||||
@@ -236,7 +237,7 @@ func (m Model) viewSitesTab() string {
|
||||
strconv.Itoa(site.ID),
|
||||
m.zones.Mark(fmt.Sprintf("site-%d", i), limitStr(site.Name, 13)),
|
||||
site.Type,
|
||||
fmtStatus(site.Status),
|
||||
fmtStatus(site.Status, site.Paused),
|
||||
fmtLatency(site.Latency),
|
||||
fmtUptime(hist.TotalChecks, hist.UpChecks),
|
||||
spark,
|
||||
@@ -245,27 +246,25 @@ func (m Model) viewSitesTab() string {
|
||||
})
|
||||
}
|
||||
|
||||
tableWidth := m.termWidth - 6
|
||||
if tableWidth < 40 {
|
||||
tableWidth = 40
|
||||
}
|
||||
|
||||
t := table.New().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderStyle(siteBorderStyle).
|
||||
Width(tableWidth).
|
||||
Headers("ID", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRY").
|
||||
Rows(rows...).
|
||||
StyleFunc(func(row, col int) lipgloss.Style {
|
||||
if row == table.HeaderRow {
|
||||
s := siteHeaderStyle
|
||||
if col < len(siteColWidths) {
|
||||
s = s.Width(siteColWidths[col])
|
||||
}
|
||||
return s
|
||||
return siteHeaderStyle
|
||||
}
|
||||
s := siteCellStyle
|
||||
if row == selectedVisual {
|
||||
s = siteSelectedStyle
|
||||
return siteSelectedStyle
|
||||
}
|
||||
if col < len(siteColWidths) {
|
||||
s = s.Width(siteColWidths[col])
|
||||
}
|
||||
return s
|
||||
return siteCellStyle
|
||||
})
|
||||
|
||||
return "\n" + t.Render()
|
||||
|
||||
@@ -26,8 +26,6 @@ var (
|
||||
|
||||
userBorderStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#444"))
|
||||
|
||||
userColWidths = []int{4, 16, 10, 44}
|
||||
)
|
||||
|
||||
type userFormData struct {
|
||||
@@ -73,27 +71,25 @@ func (m Model) viewUsersTab() string {
|
||||
})
|
||||
}
|
||||
|
||||
tableWidth := m.termWidth - 6
|
||||
if tableWidth < 40 {
|
||||
tableWidth = 40
|
||||
}
|
||||
|
||||
t := table.New().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderStyle(userBorderStyle).
|
||||
Width(tableWidth).
|
||||
Headers("ID", "USERNAME", "ROLE", "PUBLIC KEY").
|
||||
Rows(rows...).
|
||||
StyleFunc(func(row, col int) lipgloss.Style {
|
||||
if row == table.HeaderRow {
|
||||
s := userHeaderStyle
|
||||
if col < len(userColWidths) {
|
||||
s = s.Width(userColWidths[col])
|
||||
}
|
||||
return s
|
||||
return userHeaderStyle
|
||||
}
|
||||
s := userCellStyle
|
||||
if row == selectedVisual {
|
||||
s = userSelectedStyle
|
||||
return userSelectedStyle
|
||||
}
|
||||
if col < len(userColWidths) {
|
||||
s = s.Width(userColWidths[col])
|
||||
}
|
||||
return s
|
||||
return userCellStyle
|
||||
})
|
||||
|
||||
return "\n" + t.Render()
|
||||
|
||||
+21
-3
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
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"})
|
||||
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F0E442", Dark: "#F0E442"})
|
||||
dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F25D94", Dark: "#F25D94"})
|
||||
@@ -48,6 +48,8 @@ type Model struct {
|
||||
cursor int
|
||||
tableOffset int
|
||||
maxTableRows int
|
||||
termWidth int
|
||||
termHeight int
|
||||
editID int
|
||||
editToken string
|
||||
|
||||
@@ -126,6 +128,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.termWidth = msg.Width
|
||||
m.termHeight = msg.Height
|
||||
m.maxTableRows = msg.Height - 12
|
||||
if m.maxTableRows < 1 {
|
||||
m.maxTableRows = 1
|
||||
@@ -255,6 +259,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.state = stateFormUser
|
||||
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":
|
||||
if m.currentTab == 1 && len(m.alerts) > 0 {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user