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
|
Production
|
||||||
root@prod-web-01
|
root@prod-web-01 https://web01.example.com
|
||||||
root@prod-db-01
|
root@prod-db-01
|
||||||
deploy@prod-app-01
|
deploy@prod-app-01 https://app01.example.com:8080
|
||||||
|
|
||||||
Development
|
Development
|
||||||
deploy@dev-01
|
deploy@dev-01
|
||||||
@@ -60,7 +60,8 @@ Development
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Group names are freeform labels (no indentation)
|
- 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
|
- Lines starting with `#` are comments
|
||||||
|
|
||||||
### 3. Install
|
### 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)
|
username = db.Column(db.String(255), nullable=False)
|
||||||
hostname = db.Column(db.String(255), nullable=False)
|
hostname = db.Column(db.String(255), nullable=False)
|
||||||
primary_ip = db.Column(db.String(45), default='')
|
primary_ip = db.Column(db.String(45), default='')
|
||||||
|
url = db.Column(db.String(1024), default='')
|
||||||
is_online = db.Column(db.Boolean, default=False)
|
is_online = db.Column(db.Boolean, default=False)
|
||||||
last_collected = db.Column(db.DateTime, nullable=True)
|
last_collected = db.Column(db.DateTime, nullable=True)
|
||||||
details = db.Column(db.JSON, nullable=True)
|
details = db.Column(db.JSON, nullable=True)
|
||||||
@@ -55,13 +56,16 @@ def parse_infrastructure_conf():
|
|||||||
if line[0] not in (' ', '\t'):
|
if line[0] not in (' ', '\t'):
|
||||||
current_group = line.strip()
|
current_group = line.strip()
|
||||||
else:
|
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:
|
if '@' in entry:
|
||||||
user, host = entry.split('@', 1)
|
user, host = entry.split('@', 1)
|
||||||
servers.append({
|
servers.append({
|
||||||
'group': current_group or 'Default',
|
'group': current_group or 'Default',
|
||||||
'username': user.strip(),
|
'username': user.strip(),
|
||||||
'hostname': host.strip(),
|
'hostname': host.strip(),
|
||||||
|
'url': url.strip(),
|
||||||
})
|
})
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.error("infrastructure.conf not found at %s", INFRA_CONF_PATH)
|
logger.error("infrastructure.conf not found at %s", INFRA_CONF_PATH)
|
||||||
@@ -204,6 +208,7 @@ def collect_all():
|
|||||||
db.session.add(server)
|
db.session.add(server)
|
||||||
|
|
||||||
server.group_name = entry['group']
|
server.group_name = entry['group']
|
||||||
|
server.url = entry.get('url', '')
|
||||||
server.is_online = result.get('is_online', False)
|
server.is_online = result.get('is_online', False)
|
||||||
server.last_collected = datetime.now(timezone.utc)
|
server.last_collected = datetime.now(timezone.utc)
|
||||||
server.details = result
|
server.details = result
|
||||||
@@ -271,6 +276,7 @@ def api_servers():
|
|||||||
'username': s.username,
|
'username': s.username,
|
||||||
'hostname': s.hostname,
|
'hostname': s.hostname,
|
||||||
'primary_ip': s.primary_ip,
|
'primary_ip': s.primary_ip,
|
||||||
|
'url': s.url,
|
||||||
'is_online': s.is_online,
|
'is_online': s.is_online,
|
||||||
'last_collected': s.last_collected.isoformat() if s.last_collected else None,
|
'last_collected': s.last_collected.isoformat() if s.last_collected else None,
|
||||||
'details': s.details,
|
'details': s.details,
|
||||||
@@ -327,6 +333,21 @@ def format_uptime(seconds):
|
|||||||
return f"{hours}h {minutes}m"
|
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')
|
@app.template_filter('usage_color')
|
||||||
def usage_color(percent):
|
def usage_color(percent):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -45,6 +45,29 @@ else
|
|||||||
echo "usage_percent=0.0"
|
echo "usage_percent=0.0"
|
||||||
fi
|
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]"
|
echo "[memory]"
|
||||||
total_kb=$(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}')
|
total_kb=$(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}')
|
||||||
available_kb=$(grep MemAvailable /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;
|
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 {
|
.server-ip {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
|
|||||||
@@ -23,6 +23,13 @@
|
|||||||
{% set sys = d.get('system', {}) if d.get('system') else {} %}
|
{% set sys = d.get('system', {}) if d.get('system') else {} %}
|
||||||
{% set cpu = d.get('cpu', {}) if d.get('cpu') else {} %}
|
{% set cpu = d.get('cpu', {}) if d.get('cpu') else {} %}
|
||||||
{% set mem = d.get('memory', {}) if d.get('memory') 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 cpu_pct = cpu.get('usage_percent', '0')|float %}
|
||||||
{% set mem_pct = mem.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 [] %}
|
{% set disk_usages = d.get('disk_usage', []) if d.get('disk_usage') else [] %}
|
||||||
@@ -42,6 +49,9 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="status-dot {% if server.is_online %}online{% else %}offline{% endif %}"></span>
|
<span class="status-dot {% if server.is_online %}online{% else %}offline{% endif %}"></span>
|
||||||
<span class="server-name">{{ server.hostname }}</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>
|
||||||
<div class="server-ip">{{ server.primary_ip or 'No IP' }}</div>
|
<div class="server-ip">{{ server.primary_ip or 'No IP' }}</div>
|
||||||
<div class="server-os">{{ sys.get('os_pretty', '') }}</div>
|
<div class="server-os">{{ sys.get('os_pretty', '') }}</div>
|
||||||
@@ -69,6 +79,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="usage-pct">{{ '%.0f'|format(root_disk.pct) }}%</span>
|
<span class="usage-pct">{{ '%.0f'|format(root_disk.pct) }}%</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="offline-label">Unreachable</div>
|
<div class="offline-label">Unreachable</div>
|
||||||
@@ -105,6 +124,21 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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 -->
|
<!-- Memory -->
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4>Memory</h4>
|
<h4>Memory</h4>
|
||||||
|
|||||||
Reference in New Issue
Block a user