Compare commits
9
Commits
5687d8a4e7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0da87fac | ||
|
|
a21bf2c215 | ||
|
|
16f88d4401 | ||
|
|
4b74bc01a7 | ||
|
|
ca45a90930 | ||
|
|
b94343722f | ||
|
|
c14f08d4b1 | ||
|
|
93c9bf27fa | ||
|
|
49a20a946f |
No files matched your search
+26
-11
@@ -11,17 +11,25 @@
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
padding: 1rem 2rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
text-align: left;
|
||||
margin-bottom: 0rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #1e2433;
|
||||
}
|
||||
@@ -32,6 +40,7 @@
|
||||
letter-spacing: 0.05em;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header h1 span { color: #e2e8f0; }
|
||||
@@ -124,9 +133,7 @@
|
||||
}
|
||||
|
||||
.logout-form {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
@@ -380,10 +387,13 @@
|
||||
<body>
|
||||
|
||||
<% if authenticated %>
|
||||
<form method="post" class="logout-form">
|
||||
<input type="hidden" name="action" value="logout">
|
||||
<button type="submit" class="logout-btn">Log Out</button>
|
||||
</form>
|
||||
<div class="header-controls">
|
||||
<div class="last-updated" id="last-updated"></div>
|
||||
<form method="post" class="logout-form">
|
||||
<input type="hidden" name="action" value="logout">
|
||||
<button type="submit" class="logout-btn">Log Out</button>
|
||||
</form>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<header>
|
||||
@@ -400,6 +410,11 @@
|
||||
})
|
||||
.then(function(html) {
|
||||
document.getElementById("content").innerHTML = html;
|
||||
var now = new Date();
|
||||
var pad = function(n) { return String(n).padStart(2, "0"); };
|
||||
var stamp = now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate()) +
|
||||
" " + pad(now.getHours()) + ":" + pad(now.getMinutes());
|
||||
document.getElementById("last-updated").textContent = "Updated: " + stamp;
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById("content").innerHTML = "Error";
|
||||
|
||||
@@ -4,6 +4,7 @@ require "socket"
|
||||
require "fileutils"
|
||||
require "json"
|
||||
require "shellwords"
|
||||
require "etc"
|
||||
|
||||
SOCKET_PATH = "/run/malpd/malpd.sock"
|
||||
|
||||
@@ -146,8 +147,10 @@ def read_vm_dominfo(name)
|
||||
end
|
||||
|
||||
def read_vm_info(name)
|
||||
ssh_out = `ssh -Ti /root/.ssh/malp-vm-key malp@#{name}`
|
||||
JSON.parse(ssh_out)
|
||||
ssh_out = `ssh -o ConnectTimeout=1 -Ti /root/.ssh/malp-vm-key malp@#{name} 2>/dev/null`
|
||||
if ssh_out != ""
|
||||
JSON.parse(ssh_out)
|
||||
end
|
||||
end
|
||||
|
||||
def read_virtual_machines
|
||||
@@ -158,7 +161,7 @@ def read_virtual_machines
|
||||
|
||||
names.map do |name|
|
||||
dom_info = read_vm_dominfo(name)
|
||||
vm_info = read_vm_info(name)
|
||||
vm_info = read_vm_info(name) || {}
|
||||
{
|
||||
"type" => "vm",
|
||||
"name" => name,
|
||||
@@ -166,6 +169,143 @@ def read_virtual_machines
|
||||
end
|
||||
end
|
||||
|
||||
def read_os_id
|
||||
return nil unless File.exist?("/etc/os-release")
|
||||
File.readlines("/etc/os-release").each do |line|
|
||||
if line =~ /^ID=(.+)$/
|
||||
return $1.strip.delete('"').downcase
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def apt_updates
|
||||
out = `apt-get -s -o Debug::NoLocking=true upgrade 2>/dev/null`
|
||||
count = 0
|
||||
out.each_line do |line|
|
||||
count += 1 if line.start_with?("Inst ")
|
||||
end
|
||||
count
|
||||
end
|
||||
|
||||
def apt_reboot_pending
|
||||
File.exist?("/var/run/reboot-required")
|
||||
end
|
||||
|
||||
def dnf_updates
|
||||
out = `dnf -q check-update 2>/dev/null`
|
||||
status = $?.exitstatus
|
||||
return 0 if status == 0
|
||||
return nil unless status == 100
|
||||
count = 0
|
||||
out.each_line do |line|
|
||||
line = line.strip
|
||||
next if line.empty?
|
||||
next if line.start_with?("Obsoleting", "Last metadata")
|
||||
count += 1 if line.split(/\s+/).size >= 3
|
||||
end
|
||||
count
|
||||
end
|
||||
|
||||
def dnf_reboot_pending
|
||||
`dnf needs-restarting -r >/dev/null 2>&1`
|
||||
$?.exitstatus == 1
|
||||
end
|
||||
|
||||
def read_server_info
|
||||
info = { "type" => "server" }
|
||||
|
||||
if File.exist?("/etc/os-release")
|
||||
File.readlines("/etc/os-release").each do |line|
|
||||
if line =~ /^PRETTY_NAME="(.+)"$/
|
||||
info["os_name"] = $1
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case read_os_id
|
||||
when "ubuntu", "debian"
|
||||
info["updates"] = apt_updates
|
||||
info["reboot_pending"] = apt_reboot_pending
|
||||
when "almalinux", "rocky", "rhel", "centos", "fedora"
|
||||
info["updates"] = dnf_updates
|
||||
info["reboot_pending"] = dnf_reboot_pending
|
||||
end
|
||||
|
||||
out = `stat -c %W / 2>/dev/null`.strip
|
||||
if out =~ /^\d+$/ && out.to_i > 0
|
||||
info["install_date"] = Time.at(out.to_i).strftime("%Y-%m-%d")
|
||||
elsif File.exist?("/etc/machine-id")
|
||||
info["install_date"] = File.mtime("/etc/machine-id").strftime("%Y-%m-%d")
|
||||
end
|
||||
|
||||
if File.exist?("/proc/cpuinfo")
|
||||
cpuinfo = File.read("/proc/cpuinfo")
|
||||
if cpuinfo =~ /^model name\s*:\s*(.+)$/
|
||||
info["cpu_model"] = $1.strip
|
||||
end
|
||||
cores = cpuinfo.scan(/^processor\s*:/).size
|
||||
info["cpu_cores"] = cores if cores > 0
|
||||
end
|
||||
|
||||
if File.exist?("/proc/meminfo")
|
||||
mem = {}
|
||||
File.read("/proc/meminfo").each_line do |line|
|
||||
if line =~ /^(\w+):\s+(\d+)/
|
||||
mem[$1] = $2.to_i * 1024
|
||||
end
|
||||
end
|
||||
if (total = mem["MemTotal"])
|
||||
available = mem["MemAvailable"] ||
|
||||
(mem["MemFree"].to_i + mem["Buffers"].to_i + mem["Cached"].to_i)
|
||||
info["mem_total"] = total
|
||||
info["mem_used"] = total - available
|
||||
end
|
||||
end
|
||||
|
||||
info
|
||||
end
|
||||
|
||||
def read_backups
|
||||
paths = [
|
||||
["rsync", "/backup/last-backup"],
|
||||
["gmail", "/backup/gmail/new"]
|
||||
]
|
||||
entries = paths.map do |label, path|
|
||||
mtime = (File.mtime(path).to_i rescue nil)
|
||||
{ "label" => label, "path" => path, "mtime" => mtime }
|
||||
end
|
||||
{ "type" => "backup", "entries" => entries }
|
||||
end
|
||||
|
||||
def read_mail
|
||||
users = []
|
||||
|
||||
["root", "josh"].each do |user|
|
||||
begin
|
||||
home = Etc.getpwnam(user).dir
|
||||
rescue ArgumentError
|
||||
next
|
||||
end
|
||||
|
||||
count = 0
|
||||
mbox = ["/var/mail/#{user}", "/var/spool/mail/#{user}"].find { |p| File.exist?(p) }
|
||||
if mbox
|
||||
content = File.read(mbox) rescue ""
|
||||
count = content.scan(/^From /).size
|
||||
end
|
||||
|
||||
users << {
|
||||
"user" => user,
|
||||
"count" => count,
|
||||
"dead_letter" => File.exist?(File.join(home, "dead.letter"))
|
||||
}
|
||||
end
|
||||
|
||||
{ "type" => "mail", "users" => users }
|
||||
end
|
||||
|
||||
def read_ups
|
||||
out = `apcaccess 2>/dev/null`
|
||||
return nil unless $?.success? && !out.empty?
|
||||
@@ -232,6 +372,10 @@ def handle_info(conn)
|
||||
info.concat(read_filesystems)
|
||||
info.concat(read_virtual_machines)
|
||||
|
||||
info << read_server_info
|
||||
info << read_mail
|
||||
info << read_backups
|
||||
|
||||
if (ups = read_ups)
|
||||
info << ups
|
||||
end
|
||||
|
||||
+290
-90
@@ -7,6 +7,14 @@ require "json"
|
||||
require "securerandom"
|
||||
require "socket"
|
||||
|
||||
def human_age(seconds)
|
||||
return "unknown" if seconds.nil?
|
||||
return "just now" if seconds < 60
|
||||
return "#{seconds / 60} min ago" if seconds < 3600
|
||||
return "#{seconds / 3600} h ago" if seconds < 86400
|
||||
"#{seconds / 86400} d ago"
|
||||
end
|
||||
|
||||
def human_capacity(bytes)
|
||||
units = ["B", "KB", "MB", "GB", "TB", "PB"]
|
||||
i = 0
|
||||
@@ -112,6 +120,177 @@ if cgi.params.key?("content")
|
||||
drives = info.select { |e| e["type"] == "drive" }
|
||||
filesystems = info.select { |e| e["type"] == "filesystem" }
|
||||
vms = info.select { |e| e["type"] == "vm" }
|
||||
server = info.find { |e| e["type"] == "server" }
|
||||
|
||||
html << %(<div>)
|
||||
|
||||
if server
|
||||
srv_stats = []
|
||||
if updates = server["updates"]
|
||||
cls = updates == 0 ? "ok" : "warn"
|
||||
srv_stats << [cls, "#{updates} update#{updates == 1 ? "" : "s"}"]
|
||||
end
|
||||
if server["reboot_pending"]
|
||||
srv_stats << ["warn", "Reboot pending"]
|
||||
end
|
||||
srv_cls = srv_stats.any? { |c, _| c == "bad" } ? "bad" :
|
||||
srv_stats.any? { |c, _| c == "warn" } ? "warn" : "ok"
|
||||
|
||||
html << %(<div class="section-label">Server</div>)
|
||||
html << %(<div class="card #{srv_cls}"><div class="card-header"><span class="card-title">Server Info</span></div><div class="subitems">)
|
||||
|
||||
if server["os_name"]
|
||||
html << <<~HTML
|
||||
<div class="subitem">
|
||||
<div class="subitem-left"><div class="dot ok"></div><span class="subitem-name">OS</span></div>
|
||||
<span class="subitem-value ok">#{CGI.escapeHTML(server["os_name"])}</span>
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
if server["install_date"]
|
||||
html << <<~HTML
|
||||
<div class="subitem">
|
||||
<div class="subitem-left"><div class="dot ok"></div><span class="subitem-name">Installed</span></div>
|
||||
<span class="subitem-value ok">#{CGI.escapeHTML(server["install_date"])}</span>
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
cpu_model = server["cpu_model"]
|
||||
cpu_cores = server["cpu_cores"]
|
||||
if cpu_model || cpu_cores
|
||||
cpu_text = [
|
||||
cpu_model,
|
||||
cpu_cores ? "#{cpu_cores} thread#{cpu_cores == 1 ? "" : "s"}" : nil
|
||||
].compact.join(" · ")
|
||||
html << <<~HTML
|
||||
<div class="subitem">
|
||||
<div class="subitem-left"><div class="dot ok"></div><span class="subitem-name">CPU</span></div>
|
||||
<span class="subitem-value ok">#{CGI.escapeHTML(cpu_text)}</span>
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
mem_total = server["mem_total"].to_i
|
||||
mem_used = server["mem_used"].to_i
|
||||
if mem_total > 0
|
||||
mem_pct_f = mem_used * 100.0 / mem_total
|
||||
mem_pct = mem_pct_f.round
|
||||
mem_cls = mem_pct_f >= 90 ? "bad" : mem_pct_f >= 75 ? "warn" : "ok"
|
||||
html << <<~HTML
|
||||
<div class="subitem" style="flex-direction:column;align-items:stretch;gap:0.35rem;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div class="subitem-left"><div class="dot #{mem_cls}"></div><span class="subitem-name">Memory</span></div>
|
||||
<span class="subitem-value #{mem_cls}">#{mem_pct}% · #{CGI.escapeHTML(human_capacity(mem_used))} / #{CGI.escapeHTML(human_capacity(mem_total))}</span>
|
||||
</div>
|
||||
<div class="bar-track"><div class="bar-fill #{mem_cls}" style="width:#{mem_pct}%"></div></div>
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
html << %(</div>)
|
||||
|
||||
if srv_stats.any?
|
||||
srv_stats_html = srv_stats.map { |c, txt|
|
||||
%(<span class="stat #{c}">#{CGI.escapeHTML(txt)}</span>)
|
||||
}.join
|
||||
html << %(<div class="drive-stats" style="margin-top:0.45rem;padding:0 0.6rem;">#{srv_stats_html}</div>)
|
||||
end
|
||||
|
||||
html << %(</div>)
|
||||
end
|
||||
|
||||
if filesystems.any?
|
||||
total_size = filesystems.sum { |e| e["size"].to_i }
|
||||
total_used = filesystems.sum { |e| e["used"].to_i }
|
||||
total_pct_f = total_size > 0 ? (total_used * 100.0 / total_size) : 0.0
|
||||
total_pct = total_pct_f.round
|
||||
total_cls = total_pct_f >= 90 ? "bad" : total_pct_f >= 75 ? "warn" : "ok"
|
||||
|
||||
fs_rows = filesystems.map do |entry|
|
||||
mount = CGI.escapeHTML(entry["mount"].to_s)
|
||||
source = CGI.escapeHTML(entry["source"].to_s)
|
||||
fstype = CGI.escapeHTML(entry["fstype"].to_s)
|
||||
size_b = entry["size"].to_i
|
||||
used_b = entry["used"].to_i
|
||||
pct_f = size_b > 0 ? (used_b * 100.0 / size_b) : 0.0
|
||||
pct = pct_f.round
|
||||
space_cls = pct_f >= 90 ? "bad" : pct_f >= 75 ? "warn" : "ok"
|
||||
used_h = CGI.escapeHTML(human_capacity(used_b))
|
||||
size_h = CGI.escapeHTML(human_capacity(size_b))
|
||||
|
||||
fs_stats = []
|
||||
inode_cls = "ok"
|
||||
if (itot = entry["inode_total"].to_i) > 0
|
||||
iused = entry["inode_used"].to_i
|
||||
ipct_f = iused * 100.0 / itot
|
||||
inode_cls = ipct_f >= 90 ? "bad" : ipct_f >= 75 ? "warn" : "ok"
|
||||
fs_stats << [inode_cls, "Inodes #{ipct_f.round}%"]
|
||||
end
|
||||
fs_stats << ["warn", "READ-ONLY"] if entry["readonly"]
|
||||
|
||||
severities = [space_cls, inode_cls]
|
||||
row_cls = severities.include?("bad") ? "bad" :
|
||||
severities.include?("warn") ? "warn" : "ok"
|
||||
|
||||
stats_html = fs_stats.map { |c, txt|
|
||||
%(<span class="stat #{c}">#{CGI.escapeHTML(txt)}</span>)
|
||||
}.join
|
||||
stats_block = fs_stats.empty? ? "" :
|
||||
%(<div class="drive-stats">#{stats_html}</div>)
|
||||
|
||||
row_html = <<~HTML
|
||||
<div class="subitem" style="flex-direction:column;align-items:stretch;gap:0.35rem;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div class="subitem-left">
|
||||
<div class="dot #{space_cls}"></div>
|
||||
<span class="subitem-name">#{mount} · #{source}</span>
|
||||
<span class="card-capacity">#{fstype}</span>
|
||||
</div>
|
||||
<span class="subitem-value #{space_cls}">#{pct}% · #{used_h} / #{size_h}</span>
|
||||
</div>
|
||||
<div class="bar-track"><div class="bar-fill #{space_cls}" style="width:#{pct}%"></div></div>
|
||||
#{stats_block}
|
||||
</div>
|
||||
HTML
|
||||
|
||||
[row_html, row_cls]
|
||||
end
|
||||
|
||||
severities = fs_rows.map { |_, c| c }
|
||||
card_cls = severities.include?("bad") ? "bad" :
|
||||
severities.include?("warn") ? "warn" : "ok"
|
||||
|
||||
total_used_h = CGI.escapeHTML(human_capacity(total_used))
|
||||
total_size_h = CGI.escapeHTML(human_capacity(total_size))
|
||||
|
||||
html << %(<div class="section-label">File Systems</div>)
|
||||
html << <<~HTML
|
||||
<div class="card #{card_cls}">
|
||||
<div class="card-header">
|
||||
<span class="card-title">File Systems</span>
|
||||
<span class="badge #{total_cls}">#{total_pct}%</span>
|
||||
</div>
|
||||
<div class="subitems">
|
||||
HTML
|
||||
|
||||
fs_rows.each { |row_html, _| html << row_html }
|
||||
|
||||
html << %(</div>)
|
||||
html << <<~HTML
|
||||
<div style="margin-top:0.75rem;padding-top:0.6rem;border-top:1px solid #1e2433;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.3rem;">
|
||||
<span style="font-size:0.72rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Total</span>
|
||||
<span class="subitem-value #{total_cls}">#{total_pct}% · #{total_used_h} / #{total_size_h}</span>
|
||||
</div>
|
||||
<div class="bar-track" style="height:6px;"><div class="bar-fill #{total_cls}" style="width:#{total_pct}%"></div></div>
|
||||
</div>
|
||||
HTML
|
||||
html << %(</div>)
|
||||
end
|
||||
|
||||
html << %(</div>)
|
||||
|
||||
html << %(<div class="span-3">)
|
||||
|
||||
@@ -254,7 +433,12 @@ if cgi.params.key?("content")
|
||||
end
|
||||
|
||||
upses = info.select { |e| e["type"] == "ups" }
|
||||
if drives.any? || upses.any?
|
||||
mail = info.find { |e| e["type"] == "mail" }
|
||||
mail_visible = mail && (mail["users"] || []).any?
|
||||
backup = info.find { |e| e["type"] == "backup" }
|
||||
backup_visible = backup && (backup["entries"] || []).any?
|
||||
|
||||
if drives.any? || upses.any? || mail_visible || backup_visible
|
||||
html << %(<div style="display:grid;grid-template-columns:2fr 1fr;gap:1rem;align-items:start;margin-top:1rem;">)
|
||||
end
|
||||
|
||||
@@ -296,116 +480,132 @@ if cgi.params.key?("content")
|
||||
html << %(</div>)
|
||||
end
|
||||
|
||||
upses.each do |ups|
|
||||
status = ups["status"].to_s
|
||||
online = status.include?("ONLINE")
|
||||
card_cls = online ? "ok" : "bad"
|
||||
|
||||
model = ups["model"] ? "APC " + CGI.escapeHTML(ups["model"]) : "UPS"
|
||||
|
||||
stats = []
|
||||
stats << [online ? "ok" : "bad", CGI.escapeHTML(status)] unless status.empty?
|
||||
|
||||
if (charge = ups["charge"])
|
||||
cls = charge >= 80 ? "ok" : charge >= 50 ? "warn" : "bad"
|
||||
stats << [cls, "Charge #{charge.round}%"]
|
||||
end
|
||||
if (timeleft = ups["timeleft"])
|
||||
cls = timeleft >= 10 ? "ok" : timeleft >= 5 ? "warn" : "bad"
|
||||
stats << [cls, "#{timeleft.round(1)} min left"]
|
||||
end
|
||||
if (load = ups["load"])
|
||||
cls = load >= 90 ? "bad" : load >= 70 ? "warn" : "ok"
|
||||
stats << [cls, "Load #{load.round}%"]
|
||||
end
|
||||
if (lv = ups["line_voltage"])
|
||||
stats << ["ok", "Line #{lv.round} V"]
|
||||
end
|
||||
if (bv = ups["battery_voltage"])
|
||||
stats << ["ok", "Batt #{bv.round(1)} V"]
|
||||
end
|
||||
|
||||
stats_html = stats.map { |c, txt|
|
||||
%(<span class="stat #{c}">#{txt}</span>)
|
||||
}.join
|
||||
stats_block = stats.empty? ? "" :
|
||||
%(<div class="drive-stats" style="margin-top:0.6rem;">#{stats_html}</div>)
|
||||
|
||||
if upses.any? || mail_visible || backup_visible
|
||||
html << %(<div>)
|
||||
html << %(<div class="section-label">UPS</div>)
|
||||
html << <<~HTML
|
||||
<div class="card #{card_cls}">
|
||||
<div class="card-header">
|
||||
<span class="card-title">#{model}</span>
|
||||
<span class="badge #{card_cls}">#{online ? "Online" : CGI.escapeHTML(status)}</span>
|
||||
</div>
|
||||
#{stats_block}
|
||||
</div>
|
||||
HTML
|
||||
html << %(</div>)
|
||||
end
|
||||
|
||||
if drives.any? || upses.any?
|
||||
html << %(</div>)
|
||||
end
|
||||
upses.each do |ups|
|
||||
status = ups["status"].to_s
|
||||
online = status.include?("ONLINE")
|
||||
card_cls = online ? "ok" : "bad"
|
||||
|
||||
html << %(</div>)
|
||||
model = ups["model"] ? "APC " + CGI.escapeHTML(ups["model"]) : "UPS"
|
||||
|
||||
if filesystems.any?
|
||||
html << %(<div>)
|
||||
html << %(<div class="section-label">File Systems</div>)
|
||||
html << %(<div class="stack">)
|
||||
filesystems.each do |entry|
|
||||
mount = CGI.escapeHTML(entry["mount"].to_s)
|
||||
source = CGI.escapeHTML(entry["source"].to_s)
|
||||
fstype = CGI.escapeHTML(entry["fstype"].to_s)
|
||||
size_b = entry["size"].to_i
|
||||
used_b = entry["used"].to_i
|
||||
pct_f = size_b > 0 ? (used_b * 100.0 / size_b) : 0.0
|
||||
pct = pct_f.round
|
||||
space_cls = pct_f >= 90 ? "bad" : pct_f >= 75 ? "warn" : "ok"
|
||||
used_h = CGI.escapeHTML(human_capacity(used_b))
|
||||
size_h = CGI.escapeHTML(human_capacity(size_b))
|
||||
stats = []
|
||||
stats << [online ? "ok" : "bad", CGI.escapeHTML(status)] unless status.empty?
|
||||
|
||||
fs_stats = []
|
||||
inode_cls = "ok"
|
||||
if (itot = entry["inode_total"].to_i) > 0
|
||||
iused = entry["inode_used"].to_i
|
||||
ipct_f = iused * 100.0 / itot
|
||||
inode_cls = ipct_f >= 90 ? "bad" : ipct_f >= 75 ? "warn" : "ok"
|
||||
fs_stats << [inode_cls, "Inodes #{ipct_f.round}%"]
|
||||
if (charge = ups["charge"])
|
||||
cls = charge >= 80 ? "ok" : charge >= 50 ? "warn" : "bad"
|
||||
stats << [cls, "Charge #{charge.round}%"]
|
||||
end
|
||||
if (timeleft = ups["timeleft"])
|
||||
cls = timeleft >= 10 ? "ok" : timeleft >= 5 ? "warn" : "bad"
|
||||
stats << [cls, "#{timeleft.round(1)} min left"]
|
||||
end
|
||||
if (load = ups["load"])
|
||||
cls = load >= 90 ? "bad" : load >= 70 ? "warn" : "ok"
|
||||
stats << [cls, "Load #{load.round}%"]
|
||||
end
|
||||
if (lv = ups["line_voltage"])
|
||||
stats << ["ok", "Line #{lv.round} V"]
|
||||
end
|
||||
if (bv = ups["battery_voltage"])
|
||||
stats << ["ok", "Batt #{bv.round(1)} V"]
|
||||
end
|
||||
fs_stats << ["warn", "READ-ONLY"] if entry["readonly"]
|
||||
|
||||
severities = [space_cls, inode_cls]
|
||||
card_cls = severities.include?("bad") ? "bad" :
|
||||
severities.include?("warn") ? "warn" : "ok"
|
||||
|
||||
stats_html = fs_stats.map { |c, txt|
|
||||
%(<span class="stat #{c}">#{CGI.escapeHTML(txt)}</span>)
|
||||
stats_html = stats.map { |c, txt|
|
||||
%(<span class="stat #{c}">#{txt}</span>)
|
||||
}.join
|
||||
stats_block = fs_stats.empty? ? "" :
|
||||
stats_block = stats.empty? ? "" :
|
||||
%(<div class="drive-stats" style="margin-top:0.6rem;">#{stats_html}</div>)
|
||||
|
||||
html << %(<div class="section-label">UPS</div>)
|
||||
html << <<~HTML
|
||||
<div class="card #{card_cls}">
|
||||
<div class="card-header">
|
||||
<span class="card-title">#{mount} <span class="card-capacity">#{fstype}</span></span>
|
||||
<span class="badge #{space_cls}">#{pct}%</span>
|
||||
<span class="card-title">#{model}</span>
|
||||
<span class="badge #{card_cls}">#{online ? "Online" : CGI.escapeHTML(status)}</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.5rem;margin-bottom:0.3rem;">
|
||||
<span style="font-size:0.72rem;color:#64748b;font-weight:600;">#{source}</span>
|
||||
<span class="subitem-value #{space_cls}">#{used_h} / #{size_h}</span>
|
||||
</div>
|
||||
<div class="bar-track" style="height:6px;"><div class="bar-fill #{space_cls}" style="width:#{pct}%"></div></div>
|
||||
#{stats_block}
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
html << %(</div>)
|
||||
|
||||
if mail_visible
|
||||
users = mail["users"]
|
||||
has_warn = users.any? { |u| u["count"].to_i > 0 || u["dead_letter"] }
|
||||
mail_card_cls = has_warn ? "warn" : "ok"
|
||||
|
||||
html << %(<div class="section-label">Mail</div>)
|
||||
html << %(<div class="card #{mail_card_cls}"><div class="card-header"><span class="card-title">Local Mail</span></div><div class="subitems">)
|
||||
|
||||
users.each do |u|
|
||||
count = u["count"].to_i
|
||||
cls = count > 0 ? "warn" : "ok"
|
||||
label = count == 1 ? "1 message" : "#{count} messages"
|
||||
html << <<~HTML
|
||||
<div class="subitem">
|
||||
<div class="subitem-left"><div class="dot #{cls}"></div><span class="subitem-name">#{CGI.escapeHTML(u["user"].to_s)}</span></div>
|
||||
<span class="subitem-value #{cls}">#{label}</span>
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
html << %(</div>)
|
||||
|
||||
dead = users.select { |u| u["dead_letter"] }
|
||||
if dead.any?
|
||||
stats_html = dead.map { |u|
|
||||
%(<span class="stat warn">dead.letter in ~#{CGI.escapeHTML(u["user"].to_s)}</span>)
|
||||
}.join
|
||||
html << %(<div class="drive-stats" style="margin-top:0.6rem;">#{stats_html}</div>)
|
||||
end
|
||||
|
||||
html << %(</div>)
|
||||
end
|
||||
|
||||
if backup_visible
|
||||
now = Time.now.to_i
|
||||
classified = backup["entries"].map do |e|
|
||||
mtime = e["mtime"]
|
||||
cls = if mtime.nil?
|
||||
"bad"
|
||||
else
|
||||
age = now - mtime
|
||||
age < 2 * 86400 ? "ok" : age < 7 * 86400 ? "warn" : "bad"
|
||||
end
|
||||
[e, cls]
|
||||
end
|
||||
|
||||
backup_card_cls = if classified.any? { |_, c| c == "bad" } then "bad"
|
||||
elsif classified.any? { |_, c| c == "warn" } then "warn"
|
||||
else "ok"
|
||||
end
|
||||
|
||||
html << %(<div class="section-label">Backups</div>)
|
||||
html << %(<div class="card #{backup_card_cls}"><div class="card-header"><span class="card-title">Backups</span></div><div class="subitems">)
|
||||
|
||||
classified.each do |e, cls|
|
||||
age_label = e["mtime"] ? human_age(now - e["mtime"]) : "missing"
|
||||
html << <<~HTML
|
||||
<div class="subitem">
|
||||
<div class="subitem-left"><div class="dot #{cls}"></div><span class="subitem-name">#{CGI.escapeHTML(e["label"].to_s)}</span></div>
|
||||
<span class="subitem-value #{cls}">#{CGI.escapeHTML(age_label)}</span>
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
html << %(</div></div>)
|
||||
end
|
||||
|
||||
html << %(</div>)
|
||||
end
|
||||
|
||||
if drives.any? || upses.any? || mail_visible || backup_visible
|
||||
html << %(</div>)
|
||||
end
|
||||
|
||||
html << %(</div>)
|
||||
|
||||
cgi.out("type" => "text/html", "charset" => "UTF-8") do
|
||||
html
|
||||
end
|
||||
|
||||
Reference in new issue
Block a user