214 lines
6.9 KiB
Python
214 lines
6.9 KiB
Python
from fastapi import FastAPI, Request, Form
|
||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||
from jinja2 import Template
|
||
import requests
|
||
import json
|
||
from dateutil import parser, tz
|
||
from pathlib import Path
|
||
|
||
app = FastAPI()
|
||
|
||
API_URL = "http://192.168.18.4:8000/results/updatecheck/"
|
||
READ_API_KEY = "read_key_1"
|
||
REFERENCE_FILE = "reference_versions.json"
|
||
|
||
def fetch_raw_lines():
|
||
try:
|
||
response = requests.get(API_URL, headers={"Authorization": f"Bearer {READ_API_KEY}"}, timeout=5)
|
||
response.raise_for_status()
|
||
return response.json().get("lines", [])
|
||
except Exception as e:
|
||
print(f"❌ Fehler beim Abrufen der Logs: {e}")
|
||
return []
|
||
|
||
def fetch_reference():
|
||
try:
|
||
with open(REFERENCE_FILE) as f:
|
||
return json.load(f)
|
||
except Exception:
|
||
return {}
|
||
|
||
def fetch_latest_per_host():
|
||
lines = fetch_raw_lines()
|
||
reference = fetch_reference()
|
||
latest = {}
|
||
|
||
for line in lines:
|
||
parts = line.strip().split(",", 3)
|
||
if len(parts) < 4:
|
||
continue
|
||
timestamp, hostname, _, packages = parts
|
||
if hostname not in latest or timestamp > latest[hostname]["timestamp"]:
|
||
latest[hostname] = {
|
||
"timestamp": timestamp,
|
||
"raw": packages
|
||
}
|
||
|
||
result = {}
|
||
for host, entry in latest.items():
|
||
try:
|
||
dt = parser.isoparse(entry["timestamp"]).astimezone(tz.gettz("Europe/Berlin"))
|
||
display_time = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||
except:
|
||
display_time = entry["timestamp"]
|
||
|
||
pkgs = {}
|
||
for p in entry["raw"].strip(";").split(";"):
|
||
if "=" in p:
|
||
k, v = p.split("=", 1)
|
||
pkgs[k] = {
|
||
"current": v,
|
||
"expected": reference.get(k)
|
||
}
|
||
|
||
result[host] = {
|
||
"timestamp": display_time,
|
||
"packages": pkgs
|
||
}
|
||
return result
|
||
|
||
def fetch_history_for_host(hostname):
|
||
lines = fetch_raw_lines()
|
||
history = []
|
||
for line in lines:
|
||
parts = line.strip().split(",", 3)
|
||
if len(parts) < 4 or parts[1] != hostname:
|
||
continue
|
||
timestamp, _, _, raw = parts
|
||
pkgs = {}
|
||
for p in raw.strip(";").split(";"):
|
||
if "=" in p:
|
||
k, v = p.split("=", 1)
|
||
pkgs[k] = v
|
||
history.append({"timestamp": timestamp, "packages": pkgs})
|
||
history.sort(key=lambda x: x["timestamp"], reverse=True)
|
||
return history
|
||
|
||
@app.get("/", response_class=HTMLResponse)
|
||
def index():
|
||
data = fetch_latest_per_host()
|
||
return MAIN_TEMPLATE.render(data=data)
|
||
|
||
@app.get("/host/{hostname}", response_class=HTMLResponse)
|
||
def host_view(hostname: str):
|
||
return HOST_TEMPLATE.render(hostname=hostname, history=fetch_history_for_host(hostname))
|
||
|
||
@app.get("/edit", response_class=HTMLResponse)
|
||
def edit_view():
|
||
try:
|
||
data = Path(REFERENCE_FILE).read_text()
|
||
except:
|
||
data = "{}"
|
||
return EDIT_TEMPLATE.render(content=data)
|
||
|
||
@app.post("/edit", response_class=RedirectResponse)
|
||
def edit_post(content: str = Form(...)):
|
||
try:
|
||
json.loads(content)
|
||
Path(REFERENCE_FILE).write_text(content)
|
||
except Exception as e:
|
||
print(f"Fehler beim Schreiben der Referenzdatei: {e}")
|
||
return RedirectResponse(url="/edit", status_code=303)
|
||
|
||
# ========== Templates ==========
|
||
|
||
MAIN_TEMPLATE = Template("""
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Update Übersicht</title>
|
||
<style>
|
||
body { font-family: sans-serif; margin: 2rem; }
|
||
.ok { background: #d4edda; }
|
||
.outdated { background: #f8d7da; }
|
||
.missing { background: #fff3cd; }
|
||
table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
|
||
th, td { border: 1px solid #ccc; padding: 4px 10px; }
|
||
#search { margin-bottom: 1rem; padding: 4px; width: 250px; }
|
||
</style>
|
||
<script>
|
||
function toggle(id) {
|
||
const el = document.getElementById(id);
|
||
el.style.display = el.style.display === "none" ? "block" : "none";
|
||
}
|
||
function filterTable() {
|
||
let q = document.getElementById("search").value.toLowerCase();
|
||
document.querySelectorAll(".host-block").forEach(div => {
|
||
div.style.display = div.innerText.toLowerCase().includes(q) ? "block" : "none";
|
||
});
|
||
}
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<h1>Update Übersicht</h1>
|
||
<input id="search" placeholder="🔍 Host oder Paket..." onkeyup="filterTable()">
|
||
<a href="/edit">⚙️ Referenz bearbeiten</a>
|
||
{% for host, d in data.items() %}
|
||
<div class="host-block">
|
||
<button onclick="toggle('b-{{ host }}')">{{ host }} (Stand: {{ d.timestamp }})</button>
|
||
<a href="/host/{{ host }}">🔍 Verlauf</a>
|
||
<div id="b-{{ host }}" style="display:none">
|
||
<table>
|
||
<tr><th>Paket</th><th>Installiert</th><th>Referenz</th><th>Status</th></tr>
|
||
{% for p, info in d.packages.items() %}
|
||
<tr class="{% if not info.expected %}missing{% elif info.current != info.expected %}outdated{% else %}ok{% endif %}">
|
||
<td>{{ p }}</td>
|
||
<td>{{ info.current }}</td>
|
||
<td>{{ info.expected or '–' }}</td>
|
||
<td>
|
||
{% if not info.expected %}❓ fehlt
|
||
{% elif info.current != info.expected %}❌
|
||
{% else %}✅
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</body>
|
||
</html>
|
||
""")
|
||
|
||
HOST_TEMPLATE = Template("""
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="utf-8"><title>{{ hostname }}</title>
|
||
<style>
|
||
body { font-family: sans-serif; margin: 2rem; }
|
||
table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
|
||
th, td { border: 1px solid #ccc; padding: 4px 10px; }
|
||
</style></head>
|
||
<body>
|
||
<h1>Verlauf: {{ hostname }}</h1>
|
||
<a href="/">⬅ Zurück</a>
|
||
{% for h in history %}
|
||
<h3>{{ h.timestamp }}</h3>
|
||
<table>
|
||
<tr><th>Paket</th><th>Version</th></tr>
|
||
{% for p, v in h.packages.items() %}
|
||
<tr><td>{{ p }}</td><td>{{ v }}</td></tr>
|
||
{% endfor %}
|
||
</table>
|
||
{% endfor %}
|
||
</body>
|
||
</html>
|
||
""")
|
||
|
||
EDIT_TEMPLATE = Template("""
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="utf-8"><title>Referenz bearbeiten</title>
|
||
<style>body { font-family: sans-serif; margin: 2rem; }</style>
|
||
</head><body>
|
||
<h1>Referenzdatei bearbeiten</h1>
|
||
<a href="/">⬅ Zurück</a>
|
||
<form method="post">
|
||
<textarea name="content" style="width:100%;height:400px;">{{ content }}</textarea><br>
|
||
<button type="submit">💾 Speichern</button>
|
||
</form>
|
||
</body>
|
||
</html>
|
||
""")
|