Initial commit: Homelab Dashboard with YAML configuration
Features: - Service health monitoring with response times - Proxmox cluster integration (nodes, VMs, containers) - PBS backup server monitoring - Camera viewer with WebRTC (go2rtc) - Docker container monitoring - Uptime Kuma integration - Mobile-friendly responsive design - YAML-based configuration for easy setup
This commit is contained in:
20
app/templates/partials/cameras.html
Normal file
20
app/templates/partials/cameras.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{% for camera in cameras[:4] %}
|
||||
<div class="camera-thumb relative group">
|
||||
<img src="/api/camera-snapshot/{{ camera }}"
|
||||
alt="{{ camera }}"
|
||||
class="w-full h-full"
|
||||
loading="lazy"
|
||||
onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 56%22><rect fill=%22%231f2937%22 width=%22100%22 height=%2256%22/><text x=%2250%22 y=%2228%22 text-anchor=%22middle%22 fill=%22%236b7280%22 font-size=%228%22>No Signal</text></svg>'">
|
||||
<div class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
<a href="{{ go2rtc_url }}/stream.html?src={{ camera }}" target="_blank" class="text-white text-xs hover:text-cyan-400">
|
||||
{{ camera }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="absolute bottom-1 left-1 text-[10px] text-white bg-black/50 px-1 rounded">{{ camera }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a href="/cameras" class="block text-center text-xs text-gray-500 hover:text-cyan-400 mt-2">
|
||||
View all {{ cameras | length }} cameras
|
||||
</a>
|
||||
20
app/templates/partials/docker.html
Normal file
20
app/templates/partials/docker.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<div class="space-y-2">
|
||||
{% for host in hosts %}
|
||||
<div class="card rounded-lg p-2">
|
||||
<div class="flex items-center justify-between text-xs text-gray-400 mb-2">
|
||||
<span class="font-medium">{{ host.name }}</span>
|
||||
<span class="text-[10px]">{{ counts.get(host.name, 0) }} running</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for container in containers if container.host == host.name %}
|
||||
<div class="flex items-center gap-1.5 px-2 py-1 rounded bg-dark-700 text-xs">
|
||||
<span class="w-1.5 h-1.5 rounded-full {% if container.state == 'running' %}bg-emerald-500{% else %}bg-red-500{% endif %}"></span>
|
||||
<span class="text-gray-300 truncate max-w-[80px]" title="{{ container.name }}">{{ container.name }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-gray-500 text-xs">No containers</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
40
app/templates/partials/downloads.html
Normal file
40
app/templates/partials/downloads.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<div class="card rounded-lg p-3">
|
||||
{% if queue and queue.downloading > 0 %}
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-gray-400">{{ queue.downloading }} active</span>
|
||||
<span class="text-xs text-emerald-400 font-mono">{{ queue.speed }}</span>
|
||||
</div>
|
||||
|
||||
{% if queue.items %}
|
||||
<div class="space-y-2">
|
||||
{% for item in queue.items %}
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-xs mb-1">
|
||||
<span class="text-gray-300 truncate flex-1 mr-2">{{ item.name }}</span>
|
||||
<span class="text-gray-500">{{ item.progress | round(1) }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill bg-emerald-500" style="width: {{ item.progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if queue.eta and queue.eta != 'Unknown' %}
|
||||
<div class="flex items-center justify-between mt-2 text-xs text-gray-500">
|
||||
<span>ETA: {{ queue.eta }}</span>
|
||||
<span>{{ queue.size_left }} left</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="flex items-center gap-2 text-gray-500 text-xs">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<span>No active downloads</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a href="https://sabnzbd.deathstar-home.one" target="_blank" class="block text-center text-xs text-gray-500 hover:text-green-400 mt-2">
|
||||
Open Sabnzbd
|
||||
</a>
|
||||
</div>
|
||||
16
app/templates/partials/events.html
Normal file
16
app/templates/partials/events.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="card rounded-lg p-3">
|
||||
{% if events %}
|
||||
<div class="space-y-1.5">
|
||||
{% for event in events | reverse %}
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="w-1.5 h-1.5 rounded-full flex-shrink-0 {% if event.new_status == 'online' %}bg-emerald-500{% elif event.new_status == 'degraded' %}bg-amber-500{% else %}bg-red-500{% endif %}"></span>
|
||||
<span class="text-gray-300 truncate flex-1">{{ event.service }}</span>
|
||||
<span class="text-[10px] {% if event.new_status == 'online' %}text-emerald-400{% else %}text-red-400{% endif %}">{{ event.new_status }}</span>
|
||||
<span class="text-[10px] text-gray-500">{{ event.timestamp.strftime('%H:%M') }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-gray-500 text-xs text-center py-2">No recent events</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
13
app/templates/partials/favorites.html
Normal file
13
app/templates/partials/favorites.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for service in favorites %}
|
||||
{% set status = services_status.get(service.name) %}
|
||||
<a href="{{ service.url }}" target="_blank"
|
||||
class="favorite-card card flex items-center gap-2 px-3 py-2 rounded-lg hover:scale-105 transition-transform">
|
||||
<span class="w-2 h-2 rounded-full {% if status and status.status == 'online' %}bg-emerald-500{% elif status and status.status == 'degraded' %}bg-amber-500{% else %}bg-red-500{% endif %}"></span>
|
||||
<span class="text-sm font-medium">{{ service.name }}</span>
|
||||
{% if status and status.response_time_ms %}
|
||||
<span class="text-xs text-gray-500">{{ status.response_time_ms }}ms</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
66
app/templates/partials/nodes.html
Normal file
66
app/templates/partials/nodes.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2">
|
||||
{% for node in nodes_status %}
|
||||
<div class="card rounded-lg p-3 cursor-pointer" onclick="toggleNodeExpand(this)">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full {% if node.status == 'online' %}bg-emerald-500{% else %}bg-red-500{% endif %}"></span>
|
||||
<span class="font-semibold text-sm">{{ node.name }}</span>
|
||||
</div>
|
||||
{% if node.uptime_hours %}
|
||||
<span class="text-xs text-gray-500">{{ (node.uptime_hours / 24) | round(1) }}d</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if node.status == 'online' %}
|
||||
<div class="mb-2">
|
||||
<div class="flex items-center justify-between text-xs mb-1">
|
||||
<span class="text-gray-400">CPU</span>
|
||||
<span class="{% if node.cpu_percent and node.cpu_percent > 80 %}text-red-400{% elif node.cpu_percent and node.cpu_percent > 60 %}text-amber-400{% else %}text-emerald-400{% endif %}">{{ node.cpu_percent | default(0) | round(1) }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar"><div class="progress-fill {% if node.cpu_percent and node.cpu_percent > 80 %}bg-red-500{% elif node.cpu_percent and node.cpu_percent > 60 %}bg-amber-500{% else %}bg-emerald-500{% endif %}" style="width: {{ node.cpu_percent | default(0) }}%"></div></div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-center justify-between text-xs mb-1">
|
||||
<span class="text-gray-400">RAM</span>
|
||||
<span class="{% if node.memory_percent and node.memory_percent > 85 %}text-red-400{% elif node.memory_percent and node.memory_percent > 70 %}text-amber-400{% else %}text-blue-400{% endif %}">{% if node.memory_used_gb %}{{ node.memory_used_gb }}{% else %}?{% endif %}/{% if node.memory_total_gb %}{{ node.memory_total_gb }}{% else %}?{% endif %}GB</span>
|
||||
</div>
|
||||
<div class="progress-bar"><div class="progress-fill {% if node.memory_percent and node.memory_percent > 85 %}bg-red-500{% elif node.memory_percent and node.memory_percent > 70 %}bg-amber-500{% else %}bg-blue-500{% endif %}" style="width: {{ node.memory_percent | default(0) }}%"></div></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-xs mb-1">
|
||||
<span class="text-gray-400">Disk</span>
|
||||
<span class="{% if node.disk_percent and node.disk_percent > 85 %}text-red-400{% elif node.disk_percent and node.disk_percent > 70 %}text-amber-400{% else %}text-purple-400{% endif %}">{{ node.disk_percent | default(0) | round(1) }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar"><div class="progress-fill {% if node.disk_percent and node.disk_percent > 85 %}bg-red-500{% elif node.disk_percent and node.disk_percent > 70 %}bg-amber-500{% else %}bg-purple-500{% endif %}" style="width: {{ node.disk_percent | default(0) }}%"></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Expandable VM/CT list -->
|
||||
<div class="node-expanded hidden mt-2 pt-2 border-t border-gray-700">
|
||||
{% if node.vms %}
|
||||
<div class="text-[10px] text-gray-400 mb-1">VMs ({{ node.vms | length }})</div>
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
{% for vm in node.vms[:5] %}
|
||||
<span class="px-1 py-0.5 rounded text-[9px] {% if vm.status == 'running' %}bg-emerald-900/50 text-emerald-400{% else %}bg-gray-700 text-gray-400{% endif %}">{{ vm.name[:10] }}</span>
|
||||
{% endfor %}
|
||||
{% if node.vms | length > 5 %}<span class="text-[9px] text-gray-500">+{{ node.vms | length - 5 }}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if node.containers %}
|
||||
<div class="text-[10px] text-gray-400 mb-1">LXC ({{ node.containers | length }})</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for ct in node.containers[:5] %}
|
||||
<span class="px-1 py-0.5 rounded text-[9px] {% if ct.status == 'running' %}bg-blue-900/50 text-blue-400{% else %}bg-gray-700 text-gray-400{% endif %}">{{ ct.name[:10] }}</span>
|
||||
{% endfor %}
|
||||
{% if node.containers | length > 5 %}<span class="text-[9px] text-gray-500">+{{ node.containers | length - 5 }}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-red-400 text-xs py-2">Offline</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if cluster_uptime %}
|
||||
<div class="text-xs text-gray-500 mt-2 text-right">Cluster total: {{ cluster_uptime }}h</div>
|
||||
{% endif %}
|
||||
31
app/templates/partials/pbs.html
Normal file
31
app/templates/partials/pbs.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<div class="card rounded-lg p-3">
|
||||
{% if pbs.status == 'online' %}
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-emerald-400">Online</span>
|
||||
{% if pbs.last_backup %}
|
||||
<span class="text-xs text-gray-500">Last: {{ pbs.last_backup }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if pbs.datastore_usage %}
|
||||
<div class="space-y-2">
|
||||
{% for ds in pbs.datastore_usage %}
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-xs mb-1">
|
||||
<span class="text-gray-300">{{ ds.name }}</span>
|
||||
<span class="text-gray-500">{{ ds.used_gb }}/{{ ds.total_gb }}GB</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill {% if ds.percent > 85 %}bg-red-500{% elif ds.percent > 70 %}bg-amber-500{% else %}bg-orange-500{% endif %}" style="width: {{ ds.percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="flex items-center gap-2 text-gray-500 text-xs">
|
||||
<svg class="w-4 h-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
<span>PBS Offline</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="https://192.168.1.159:8007" target="_blank" class="block text-center text-xs text-gray-500 hover:text-orange-400 mt-2">Open PBS</a>
|
||||
</div>
|
||||
60
app/templates/partials/services.html
Normal file
60
app/templates/partials/services.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% for category_name, category_services in services_by_category.items() %}
|
||||
{% set cat_info = categories.get(category_name, {}) %}
|
||||
<div class="category-section mb-3">
|
||||
<div class="flex items-center gap-2 mb-2 cursor-pointer select-none" onclick="toggleCategory(this)">
|
||||
<svg class="w-3 h-3 text-gray-500 collapse-icon transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
<span class="w-3 h-3 rounded-full
|
||||
{% if cat_info.color == 'blue' %}bg-blue-500
|
||||
{% elif cat_info.color == 'purple' %}bg-purple-500
|
||||
{% elif cat_info.color == 'amber' %}bg-amber-500
|
||||
{% elif cat_info.color == 'emerald' %}bg-emerald-500
|
||||
{% elif cat_info.color == 'cyan' %}bg-cyan-500
|
||||
{% else %}bg-gray-500{% endif %}"></span>
|
||||
<h3 class="text-sm font-semibold text-gray-300">{{ category_name }}</h3>
|
||||
<span class="text-xs text-gray-500">({{ category_services | length }})</span>
|
||||
</div>
|
||||
|
||||
<div class="collapsible-content">
|
||||
<div class="service-grid">
|
||||
{% for service in category_services %}
|
||||
{% set status = services_status.get(service.name) %}
|
||||
<a href="{{ service.url }}" target="_blank"
|
||||
class="service-card card rounded-lg p-2 flex items-center gap-2 hover:scale-[1.02] transition-transform"
|
||||
data-name="{{ service.name }}"
|
||||
data-category="{{ category_name }}">
|
||||
<!-- Status dot -->
|
||||
<span class="status-dot w-2 h-2 rounded-full flex-shrink-0
|
||||
{% if status and status.status == 'online' %}bg-emerald-500 status-online
|
||||
{% elif status and status.status == 'degraded' %}bg-amber-500 status-pulse
|
||||
{% else %}bg-red-500 status-offline{% endif %}"></span>
|
||||
|
||||
<!-- Icon -->
|
||||
<svg class="svc-icon text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">{{ get_service_icon(service.icon) | safe }}</svg>
|
||||
|
||||
<!-- Name and response time -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-xs font-medium truncate">{{ service.name }}</div>
|
||||
{% if status and status.response_time_ms %}
|
||||
<div class="text-[10px] {% if status.response_time_ms < 100 %}response-fast{% elif status.response_time_ms < 500 %}response-medium{% else %}response-slow{% endif %}">
|
||||
{{ status.response_time_ms }}ms
|
||||
</div>
|
||||
{% elif status and status.status == 'offline' %}
|
||||
<div class="text-[10px] text-red-400">Offline</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Critical badge -->
|
||||
{% if service.critical %}
|
||||
<svg class="w-3 h-3 text-red-400 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path></svg>
|
||||
{% endif %}
|
||||
|
||||
<!-- Group badge -->
|
||||
{% if service.group %}
|
||||
<span class="text-[8px] px-1 py-0.5 rounded bg-dark-600 text-gray-400">{{ service.group[:3] }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
17
app/templates/partials/status_banner.html
Normal file
17
app/templates/partials/status_banner.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% if critical_down %}
|
||||
<div class="status-banner rounded-lg px-4 py-2 flex items-center justify-between border border-red-900/50">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-5 h-5 text-red-400 status-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
||||
<span class="text-sm text-red-300">
|
||||
<strong>Critical services down:</strong> {{ critical_down | join(", ") }}
|
||||
</span>
|
||||
</div>
|
||||
<a href="#services-container" class="text-xs text-red-400 hover:text-red-300">View details</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="status-banner all-good rounded-lg px-4 py-2 flex items-center gap-3 border border-emerald-900/50">
|
||||
<svg class="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<span class="text-sm text-emerald-300">All critical services operational</span>
|
||||
<span class="text-xs text-gray-500 ml-auto">{{ online_count }}/{{ total_count }} services online</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
45
app/templates/partials/uptime.html
Normal file
45
app/templates/partials/uptime.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<div class="card rounded-lg p-3">
|
||||
{% if uptime.monitors %}
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-gray-400">{{ uptime.summary.up }}/{{ uptime.summary.total }} up</span>
|
||||
{% if uptime.summary.down > 0 %}
|
||||
<span class="text-xs text-red-400">{{ uptime.summary.down }} down</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
{% for monitor in uptime.monitors[:8] %}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full flex-shrink-0 {% if monitor.status == 1 %}bg-emerald-500{% else %}bg-red-500{% endif %}"></span>
|
||||
<span class="text-xs text-gray-300 truncate flex-1">{{ monitor.name }}</span>
|
||||
<!-- Sparkline -->
|
||||
{% if monitor.heartbeats %}
|
||||
<div class="flex items-end gap-px h-3">
|
||||
{% for hb in monitor.heartbeats[-12:] %}
|
||||
{% set ping_height = ((hb.ping or 50) / 5) %}
|
||||
{% if ping_height > 100 %}{% set ping_height = 100 %}{% endif %}
|
||||
{% if ping_height < 20 %}{% set ping_height = 20 %}{% endif %}
|
||||
<div class="w-1 {% if hb.status == 1 %}bg-emerald-500/60{% else %}bg-red-500/60{% endif %}"
|
||||
style="height: {{ ping_height }}%"></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if monitor.ping %}
|
||||
<span class="text-[10px] text-gray-500 w-8 text-right">{{ monitor.ping }}ms</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if uptime.monitors | length > 8 %}
|
||||
<div class="text-xs text-gray-500 mt-2">+{{ uptime.monitors | length - 8 }} more</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="flex items-center gap-2 text-gray-500 text-xs">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<span>No uptime data</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ uptime_kuma_url }}" target="_blank" class="block text-center text-xs text-gray-500 hover:text-amber-400 mt-2">
|
||||
Open Uptime Kuma
|
||||
</a>
|
||||
</div>
|
||||
Reference in New Issue
Block a user