Add temperature monitoring and optional server URL links to dashboard
This commit is contained in:
@@ -50,9 +50,9 @@ Edit `infrastructure.conf` to define your servers:
|
||||
|
||||
```
|
||||
Production
|
||||
root@prod-web-01
|
||||
root@prod-web-01 https://web01.example.com
|
||||
root@prod-db-01
|
||||
deploy@prod-app-01
|
||||
deploy@prod-app-01 https://app01.example.com:8080
|
||||
|
||||
Development
|
||||
deploy@dev-01
|
||||
@@ -60,7 +60,8 @@ Development
|
||||
```
|
||||
|
||||
- Group names are freeform labels (no indentation)
|
||||
- Servers are indented with `USERNAME@HOSTNAME`
|
||||
- Servers are indented with `USERNAME@HOSTNAME [URL]`
|
||||
- An optional URL after the host adds a clickable link on the dashboard
|
||||
- Lines starting with `#` are comments
|
||||
|
||||
### 3. Install
|
||||
|
||||
23
app/app.py
23
app/app.py
@@ -35,6 +35,7 @@ class Server(db.Model):
|
||||
username = db.Column(db.String(255), nullable=False)
|
||||
hostname = db.Column(db.String(255), nullable=False)
|
||||
primary_ip = db.Column(db.String(45), default='')
|
||||
url = db.Column(db.String(1024), default='')
|
||||
is_online = db.Column(db.Boolean, default=False)
|
||||
last_collected = db.Column(db.DateTime, nullable=True)
|
||||
details = db.Column(db.JSON, nullable=True)
|
||||
@@ -55,13 +56,16 @@ def parse_infrastructure_conf():
|
||||
if line[0] not in (' ', '\t'):
|
||||
current_group = line.strip()
|
||||
else:
|
||||
entry = line.strip()
|
||||
parts = line.strip().split(None, 1)
|
||||
entry = parts[0] if parts else ''
|
||||
url = parts[1] if len(parts) > 1 else ''
|
||||
if '@' in entry:
|
||||
user, host = entry.split('@', 1)
|
||||
servers.append({
|
||||
'group': current_group or 'Default',
|
||||
'username': user.strip(),
|
||||
'hostname': host.strip(),
|
||||
'url': url.strip(),
|
||||
})
|
||||
except FileNotFoundError:
|
||||
logger.error("infrastructure.conf not found at %s", INFRA_CONF_PATH)
|
||||
@@ -204,6 +208,7 @@ def collect_all():
|
||||
db.session.add(server)
|
||||
|
||||
server.group_name = entry['group']
|
||||
server.url = entry.get('url', '')
|
||||
server.is_online = result.get('is_online', False)
|
||||
server.last_collected = datetime.now(timezone.utc)
|
||||
server.details = result
|
||||
@@ -271,6 +276,7 @@ def api_servers():
|
||||
'username': s.username,
|
||||
'hostname': s.hostname,
|
||||
'primary_ip': s.primary_ip,
|
||||
'url': s.url,
|
||||
'is_online': s.is_online,
|
||||
'last_collected': s.last_collected.isoformat() if s.last_collected else None,
|
||||
'details': s.details,
|
||||
@@ -327,6 +333,21 @@ def format_uptime(seconds):
|
||||
return f"{hours}h {minutes}m"
|
||||
|
||||
|
||||
@app.template_filter('temp_color')
|
||||
def temp_color(temp_c):
|
||||
try:
|
||||
t = float(temp_c)
|
||||
except (TypeError, ValueError):
|
||||
return '#64748b'
|
||||
if t >= 90:
|
||||
return '#ef4444'
|
||||
if t >= 75:
|
||||
return '#f97316'
|
||||
if t >= 60:
|
||||
return '#eab308'
|
||||
return '#22c55e'
|
||||
|
||||
|
||||
@app.template_filter('usage_color')
|
||||
def usage_color(percent):
|
||||
try:
|
||||
|
||||
@@ -45,6 +45,29 @@ else
|
||||
echo "usage_percent=0.0"
|
||||
fi
|
||||
|
||||
# Temperatures - try sensors (lm-sensors), fall back to thermal zones
|
||||
echo "[temperatures]"
|
||||
if command -v sensors &>/dev/null; then
|
||||
sensors 2>/dev/null | awk -F'[: ]+' '
|
||||
/^[^ ]/ { chip=$1 }
|
||||
/°C/ {
|
||||
label=$1
|
||||
gsub(/^ +| +$/, "", label)
|
||||
match($0, /[+-]([0-9]+\.[0-9]+)°C/, m)
|
||||
if (m[1]+0 > 0) print chip "/" label "=" m[1]
|
||||
}
|
||||
'
|
||||
else
|
||||
for tz in /sys/class/thermal/thermal_zone*; do
|
||||
[ -f "$tz/temp" ] || continue
|
||||
type=$(cat "$tz/type" 2>/dev/null || echo "unknown")
|
||||
temp_mc=$(cat "$tz/temp" 2>/dev/null || echo 0)
|
||||
temp_c=$((temp_mc / 1000))
|
||||
temp_frac=$(( (temp_mc % 1000) / 100 ))
|
||||
[ "$temp_c" -gt 0 ] 2>/dev/null && echo "${type}=${temp_c}.${temp_frac}"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "[memory]"
|
||||
total_kb=$(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}')
|
||||
available_kb=$(grep MemAvailable /proc/meminfo 2>/dev/null | awk '{print $2}')
|
||||
|
||||
@@ -121,6 +121,19 @@ main {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.server-link {
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.server-link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.server-ip {
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
|
||||
@@ -23,6 +23,13 @@
|
||||
{% set sys = d.get('system', {}) if d.get('system') else {} %}
|
||||
{% set cpu = d.get('cpu', {}) if d.get('cpu') else {} %}
|
||||
{% set mem = d.get('memory', {}) if d.get('memory') else {} %}
|
||||
{% set temps = d.get('temperatures', {}) if d.get('temperatures') else {} %}
|
||||
{% set max_temp = namespace(val=0.0) %}
|
||||
{% for k, v in temps.items() %}
|
||||
{% if v|float > max_temp.val %}
|
||||
{% set max_temp.val = v|float %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set cpu_pct = cpu.get('usage_percent', '0')|float %}
|
||||
{% set mem_pct = mem.get('usage_percent', '0')|float %}
|
||||
{% set disk_usages = d.get('disk_usage', []) if d.get('disk_usage') else [] %}
|
||||
@@ -42,6 +49,9 @@
|
||||
<div class="card-header">
|
||||
<span class="status-dot {% if server.is_online %}online{% else %}offline{% endif %}"></span>
|
||||
<span class="server-name">{{ server.hostname }}</span>
|
||||
{% if server.url %}
|
||||
<a href="{{ server.url }}" class="server-link" target="_blank" rel="noopener" onclick="event.stopPropagation();" title="{{ server.url }}">↗</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="server-ip">{{ server.primary_ip or 'No IP' }}</div>
|
||||
<div class="server-os">{{ sys.get('os_pretty', '') }}</div>
|
||||
@@ -69,6 +79,15 @@
|
||||
</div>
|
||||
<span class="usage-pct">{{ '%.0f'|format(root_disk.pct) }}%</span>
|
||||
</div>
|
||||
{% if max_temp.val > 0 %}
|
||||
<div class="usage-row">
|
||||
<span class="usage-label">TEMP</span>
|
||||
<div class="usage-bar-bg">
|
||||
<div class="usage-bar-fill" style="width: {{ [max_temp.val, 100.0]|min }}%; background: {{ max_temp.val|temp_color }};"></div>
|
||||
</div>
|
||||
<span class="usage-pct">{{ '%.0f'|format(max_temp.val) }}°</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="offline-label">Unreachable</div>
|
||||
@@ -105,6 +124,21 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Temperatures -->
|
||||
{% if temps %}
|
||||
<div class="detail-section">
|
||||
<h4>Temperatures</h4>
|
||||
<table>
|
||||
{% for sensor, value in temps.items() %}
|
||||
<tr>
|
||||
<td>{{ sensor }}</td>
|
||||
<td style="color: {{ value|float|temp_color }}">{{ value }}°C</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Memory -->
|
||||
<div class="detail-section">
|
||||
<h4>Memory</h4>
|
||||
|
||||
Reference in New Issue
Block a user