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:
@@ -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)
|
||||
Reference in New Issue
Block a user