Files
homelab-configs/apps/homepage/generate-config.py
T
mo 43c4ed7a6d 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>
2026-05-17 18:45:55 +02:00

373 lines
12 KiB
Python

#!/usr/bin/env python3
"""Generate Homepage YAML from Homarr default.json with smart categorization."""
import json
import re
from pathlib import Path
from urllib.parse import urlparse
_ROOT = Path(__file__).resolve().parent
HOMARR = _ROOT.parent / "homarr" / "config" / "default.json"
OUT = _ROOT / "config"
WEBSITES_ADD = _ROOT / "websites-add.txt"
ICON_MAP = {
"adguard": "adguard-home.png",
"gitea": "gitea.png",
"grafana": "grafana.png",
"prometheus": "prometheus.png",
"portainer": "portainer.png",
"postgres": "postgres.png",
"neo4j": "neo4j.png",
"home assistant": "home-assistant.png",
"ha ": "home-assistant.png",
"unifi": "unifi.png",
"nginx": "nginx-proxy-manager.png",
"vaultwarden": "vaultwarden.png",
"immich": "immich.png",
"n8n": "n8n.png",
"sonarr": "sonarr.png",
"radarr": "radarr.png",
"prowlarr": "prowlarr.png",
"qbittorrent": "qbittorrent.png",
"sabnzbd": "sabnzbd.png",
"nextcloud": "nextcloud.png",
"excalidraw": "excalidraw.png",
"wazuh": "wazuh.png",
"truenas": "truenas.png",
"frigate": "frigate.png",
"linkwarden": "linkwarden.png",
"uptime": "uptime-kuma.png",
"changedetection": "changedetection.png",
"traccar": "traccar.png",
"tunarr": "tunarr.png",
"metube": "metube.png",
"remotely": "remotely.png",
"homarr": "homarr.png",
"chatgpt": "si-openai",
"github": "si-github",
"anthropic": "si-anthropic",
"deepseek": "si-deepseek",
"grok": "si-x",
"youtube": "si-youtube",
"proxmox": "proxmox.png",
}
# Hex colors per group for mdi/si icons
GROUP_ICON_COLORS = {
"Infrastructure": "#3b82f6",
"Media & TV": "#a855f7",
"Smart Home": "#22c55e",
"Productivity": "#06b6d4",
"Tools & Utils": "#f59e0b",
"AI Assistants": "#ec4899",
"Dev & Docs": "#14b8a6",
"Web Design": "#f97316",
}
# Service groups → Homepage tab
SERVICE_GROUPS = [
"Infrastructure",
"Media & TV",
"Smart Home",
"Productivity",
"Tools & Utils",
"AI Assistants",
"Dev & Docs",
"Web Design",
]
HOMELAB_RULES = [
("Infrastructure", [
"proxmox", "portainer", "truenas", " adguard", "unifi", "wazuh", "idrac",
"webmin", "pve script", "modem", "rackula", "mo-nas", "nginx", "nas",
"lxc", "browserless", "remotely", "minarca", "change detection",
"changedetection", "dsm on", "dsm aux", "turnkey", "synology stuff",
]),
("Media & TV", [
"tunarr", "immich", "frigate", "metube", "nodecast", "nzb geek",
"usenetdeal",
]),
("Smart Home", [
" ha", "home assistant", "ring", "mawaqit", "home control", "ha voice",
]),
("Productivity", [
"nextcloud", "vaultwarden", "n8n", "gitea", "excalidraw", "linkwarden",
"invoice", "onlyoffice", "neo4j", "crm", "resume", "openresume",
"bentopdf", "vert", "moocup", "thunderbird", "noted apps",
]),
]
RSS_BOOKMARKS = {
"Tech News": [
("Hacker News", "https://hnrss.org/frontpage"),
("Ars Technica", "https://feeds.arstechnica.com/arstechnica/index"),
("The Verge", "https://www.theverge.com/rss/index.xml"),
("Lobsters", "https://lobste.rs/rss"),
],
"Security": [
("BleepingComputer", "https://www.bleepingcomputer.com/feed/"),
("Krebs on Security", "https://krebsonsecurity.com/feed/"),
("The Hacker News", "https://feeds.feedburner.com/TheHackersNews"),
],
"Homelab": [
("selfh.st", "https://selfh.st/rss/"),
("Proxmox Forum", "https://forum.proxmox.com/forums/-/index.rss"),
("r/selfhosted", "https://www.reddit.com/r/selfhosted/.rss"),
("Docker Blog", "https://www.docker.com/blog/feed/"),
("LinuxServer.io", "https://www.linuxserver.io/blog/rss/"),
("Marius Hosting", "https://mariushosting.com/feed/"),
],
"AI & Dev": [
("OpenAI Blog", "https://openai.com/blog/rss.xml"),
("Hugging Face Blog", "https://huggingface.co/blog/feed.xml"),
("GitHub Blog", "https://github.blog/feed/"),
],
"Blogs": [
("Jack van lightly", "https://jack-vanlightly.com/feed.xml"),
("Juhache", "https://juhache.substack.com/feed"),
],
}
def slug_name(name: str) -> str:
s = re.sub(r"[^a-zA-Z0-9]+", "-", name).strip("-").lower()
return s or "service"
def guess_icon(name: str, url: str) -> str:
n = name.lower()
for key, icon in ICON_MAP.items():
if key in n:
return icon
host = urlparse(url).hostname or ""
if host:
part = host.split(".")[0]
if part in ("192", "www"):
return "mdi-server-network"
return f"mdi-{part[:12]}"
return "mdi-circle-outline"
def colorize_icon(icon: str, group: str) -> str:
"""Add hex color to vector icons for vivid Homepage gradients."""
if "#" in icon or icon.endswith((".png", ".webp", ".svg")) or icon.startswith("http"):
return icon
color = GROUP_ICON_COLORS.get(group, "#22d3ee")
if icon.startswith(("mdi-", "si-")):
return f"{icon}-{color}"
return icon
def classify_homelab(name: str, url: str) -> str:
blob = f" {name.lower()} {url.lower()} "
for group, keys in HOMELAB_RULES:
if any(k in blob for k in keys):
return group
return "Tools & Utils"
def classify_app(name: str, url: str, homarr_cat: str) -> str:
if homarr_cat == "AI":
return "AI Assistants"
if homarr_cat == "HTML5":
return "Web Design"
if homarr_cat in ("Other", "Data Stuff", "Projects"):
return "Dev & Docs"
if homarr_cat == "HomeLab Stuff":
return classify_homelab(name, url)
return "Tools & Utils"
def yaml_escape(s: str) -> str:
return s.replace("\\", "\\\\").replace('"', '\\"')
def load_websites_extra() -> dict:
"""Lees websites-add.txt: groep|naam|url per regel."""
extra = {g: [] for g in SERVICE_GROUPS}
if not WEBSITES_ADD.exists():
return extra
for raw in 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
group, name, url = parts
if group not in extra:
print(f" Waarschuwing: onbekende groep '{group}' — regel overgeslagen")
continue
if name and url:
extra[group].append((name, url))
return extra
def merge_groups(base: dict, extra: dict) -> dict:
"""Homarr + handmatige sites; extra overschrijft zelfde naam in zelfde groep."""
out = {g: list(base.get(g, [])) for g in SERVICE_GROUPS}
for group, items in extra.items():
if group not in out:
out[group] = []
names = {n.lower() for n, _ in out[group]}
for name, url in items:
key = name.lower()
if key in names:
out[group] = [(n, u) for n, u in out[group] if n.lower() != key]
names.discard(key)
out[group].append((name, url))
names.add(key)
return out
def write_services(by_group: dict) -> str:
lines = []
for group in SERVICE_GROUPS:
items = by_group.get(group, [])
if not items:
continue
lines.append(f"- {group}:")
seen = set()
for name, url in items:
key = slug_name(name)
if key in seen:
key = f"{key}-{len(seen)}"
seen.add(key)
icon = colorize_icon(guess_icon(name, url), group)
lines.append(f" - {name}:")
lines.append(f" icon: {icon}")
lines.append(f" href: {url}")
lines.append(f" description: {yaml_escape(name)}")
try:
p = urlparse(url)
if p.scheme and p.hostname and re.match(r"^\d+\.\d+\.\d+\.\d+$", p.hostname):
base = f"{p.scheme}://{p.netloc}"
lines.append(f" siteMonitor: {base}")
lines.append(" statusStyle: dot")
except Exception:
pass
lines.append("")
return "\n".join(lines)
def write_settings() -> str:
layout = {
"Infrastructure": {"tab": "Ops", "icon": "mdi-server-network", "columns": 4, "style": "row"},
"Media & TV": {"tab": "Media", "icon": "mdi-television-play", "columns": 4, "style": "row"},
"Smart Home": {"tab": "Home", "icon": "mdi-home-automation", "columns": 3, "style": "row"},
"Productivity": {"tab": "Work", "icon": "mdi-briefcase-outline", "columns": 4, "style": "row"},
"Tools & Utils": {"tab": "Ops", "icon": "mdi-toolbox-outline", "columns": 3, "style": "row"},
"AI Assistants": {"tab": "AI", "icon": "mdi-brain", "columns": 4, "style": "row"},
"Dev & Docs": {"tab": "AI", "icon": "mdi-book-open-page-variant", "columns": 3, "style": "row"},
"Web Design": {"tab": "AI", "icon": "mdi-palette-outline", "columns": 3, "style": "row"},
"Tech News": {"tab": "Feeds", "icon": "mdi-newspaper-variant-outline", "columns": 2},
"Security": {"tab": "Feeds", "icon": "mdi-shield-alert-outline", "columns": 2},
"Homelab": {"tab": "Feeds", "icon": "mdi-server", "columns": 3},
"AI & Dev": {"tab": "Feeds", "icon": "mdi-robot-outline", "columns": 2},
"Blogs": {"tab": "Feeds", "icon": "mdi-rss", "columns": 2},
}
lines = [
"---",
"title: EL-KADI OPS",
"description: Homelab command surface",
"theme: dark",
"color: slate",
"iconStyle: gradient",
"headerStyle: underlined",
"useEqualHeights: true",
"statusStyle: dot",
"fullWidth: true",
"cardBlur: md",
"hideVersion: false",
"quicklaunch:",
" searchDescriptions: true",
" hideInternetSearch: false",
" showSearchSuggestions: true",
"layout:",
]
for name, opts in layout.items():
lines.append(f" {name}:")
for k, v in opts.items():
lines.append(f" {k}: {v}")
return "\n".join(lines) + "\n"
def write_bookmarks() -> str:
lines = ["---"]
for group, items in RSS_BOOKMARKS.items():
lines.append(f"- {group}:")
for name, url in items:
abbr = "".join(w[0] for w in re.findall(r"[A-Za-z0-9]+", name)[:2]).upper()[:2] or "RS"
lines.append(f" - {name}:")
lines.append(f" - abbr: {abbr}")
lines.append(f" href: {url}")
return "\n".join(lines) + "\n"
def write_widgets() -> str:
return """---
- logo:
icon: /images/logo.jpg
- greeting:
text_size: xl
text: el-kadi ops
- datetime:
text_size: xl
format:
timeStyle: short
dateStyle: medium
hour12: false
- search:
provider: google
target: _blank
showSearchSuggestions: true
- resources:
cpu: true
memory: true
cputemp: true
uptime: true
units: metric
refresh: 3000
"""
def main():
with open(HOMARR) as f:
d = json.load(f)
cats = {c["id"]: c["name"] for c in d.get("categories", [])}
by_group: dict[str, list] = {g: [] for g in SERVICE_GROUPS}
for a in d.get("apps", []):
name = (a.get("name") or "").strip()
url = (a.get("url") or "").strip()
if not name or not url:
continue
area = a.get("area", {}).get("properties", {}).get("id", "")
homarr_cat = cats.get(area, "Other")
group = classify_app(name, url, homarr_cat)
by_group.setdefault(group, []).append((name, url))
extra = load_websites_extra()
extra_count = sum(len(v) for v in extra.values())
by_group = merge_groups(by_group, extra)
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "services.yaml").write_text(write_services(by_group))
(OUT / "settings.yaml").write_text(write_settings())
(OUT / "widgets.yaml").write_text(write_widgets())
(OUT / "bookmarks.yaml").write_text(write_bookmarks())
total = sum(len(v) for v in by_group.values())
print(
f"Generated {total} apps in {sum(1 for v in by_group.values() if v)} groups"
+ (f" (+{extra_count} uit websites-add.txt)" if extra_count else "")
)
if __name__ == "__main__":
main()