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
|
DNSResolveType string
|
||||||
DNSServer string
|
DNSServer string
|
||||||
IgnoreTLS bool
|
IgnoreTLS bool
|
||||||
|
Paused bool
|
||||||
|
|
||||||
FailureCount int
|
FailureCount int
|
||||||
Status string
|
Status string
|
||||||
|
|||||||
@@ -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
@@ -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>`
|
||||||
|
|||||||
@@ -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))")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user