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;
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 2rem;
|
padding: 1rem 2rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-controls {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
text-align: left;
|
||||||
text-align: center;
|
margin-bottom: 0rem;
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
border-bottom: 1px solid #1e2433;
|
border-bottom: 1px solid #1e2433;
|
||||||
}
|
}
|
||||||
@@ -32,6 +40,7 @@
|
|||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
header h1 span { color: #e2e8f0; }
|
header h1 span { color: #e2e8f0; }
|
||||||
@@ -124,9 +133,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logout-form {
|
.logout-form {
|
||||||
position: fixed;
|
margin: 0;
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn {
|
.logout-btn {
|
||||||
@@ -380,10 +387,13 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<% if authenticated %>
|
<% if authenticated %>
|
||||||
<form method="post" class="logout-form">
|
<div class="header-controls">
|
||||||
<input type="hidden" name="action" value="logout">
|
<div class="last-updated" id="last-updated"></div>
|
||||||
<button type="submit" class="logout-btn">Log Out</button>
|
<form method="post" class="logout-form">
|
||||||
</form>
|
<input type="hidden" name="action" value="logout">
|
||||||
|
<button type="submit" class="logout-btn">Log Out</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
@@ -400,6 +410,11 @@
|
|||||||
})
|
})
|
||||||
.then(function(html) {
|
.then(function(html) {
|
||||||
document.getElementById("content").innerHTML = 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() {
|
.catch(function() {
|
||||||
document.getElementById("content").innerHTML = "Error";
|
document.getElementById("content").innerHTML = "Error";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ require "socket"
|
|||||||
require "fileutils"
|
require "fileutils"
|
||||||
require "json"
|
require "json"
|
||||||
require "shellwords"
|
require "shellwords"
|
||||||
|
require "etc"
|
||||||
|
|
||||||
SOCKET_PATH = "/run/malpd/malpd.sock"
|
SOCKET_PATH = "/run/malpd/malpd.sock"
|
||||||
|
|
||||||
@@ -146,8 +147,10 @@ def read_vm_dominfo(name)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def read_vm_info(name)
|
def read_vm_info(name)
|
||||||
ssh_out = `ssh -Ti /root/.ssh/malp-vm-key malp@#{name}`
|
ssh_out = `ssh -o ConnectTimeout=1 -Ti /root/.ssh/malp-vm-key malp@#{name} 2>/dev/null`
|
||||||
JSON.parse(ssh_out)
|
if ssh_out != ""
|
||||||
|
JSON.parse(ssh_out)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_virtual_machines
|
def read_virtual_machines
|
||||||
@@ -158,7 +161,7 @@ def read_virtual_machines
|
|||||||
|
|
||||||
names.map do |name|
|
names.map do |name|
|
||||||
dom_info = read_vm_dominfo(name)
|
dom_info = read_vm_dominfo(name)
|
||||||
vm_info = read_vm_info(name)
|
vm_info = read_vm_info(name) || {}
|
||||||
{
|
{
|
||||||
"type" => "vm",
|
"type" => "vm",
|
||||||
"name" => name,
|
"name" => name,
|
||||||
@@ -166,6 +169,143 @@ def read_virtual_machines
|
|||||||
end
|
end
|
||||||
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
|
def read_ups
|
||||||
out = `apcaccess 2>/dev/null`
|
out = `apcaccess 2>/dev/null`
|
||||||
return nil unless $?.success? && !out.empty?
|
return nil unless $?.success? && !out.empty?
|
||||||
@@ -232,6 +372,10 @@ def handle_info(conn)
|
|||||||
info.concat(read_filesystems)
|
info.concat(read_filesystems)
|
||||||
info.concat(read_virtual_machines)
|
info.concat(read_virtual_machines)
|
||||||
|
|
||||||
|
info << read_server_info
|
||||||
|
info << read_mail
|
||||||
|
info << read_backups
|
||||||
|
|
||||||
if (ups = read_ups)
|
if (ups = read_ups)
|
||||||
info << ups
|
info << ups
|
||||||
end
|
end
|
||||||
|
|||||||
+290
-90
@@ -7,6 +7,14 @@ require "json"
|
|||||||
require "securerandom"
|
require "securerandom"
|
||||||
require "socket"
|
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)
|
def human_capacity(bytes)
|
||||||
units = ["B", "KB", "MB", "GB", "TB", "PB"]
|
units = ["B", "KB", "MB", "GB", "TB", "PB"]
|
||||||
i = 0
|
i = 0
|
||||||
@@ -112,6 +120,177 @@ if cgi.params.key?("content")
|
|||||||
drives = info.select { |e| e["type"] == "drive" }
|
drives = info.select { |e| e["type"] == "drive" }
|
||||||
filesystems = info.select { |e| e["type"] == "filesystem" }
|
filesystems = info.select { |e| e["type"] == "filesystem" }
|
||||||
vms = info.select { |e| e["type"] == "vm" }
|
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">)
|
html << %(<div class="span-3">)
|
||||||
|
|
||||||
@@ -254,7 +433,12 @@ if cgi.params.key?("content")
|
|||||||
end
|
end
|
||||||
|
|
||||||
upses = info.select { |e| e["type"] == "ups" }
|
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;">)
|
html << %(<div style="display:grid;grid-template-columns:2fr 1fr;gap:1rem;align-items:start;margin-top:1rem;">)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -296,116 +480,132 @@ if cgi.params.key?("content")
|
|||||||
html << %(</div>)
|
html << %(</div>)
|
||||||
end
|
end
|
||||||
|
|
||||||
upses.each do |ups|
|
if upses.any? || mail_visible || backup_visible
|
||||||
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>)
|
|
||||||
|
|
||||||
html << %(<div>)
|
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?
|
upses.each do |ups|
|
||||||
html << %(</div>)
|
status = ups["status"].to_s
|
||||||
end
|
online = status.include?("ONLINE")
|
||||||
|
card_cls = online ? "ok" : "bad"
|
||||||
|
|
||||||
html << %(</div>)
|
model = ups["model"] ? "APC " + CGI.escapeHTML(ups["model"]) : "UPS"
|
||||||
|
|
||||||
if filesystems.any?
|
stats = []
|
||||||
html << %(<div>)
|
stats << [online ? "ok" : "bad", CGI.escapeHTML(status)] unless status.empty?
|
||||||
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))
|
|
||||||
|
|
||||||
fs_stats = []
|
if (charge = ups["charge"])
|
||||||
inode_cls = "ok"
|
cls = charge >= 80 ? "ok" : charge >= 50 ? "warn" : "bad"
|
||||||
if (itot = entry["inode_total"].to_i) > 0
|
stats << [cls, "Charge #{charge.round}%"]
|
||||||
iused = entry["inode_used"].to_i
|
end
|
||||||
ipct_f = iused * 100.0 / itot
|
if (timeleft = ups["timeleft"])
|
||||||
inode_cls = ipct_f >= 90 ? "bad" : ipct_f >= 75 ? "warn" : "ok"
|
cls = timeleft >= 10 ? "ok" : timeleft >= 5 ? "warn" : "bad"
|
||||||
fs_stats << [inode_cls, "Inodes #{ipct_f.round}%"]
|
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
|
end
|
||||||
fs_stats << ["warn", "READ-ONLY"] if entry["readonly"]
|
|
||||||
|
|
||||||
severities = [space_cls, inode_cls]
|
stats_html = stats.map { |c, txt|
|
||||||
card_cls = severities.include?("bad") ? "bad" :
|
%(<span class="stat #{c}">#{txt}</span>)
|
||||||
severities.include?("warn") ? "warn" : "ok"
|
|
||||||
|
|
||||||
stats_html = fs_stats.map { |c, txt|
|
|
||||||
%(<span class="stat #{c}">#{CGI.escapeHTML(txt)}</span>)
|
|
||||||
}.join
|
}.join
|
||||||
stats_block = fs_stats.empty? ? "" :
|
stats_block = stats.empty? ? "" :
|
||||||
%(<div class="drive-stats" style="margin-top:0.6rem;">#{stats_html}</div>)
|
%(<div class="drive-stats" style="margin-top:0.6rem;">#{stats_html}</div>)
|
||||||
|
|
||||||
|
html << %(<div class="section-label">UPS</div>)
|
||||||
html << <<~HTML
|
html << <<~HTML
|
||||||
<div class="card #{card_cls}">
|
<div class="card #{card_cls}">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title">#{mount} <span class="card-capacity">#{fstype}</span></span>
|
<span class="card-title">#{model}</span>
|
||||||
<span class="badge #{space_cls}">#{pct}%</span>
|
<span class="badge #{card_cls}">#{online ? "Online" : CGI.escapeHTML(status)}</span>
|
||||||
</div>
|
</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}
|
#{stats_block}
|
||||||
</div>
|
</div>
|
||||||
HTML
|
HTML
|
||||||
end
|
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>)
|
html << %(</div>)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if drives.any? || upses.any? || mail_visible || backup_visible
|
||||||
|
html << %(</div>)
|
||||||
|
end
|
||||||
|
|
||||||
|
html << %(</div>)
|
||||||
|
|
||||||
cgi.out("type" => "text/html", "charset" => "UTF-8") do
|
cgi.out("type" => "text/html", "charset" => "UTF-8") do
|
||||||
html
|
html
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in new issue
Block a user