Initial commit: Homelab Infrastructure Dashboard
Features: - Real-time Proxmox cluster monitoring (nodes, LXC containers) - Camera integration with go2rtc streams - Arr stack download progress monitoring - PBS backup status - Docker container status - Uptime Kuma service health - FastAPI backend with HTMX frontend
This commit is contained in:
145
app/templates/base.html
Normal file
145
app/templates/base.html
Normal file
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DeathStar Homelab</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dark: { 900: '#0a0f1a', 800: '#111827', 700: '#1f2937', 600: '#374151' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@keyframes pulse-glow { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
|
||||
.status-pulse { animation: pulse-glow 2s ease-in-out infinite; }
|
||||
.htmx-request { opacity: 0.5; }
|
||||
body { background: linear-gradient(135deg, #0a0f1a 0%, #111827 100%); }
|
||||
.card { background: rgba(31, 41, 55, 0.5); backdrop-filter: blur(10px); border: 1px solid rgba(75, 85, 99, 0.3); }
|
||||
.card:hover { border-color: rgba(99, 102, 241, 0.5); }
|
||||
.service-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.5rem; }
|
||||
.progress-bar { height: 4px; border-radius: 2px; background: rgba(75, 85, 99, 0.5); overflow: hidden; }
|
||||
.progress-fill { height: 100%; border-radius: 2px; transition: width 0.3s ease; }
|
||||
.collapsible-content { max-height: 1000px; overflow: hidden; transition: max-height 0.3s ease; }
|
||||
.collapsed .collapsible-content { max-height: 0; }
|
||||
.camera-thumb { aspect-ratio: 16/9; background: #1f2937; border-radius: 0.5rem; overflow: hidden; }
|
||||
.camera-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||
#search-modal { backdrop-filter: blur(4px); }
|
||||
.favorite-card { background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); }
|
||||
.status-banner { background: linear-gradient(90deg, rgba(239, 68, 68, 0.1) 0%, rgba(239, 68, 68, 0.05) 100%); }
|
||||
.status-banner.all-good { background: linear-gradient(90deg, rgba(34, 197, 94, 0.1) 0%, rgba(34, 197, 94, 0.05) 100%); }
|
||||
.response-fast { color: #10b981; }
|
||||
.response-medium { color: #f59e0b; }
|
||||
.response-slow { color: #ef4444; }
|
||||
.svc-icon { width: 14px; height: 14px; flex-shrink: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-gray-100 min-h-screen p-3">
|
||||
<div class="max-w-[1920px] mx-auto">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- Search Modal -->
|
||||
<div id="search-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center">
|
||||
<div class="bg-dark-800 rounded-xl p-4 w-full max-w-md mx-4 border border-gray-700">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
<input type="text" id="search-input" placeholder="Search services..." class="flex-1 bg-transparent border-none outline-none text-white placeholder-gray-500">
|
||||
<kbd class="px-2 py-1 text-xs bg-dark-700 rounded text-gray-400">ESC</kbd>
|
||||
</div>
|
||||
<div id="search-results" class="max-h-64 overflow-y-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Clock
|
||||
function updateClock() {
|
||||
const now = new Date();
|
||||
const el = document.getElementById('clock');
|
||||
if (el) el.textContent = now.toLocaleTimeString();
|
||||
}
|
||||
setInterval(updateClock, 1000);
|
||||
updateClock();
|
||||
|
||||
// Search
|
||||
const searchModal = document.getElementById('search-modal');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === '/' && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== 'INPUT') {
|
||||
e.preventDefault();
|
||||
searchModal.classList.remove('hidden');
|
||||
searchModal.classList.add('flex');
|
||||
searchInput.focus();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
searchModal.classList.add('hidden');
|
||||
searchModal.classList.remove('flex');
|
||||
searchInput.value = '';
|
||||
searchResults.innerHTML = '';
|
||||
}
|
||||
if (e.key === 'r' && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== 'INPUT') {
|
||||
refreshAll();
|
||||
}
|
||||
});
|
||||
|
||||
searchModal.addEventListener('click', function(e) {
|
||||
if (e.target === searchModal) {
|
||||
searchModal.classList.add('hidden');
|
||||
searchModal.classList.remove('flex');
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const query = e.target.value.toLowerCase();
|
||||
if (query.length < 2) { searchResults.innerHTML = ''; return; }
|
||||
const cards = document.querySelectorAll('.service-card');
|
||||
const matches = [];
|
||||
cards.forEach(card => {
|
||||
const name = card.dataset.name?.toLowerCase() || '';
|
||||
if (name.includes(query)) {
|
||||
const url = card.querySelector('a')?.href || '#';
|
||||
const status = card.querySelector('.status-dot')?.classList.contains('bg-emerald-500') ? 'online' : 'offline';
|
||||
matches.push({ name: card.dataset.name, url, status });
|
||||
}
|
||||
});
|
||||
searchResults.innerHTML = matches.length === 0 ? '<div class="text-gray-500 text-sm py-2">No services found</div>' :
|
||||
matches.map(m => `<a href="${m.url}" target="_blank" class="flex items-center gap-3 p-2 rounded hover:bg-dark-700"><span class="w-2 h-2 rounded-full ${m.status === 'online' ? 'bg-emerald-500' : 'bg-red-500'}"></span><span>${m.name}</span></a>`).join('');
|
||||
});
|
||||
|
||||
function toggleCategory(header) {
|
||||
const section = header.closest('.category-section');
|
||||
section.classList.toggle('collapsed');
|
||||
const icon = header.querySelector('.collapse-icon');
|
||||
icon.style.transform = section.classList.contains('collapsed') ? 'rotate(-90deg)' : '';
|
||||
}
|
||||
|
||||
function refreshAll() {
|
||||
htmx.trigger('#status-banner', 'refresh');
|
||||
htmx.trigger('#favorites-container', 'refresh');
|
||||
htmx.trigger('#nodes-container', 'refresh');
|
||||
htmx.trigger('#services-container', 'refresh');
|
||||
htmx.trigger('#cameras-container', 'refresh');
|
||||
htmx.trigger('#downloads-container', 'refresh');
|
||||
htmx.trigger('#docker-container', 'refresh');
|
||||
htmx.trigger('#uptime-container', 'refresh');
|
||||
htmx.trigger('#pbs-container', 'refresh');
|
||||
htmx.trigger('#events-container', 'refresh');
|
||||
}
|
||||
|
||||
function toggleNodeExpand(nodeEl) {
|
||||
const expanded = nodeEl.querySelector('.node-expanded');
|
||||
if (expanded) expanded.classList.toggle('hidden');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user