Initial homelab infrastructure

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-13 19:41:46 +02:00
co-authored by Cursor
commit 5014d55d62
31 changed files with 1594 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
from flask import Flask, render_template, jsonify, request, Response
import requests
import os
from functools import wraps
from requests.auth import HTTPBasicAuth
app = Flask(__name__)
WOL_API_URL = os.getenv("WOL_API_URL", "https://wol.zea.lt")
WOL_API_USER = os.getenv("WOL_API_USER")
WOL_API_PASS = os.getenv("WOL_API_PASS")
WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")
WEATHER_CITY = os.getenv("WEATHER_CITY", "")
def _fetch_wol_status():
"""Fetch device list and status from WOL API (single source of truth)."""
try:
auth = (
HTTPBasicAuth(WOL_API_USER, WOL_API_PASS)
if WOL_API_USER and WOL_API_PASS
else None
)
r = requests.get(
f"{WOL_API_URL}/status",
auth=auth,
timeout=5,
)
if r.ok:
return r.json().get("devices", {})
except requests.RequestException:
pass
return {}
def check_auth(username, password):
return username == WOL_API_USER and password == WOL_API_PASS
def authenticate():
"""Sends a 401 response that enables basic auth."""
return Response(
"Could not verify your access level for that URL.\n"
"You have to login with proper credentials",
401,
{"WWW-Authenticate": 'Basic realm="Login Required"'},
)
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
@app.route("/")
def dashboard():
devices_data = _fetch_wol_status()
devices = {k: v.get("mac", "") for k, v in devices_data.items()}
status = {k: v.get("online", False) for k, v in devices_data.items()}
return render_template(
"dashboard.html",
devices=devices,
status=status,
)
@app.route("/wake/<device>", methods=["POST"])
@requires_auth
def wake_device(device):
devices_data = _fetch_wol_status()
if device not in devices_data:
return jsonify({"error": "Device not found"}), 404
try:
auth = HTTPBasicAuth(WOL_API_USER, WOL_API_PASS)
r = requests.post(
f"{WOL_API_URL}/wake/{device}",
auth=auth,
timeout=5,
)
if r.ok:
return jsonify({"message": f"Wake command sent to {device}"})
return jsonify({"error": f"Failed to send wake command: {r.text}"}), r.status_code
except requests.RequestException as e:
return jsonify({"error": str(e)}), 500
@app.route("/weather")
def weather():
"""Proxy for OpenWeatherMap keeps API key server-side."""
if not WEATHER_API_KEY or not WEATHER_CITY:
return jsonify({"error": "Weather not configured"}), 503
try:
r = requests.get(
"https://api.openweathermap.org/data/2.5/weather",
params={
"q": WEATHER_CITY,
"appid": WEATHER_API_KEY,
"units": "metric",
},
timeout=5,
)
if r.ok:
return jsonify(r.json())
except requests.RequestException:
pass
return jsonify({"error": "Weather unavailable"}), 503
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)