Add Homepage dashboard on Proxmox with Palantir theme and Admin UI.

Deploy gethomepage on pve CT 120, categorized services from Homarr, RSS feeds,
custom styling, and a browser-based admin UI on the NAS for adding sites.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mo
2026-05-17 18:45:55 +02:00
parent 9f431ff97b
commit 43c4ed7a6d
27 changed files with 2851 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""Homepage Admin UI — sites toevoegen via browser."""
import os
import re
import subprocess
import threading
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
APP_ROOT = Path(os.environ.get("HOMEPAGE_DIR", Path(__file__).resolve().parent.parent))
WEBSITES_ADD = APP_ROOT / "websites-add.txt"
HOMELAB_APPS = APP_ROOT.parent
GENERATE = APP_ROOT / "generate-config.py"
DEPLOY = APP_ROOT / "deploy-to-pve.sh"
GROUPS = [
"Infrastructure",
"Media & TV",
"Smart Home",
"Productivity",
"Tools & Utils",
"AI Assistants",
"Dev & Docs",
"Web Design",
]
TAB_FOR_GROUP = {
"Infrastructure": "Ops",
"Media & TV": "Media",
"Smart Home": "Home",
"Productivity": "Work",
"Tools & Utils": "Ops",
"AI Assistants": "AI",
"Dev & Docs": "AI",
"Web Design": "AI",
}
_apply_lock = threading.Lock()
_apply_status = {"running": False, "ok": None, "log": ""}
class SiteIn(BaseModel):
group: str
name: str
url: str
class SiteOut(BaseModel):
id: int
group: str
name: str
url: str
tab: str
def read_sites() -> list[dict]:
if not WEBSITES_ADD.exists():
WEBSITES_ADD.write_text(
"# groep|naam|url\n# Of gebruik de UI op poort 3010\n\n",
encoding="utf-8",
)
rows = []
for i, raw in enumerate(WEBSITES_ADD.read_text(encoding="utf-8").splitlines()):
line = raw.strip()
if not line or line.startswith("#"):
continue
parts = [p.strip() for p in line.split("|", 2)]
if len(parts) != 3:
continue
g, n, u = parts
rows.append(
{
"id": len(rows),
"group": g,
"name": n,
"url": u,
"tab": TAB_FOR_GROUP.get(g, "?"),
}
)
return rows
def write_sites(rows: list[tuple[str, str, str]]) -> None:
lines = [
"# Extra websites — beheerd via Homepage Admin UI",
"# groep|naam|url",
"",
]
for g, n, u in rows:
n = n.replace("|", "-")
lines.append(f"{g}|{n}|{u}")
lines.append("")
WEBSITES_ADD.write_text("\n".join(lines), encoding="utf-8")
def run_apply():
global _apply_status
with _apply_lock:
_apply_status = {"running": True, "ok": None, "log": "Start…\n"}
try:
gen = subprocess.run(
["python3", str(GENERATE)],
cwd=str(APP_ROOT),
capture_output=True,
text=True,
timeout=120,
)
_apply_status["log"] += gen.stdout or ""
if gen.stderr:
_apply_status["log"] += gen.stderr
if gen.returncode != 0:
_apply_status["ok"] = False
_apply_status["log"] += "\nFout bij generate-config.py"
return
_apply_status["log"] += "\nDeploy naar Proxmox…\n"
dep = subprocess.run(
["/bin/sh", str(DEPLOY)],
cwd=str(APP_ROOT),
capture_output=True,
text=True,
timeout=300,
env={**os.environ},
)
_apply_status["log"] += dep.stdout or ""
if dep.stderr:
_apply_status["log"] += dep.stderr
_apply_status["ok"] = dep.returncode == 0
if _apply_status["ok"]:
_apply_status["log"] += "\n✓ Klaar — refresh Homepage (Ctrl+Shift+R)"
else:
_apply_status["log"] += "\n✗ Deploy mislukt"
except Exception as e:
_apply_status["ok"] = False
_apply_status["log"] += f"\nFout: {e}"
finally:
_apply_status["running"] = False
app = FastAPI(title="Homepage Admin")
static_dir = Path(__file__).parent / "static"
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
@app.get("/", response_class=HTMLResponse)
def index():
return FileResponse(static_dir / "index.html")
@app.get("/api/groups")
def api_groups():
return [{"name": g, "tab": TAB_FOR_GROUP.get(g, "")} for g in GROUPS]
@app.get("/api/sites")
def api_sites():
return read_sites()
@app.post("/api/sites")
def api_add(site: SiteIn):
if site.group not in GROUPS:
raise HTTPException(400, f"Onbekende groep: {site.group}")
name = site.name.strip()
url = site.url.strip()
if not name or not url:
raise HTTPException(400, "Naam en URL zijn verplicht")
if not re.match(r"^https?://", url, re.I):
raise HTTPException(400, "URL moet beginnen met http:// of https://")
rows = [(s["group"], s["name"], s["url"]) for s in read_sites()]
rows.append((site.group, name, url))
write_sites(rows)
return {"ok": True}
@app.delete("/api/sites/{site_id}")
def api_delete(site_id: int):
sites = read_sites()
if site_id < 0 or site_id >= len(sites):
raise HTTPException(404, "Site niet gevonden")
rows = [(s["group"], s["name"], s["url"]) for i, s in enumerate(sites) if i != site_id]
write_sites(rows)
return {"ok": True}
@app.post("/api/apply")
def api_apply():
if _apply_status.get("running"):
raise HTTPException(409, "Bezig met toepassen…")
t = threading.Thread(target=run_apply, daemon=True)
t.start()
return {"ok": True, "message": "Gestart"}
@app.get("/api/apply/status")
def api_apply_status():
return _apply_status
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3010)