198 lines
5.6 KiB
HTML
198 lines
5.6 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>HomeLab Dashboard</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet" />
|
||
<style>
|
||
html, body {
|
||
height: 100%;
|
||
margin: 0;
|
||
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
|
||
color: #fff;
|
||
font-family: 'Segoe UI', sans-serif;
|
||
}
|
||
|
||
.container {
|
||
min-height: 100vh;
|
||
padding: 2rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2rem;
|
||
}
|
||
|
||
.clock {
|
||
font-size: 5rem;
|
||
font-weight: bold;
|
||
text-align: center;
|
||
}
|
||
|
||
.date {
|
||
font-size: 1.5rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.weather {
|
||
text-align: center;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
input[type="search"] {
|
||
width: 100%;
|
||
padding: 0.75rem 1rem;
|
||
border-radius: 0.5rem;
|
||
border: none;
|
||
margin: 1rem auto 2rem;
|
||
font-size: 1.1rem;
|
||
max-width: 600px;
|
||
}
|
||
|
||
.card-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.device-card, .service-card {
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border-radius: 1rem;
|
||
padding: 1.25rem;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
transition: transform 0.2s ease;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.device-card:hover, .service-card:hover {
|
||
transform: scale(1.03);
|
||
background: rgba(255, 255, 255, 0.09);
|
||
}
|
||
|
||
.device-status {
|
||
font-size: 2rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.btn-wake {
|
||
padding: 0.5rem 1.25rem;
|
||
font-size: 0.95rem;
|
||
border-radius: 0.5rem;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
a {
|
||
color: #0dcaf0;
|
||
text-decoration: none;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="clock" id="clock"></div>
|
||
<div class="date" id="date"></div>
|
||
<div class="weather" id="weather">Loading weather...</div>
|
||
|
||
<input type="search" id="search" placeholder="Search Google or type a URL..." onkeydown="searchInput(event)" />
|
||
|
||
<section>
|
||
<h2>Services</h2>
|
||
<div class="card-grid">
|
||
<div class="service-card"><a href="https://git.zea.lt">🗂️ Gitea</a></div>
|
||
<div class="service-card"><a href="https://registry.zea.lt">📦 Docker Registry</a></div>
|
||
<div class="service-card"><a href="https://traefik.zea.lt">📊 Traefik Dashboard</a></div>
|
||
<div class="service-card"><a href="https://wol.zea.lt">🖲️ Wake-on-LAN API</a></div>
|
||
<div class="service-card"><a href="https://kuma.zea.lt">📈 Uptime Kuma</a></div>
|
||
<div class="service-card"><a href="https://drone.zea.lt">🚀 Drone CI</a></div>
|
||
<div class="service-card"><a href="https://print.zea.lt">🖨️ Printer (CUPS)</a></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Devices</h2>
|
||
<div class="card-grid">
|
||
{% for dev, mac in devices.items() %}
|
||
<div class="device-card">
|
||
<div class="device-status">
|
||
{% if status[dev] %}
|
||
🟢
|
||
{% else %}
|
||
🔴
|
||
{% endif %}
|
||
</div>
|
||
<h5>{{ dev|capitalize }}</h5>
|
||
<button
|
||
class="btn btn-{{ 'secondary' if status[dev] else 'primary' }} btn-wake"
|
||
{% if status[dev] %}disabled{% endif %}
|
||
onclick="wake('{{ dev }}')">
|
||
Wake
|
||
</button>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<script>
|
||
// Clock & date
|
||
function updateClock() {
|
||
const now = new Date();
|
||
const clock = now.toLocaleTimeString();
|
||
const date = now.toLocaleDateString(undefined, {
|
||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||
});
|
||
document.getElementById('clock').textContent = clock;
|
||
document.getElementById('date').textContent = date;
|
||
}
|
||
setInterval(updateClock, 1000);
|
||
updateClock();
|
||
|
||
// Weather (fetched via server proxy – API key stays server-side)
|
||
async function fetchWeather() {
|
||
const res = await fetch('/weather');
|
||
if (!res.ok) return document.getElementById('weather').textContent = 'Weather unavailable';
|
||
const data = await res.json();
|
||
if (data.error) return document.getElementById('weather').textContent = 'Weather unavailable';
|
||
const temp = data.main.temp.toFixed(1);
|
||
const desc = data.weather[0].description;
|
||
const icon = data.weather[0].icon;
|
||
const city = data.name;
|
||
document.getElementById('weather').innerHTML =
|
||
`<img src="https://openweathermap.org/img/wn/${icon}.png" alt="" /> ${city}: ${temp}°C, ${desc}`;
|
||
}
|
||
fetchWeather();
|
||
|
||
// Wake device
|
||
async function wake(device) {
|
||
const resp = await fetch(`/wake/${device}`, { method: 'POST' });
|
||
const data = await resp.json();
|
||
alert(data.message || data.error);
|
||
location.reload();
|
||
}
|
||
|
||
// Smart search
|
||
function searchInput(e) {
|
||
if (e.key === 'Enter') {
|
||
let q = e.target.value.trim();
|
||
if (!q) return;
|
||
if (!q.startsWith('http') && q.includes('.') && !q.includes(' ')) {
|
||
if (!q.startsWith('http://') && !q.startsWith('https://')) q = 'http://' + q;
|
||
location.href = q;
|
||
} else {
|
||
location.href = 'https://www.google.com/search?q=' + encodeURIComponent(q);
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|