From 43c4ed7a6d186dd0c5cab5c73ab2e30567629f59 Mon Sep 17 00:00:00 2001 From: mo Date: Sun, 17 May 2026 18:45:55 +0200 Subject: [PATCH] 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 --- INVENTORY.md | 1 + apps/homepage/README.md | 89 +++++ apps/homepage/add-website.sh | 59 +++ apps/homepage/admin/Dockerfile | 17 + apps/homepage/admin/requirements.txt | 3 + apps/homepage/admin/server.py | 208 ++++++++++ apps/homepage/admin/static/index.html | 260 +++++++++++++ apps/homepage/apply.sh | 9 + apps/homepage/config/bookmarks.yaml | 60 +++ apps/homepage/config/custom.css | 431 +++++++++++++++++++++ apps/homepage/config/custom.js | 143 +++++++ apps/homepage/config/services.yaml | 505 +++++++++++++++++++++++++ apps/homepage/config/settings.yaml | 77 ++++ apps/homepage/config/widgets.yaml | 27 ++ apps/homepage/custom.css | 431 +++++++++++++++++++++ apps/homepage/deploy-to-pve.sh | 64 ++++ apps/homepage/docker-compose.admin.yml | 17 + apps/homepage/docker-compose.yml | 18 + apps/homepage/generate-config.py | 372 ++++++++++++++++++ apps/homepage/public/images/README.md | 18 + apps/homepage/public/images/logo.jpg | Bin 0 -> 10131 bytes apps/homepage/set-logo.sh | 9 + apps/homepage/start-admin.sh | 12 + apps/homepage/websites-add.txt | 7 + apps/homepage/websites.yaml | 2 + apps/proxmox/hosts/pve/lxc/120.conf | 11 + apps/proxmox/lxc-inventory.md | 1 + 27 files changed, 2851 insertions(+) create mode 100644 apps/homepage/README.md create mode 100644 apps/homepage/add-website.sh create mode 100644 apps/homepage/admin/Dockerfile create mode 100644 apps/homepage/admin/requirements.txt create mode 100644 apps/homepage/admin/server.py create mode 100644 apps/homepage/admin/static/index.html create mode 100644 apps/homepage/apply.sh create mode 100644 apps/homepage/config/bookmarks.yaml create mode 100644 apps/homepage/config/custom.css create mode 100644 apps/homepage/config/custom.js create mode 100644 apps/homepage/config/services.yaml create mode 100644 apps/homepage/config/settings.yaml create mode 100644 apps/homepage/config/widgets.yaml create mode 100644 apps/homepage/custom.css create mode 100644 apps/homepage/deploy-to-pve.sh create mode 100644 apps/homepage/docker-compose.admin.yml create mode 100644 apps/homepage/docker-compose.yml create mode 100644 apps/homepage/generate-config.py create mode 100644 apps/homepage/public/images/README.md create mode 100644 apps/homepage/public/images/logo.jpg create mode 100644 apps/homepage/set-logo.sh create mode 100644 apps/homepage/start-admin.sh create mode 100644 apps/homepage/websites-add.txt create mode 100644 apps/homepage/websites.yaml create mode 100644 apps/proxmox/hosts/pve/lxc/120.conf diff --git a/INVENTORY.md b/INVENTORY.md index 4c58d60..d25bee4 100644 --- a/INVENTORY.md +++ b/INVENTORY.md @@ -13,6 +13,7 @@ Private repo. Laatst bijgewerkt vanaf NAS `192.168.1.211`. | DuckDNS | [apps/duckdns](apps/duckdns/) | — | running | | Neo4j | [apps/neo4j](apps/neo4j/) | :49153–49155 | running | | Homarr | [apps/homarr](apps/homarr/) | :4755 | running | +| Homepage | [apps/homepage](apps/homepage/) | http://192.168.1.192:3000 (pve CT 120) | running | | Portainer | [apps/portainer](apps/portainer/) | :9000 | running | | Remotely | [apps/remotely](apps/remotely/) | :8080 | running | | Excalidraw | [apps/excalidraw](apps/excalidraw/) | :3765 | running | diff --git a/apps/homepage/README.md b/apps/homepage/README.md new file mode 100644 index 0000000..07a3d5f --- /dev/null +++ b/apps/homepage/README.md @@ -0,0 +1,89 @@ +# Homepage (gethomepage.dev) + +Palantir-stijl homelab-dashboard. + +| | | +|---|---| +| **URL** | http://192.168.1.192:3000 | +| **Proxmox** | pve CT 120 (`192.168.1.216`) | + +## Grafisch toevoegen (Admin UI) + +Open in je browser: + +**http://192.168.1.211:3010** + +Eerste keer starten op de NAS: + +```bash +cd /volume1/docker/homelab-configs/apps/homepage +./start-admin.sh +``` + +1. Vul naam + URL in → **Toevoegen** +2. Klik **Toepassen op Homepage** (deploy naar Proxmox) +3. Refresh http://192.168.1.192:3000 + +--- + +## Snel een website toevoegen (terminal) + +**Niet** in `services.yaml` zoeken — gebruik **`websites-add.txt`** of het script: + +### Optie A — interactief (makkelijkst) + +```bash +cd /volume1/docker/homelab-configs/apps/homepage +./add-website.sh +``` + +### Optie B — één commando + +```bash +./add-website.sh "Jellyfin" "http://192.168.1.10:8096" "Media & TV" +``` + +### Optie C — regel toevoegen in `websites-add.txt` + +``` +Productivity|Mijn app|http://192.168.1.150:8080 +AI Assistants|Claude|https://claude.ai +``` + +Daarna: `./apply.sh` (genereert config + deploy) + +**Groepen:** `Infrastructure`, `Media & TV`, `Smart Home`, `Productivity`, `Tools & Utils`, `AI Assistants`, `Dev & Docs`, `Web Design` + +## Tabs + +| Tab | Inhoud | +|-----|--------| +| **Ops** | Infrastructure, Tools & Utils | +| **Media** | Media & TV | +| **Home** | Smart Home | +| **Work** | Productivity | +| **AI** | AI Assistants, Dev & Docs, Web Design | +| **Feeds** | RSS + bookmarks | + +## Overige commando's + +```bash +./set-logo.sh /pad/naar/foto.jpg # profielfoto +python3 generate-config.py # Homarr + websites-add.txt → config/ +./deploy-to-pve.sh # alleen deploy +./apply.sh # generate + deploy +``` + +## Config-bestanden + +| Bestand | Wie bewerkt | +|---------|-------------| +| **`websites-add.txt`** | **Jij** — extra links (1 regel per site) | +| `config/services.yaml` | Automatisch gegenereerd | +| `config/settings.yaml` | Tabs, layout | +| `config/widgets.yaml` | Logo, klok, zoeken | +| `config/custom.css` | Thema | + +Homarr-sync: wijzigingen in Homarr → `python3 generate-config.py` → `./apply.sh` + +Docs: https://gethomepage.dev/ diff --git a/apps/homepage/add-website.sh b/apps/homepage/add-website.sh new file mode 100644 index 0000000..b77dc38 --- /dev/null +++ b/apps/homepage/add-website.sh @@ -0,0 +1,59 @@ +#!/bin/sh +# Voeg snel een website toe: ./add-website.sh of ./add-website.sh "Naam" "url" "groep" +set -e +APP="$(cd "$(dirname "$0")" && pwd)" +FILE="$APP/websites-add.txt" + +usage() { + echo "Gebruik:" + echo " $0" + echo " $0 \"Naam\" \"http://url\" [groep]" + echo "" + echo "Groepen: Infrastructure | \"Media & TV\" | \"Smart Home\" | Productivity" + echo " \"Tools & Utils\" | \"AI Assistants\" | \"Dev & Docs\" | \"Web Design\"" + exit 1 +} + +pick_group() { + echo "Kies groep:" + echo " 1) Infrastructure 5) Tools & Utils" + echo " 2) Media & TV 6) AI Assistants" + echo " 3) Smart Home 7) Dev & Docs" + echo " 4) Productivity 8) Web Design" + printf "> " + read -r n + case "$n" in + 1) echo "Infrastructure" ;; + 2) echo "Media & TV" ;; + 3) echo "Smart Home" ;; + 4) echo "Productivity" ;; + 5) echo "Tools & Utils" ;; + 6) echo "AI Assistants" ;; + 7) echo "Dev & Docs" ;; + 8) echo "Web Design" ;; + *) echo "Productivity" ;; + esac +} + +if [ $# -eq 0 ]; then + printf "Naam: " + read -r NAME + printf "URL: " + read -r URL + GROUP=$(pick_group) +elif [ $# -ge 2 ]; then + NAME="$1" + URL="$2" + GROUP="${3:-Productivity}" +else + usage +fi + +[ -n "$NAME" ] && [ -n "$URL" ] || usage + +# Pipe in naam/url escapen +NAME_ESC=$(printf '%s' "$NAME" | sed 's/|/-/g') +echo "${GROUP}|${NAME_ESC}|${URL}" >> "$FILE" +echo "Toegevoegd: ${GROUP} | ${NAME} | ${URL}" +echo "" +"$APP/apply.sh" diff --git a/apps/homepage/admin/Dockerfile b/apps/homepage/admin/Dockerfile new file mode 100644 index 0000000..d407b6b --- /dev/null +++ b/apps/homepage/admin/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.11-slim-bookworm + +RUN apt-get update && apt-get install -y --no-install-recommends \ + docker.io curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY server.py . +COPY static ./static + +ENV PYTHONUNBUFFERED=1 +EXPOSE 3010 + +CMD ["python", "server.py"] diff --git a/apps/homepage/admin/requirements.txt b/apps/homepage/admin/requirements.txt new file mode 100644 index 0000000..ada379a --- /dev/null +++ b/apps/homepage/admin/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.115.6 +uvicorn[standard]==0.32.1 +pydantic==2.10.3 diff --git a/apps/homepage/admin/server.py b/apps/homepage/admin/server.py new file mode 100644 index 0000000..f21ec91 --- /dev/null +++ b/apps/homepage/admin/server.py @@ -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) diff --git a/apps/homepage/admin/static/index.html b/apps/homepage/admin/static/index.html new file mode 100644 index 0000000..cf7eefd --- /dev/null +++ b/apps/homepage/admin/static/index.html @@ -0,0 +1,260 @@ + + + + + + Homepage Admin — EL-KADI OPS + + + + + +
+
+

Homepage AdminWebsites toevoegen

+ +
+ +
+

Nieuwe website

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+

Jouw extra sites

+
+

Laden…

+
+
+ +
+

Dashboard bijwerken

+

+ Na toevoegen of verwijderen: klik hieronder. Dit genereert de config en deployt naar Proxmox (±1 min). +

+ +

+    
+
+
+ + + + diff --git a/apps/homepage/apply.sh b/apps/homepage/apply.sh new file mode 100644 index 0000000..9653003 --- /dev/null +++ b/apps/homepage/apply.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Genereer config + deploy naar Proxmox (na wijziging websites.yaml of Homarr) +set -e +APP="$(cd "$(dirname "$0")" && pwd)" +cd "$APP" +echo "=== Config genereren (Homarr + websites.yaml) ===" +python3 generate-config.py +echo "=== Deploy naar CT 120 ===" +./deploy-to-pve.sh diff --git a/apps/homepage/config/bookmarks.yaml b/apps/homepage/config/bookmarks.yaml new file mode 100644 index 0000000..7839150 --- /dev/null +++ b/apps/homepage/config/bookmarks.yaml @@ -0,0 +1,60 @@ +--- +- Tech News: + - Hacker News: + - abbr: HN + href: https://hnrss.org/frontpage + - Ars Technica: + - abbr: AT + href: https://feeds.arstechnica.com/arstechnica/index + - The Verge: + - abbr: TV + href: https://www.theverge.com/rss/index.xml + - Lobsters: + - abbr: L + href: https://lobste.rs/rss +- Security: + - BleepingComputer: + - abbr: B + href: https://www.bleepingcomputer.com/feed/ + - Krebs on Security: + - abbr: KO + href: https://krebsonsecurity.com/feed/ + - The Hacker News: + - abbr: TH + href: https://feeds.feedburner.com/TheHackersNews +- Homelab: + - selfh.st: + - abbr: SS + href: https://selfh.st/rss/ + - Proxmox Forum: + - abbr: PF + href: https://forum.proxmox.com/forums/-/index.rss + - r/selfhosted: + - abbr: RS + href: https://www.reddit.com/r/selfhosted/.rss + - Docker Blog: + - abbr: DB + href: https://www.docker.com/blog/feed/ + - LinuxServer.io: + - abbr: LI + href: https://www.linuxserver.io/blog/rss/ + - Marius Hosting: + - abbr: MH + href: https://mariushosting.com/feed/ +- AI & Dev: + - OpenAI Blog: + - abbr: OB + href: https://openai.com/blog/rss.xml + - Hugging Face Blog: + - abbr: HF + href: https://huggingface.co/blog/feed.xml + - GitHub Blog: + - abbr: GB + href: https://github.blog/feed/ +- Blogs: + - Jack van lightly: + - abbr: JV + href: https://jack-vanlightly.com/feed.xml + - Juhache: + - abbr: J + href: https://juhache.substack.com/feed diff --git a/apps/homepage/config/custom.css b/apps/homepage/config/custom.css new file mode 100644 index 0000000..c217be0 --- /dev/null +++ b/apps/homepage/config/custom.css @@ -0,0 +1,431 @@ +/* EL-KADI OPS — Palantir blue + orange */ +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@400;500;600&display=swap'); + +:root { + --p-void: #02060c; + --p-deep: #051018; + --p-surface: #0a1424; + --p-elevated: rgba(10, 22, 40, 0.92); + --p-border: rgba(56, 132, 220, 0.18); + --p-border-hot: rgba(251, 146, 60, 0.45); + --p-blue: #3b82f6; + --p-cyan: #22d3ee; + --p-orange: #fb923c; + --p-amber: #fbbf24; + --p-teal: #2dd4bf; + --p-text: #e8eef7; + --p-muted: #64748b; + --p-mono: 'JetBrains Mono', ui-monospace, monospace; + --p-sans: 'DM Sans', system-ui, sans-serif; +} + +html { color-scheme: dark; } + +body { + font-family: var(--p-sans) !important; + background: var(--p-void) !important; + color: var(--p-text) !important; +} + +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 100% 70% at 8% -15%, rgba(59, 130, 246, 0.22), transparent 52%), + radial-gradient(ellipse 80% 55% at 95% 5%, rgba(34, 211, 238, 0.12), transparent 48%), + radial-gradient(ellipse 60% 50% at 88% 75%, rgba(251, 146, 60, 0.14), transparent 45%), + radial-gradient(ellipse 50% 40% at 12% 85%, rgba(251, 146, 60, 0.08), transparent 40%), + linear-gradient(165deg, #061018 0%, #02060c 45%, #030810 100%); + pointer-events: none; + z-index: 0; + animation: bg-breathe 14s ease-in-out infinite alternate; +} + +@keyframes bg-breathe { + 0% { opacity: 1; } + 100% { opacity: 0.9; } +} + +body::after { + content: ''; + position: fixed; + inset: 0; + background-image: + linear-gradient(rgba(59, 130, 246, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(59, 130, 246, 0.04) 1px, transparent 1px); + background-size: 56px 56px; + mask-image: radial-gradient(ellipse 90% 80% at 50% 30%, black 10%, transparent 72%); + pointer-events: none; + z-index: 0; + animation: grid-drift 40s linear infinite; +} + +@keyframes grid-drift { + from { transform: translate(0, 0); } + to { transform: translate(-56px, -56px); } +} + +.palantir-scanline { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 1; + background: repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(0,0,0,0.04) 3px, rgba(0,0,0,0.04) 6px); + opacity: 0.35; +} + +.palantir-noise { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 1; + opacity: 0.03; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); +} + +#page_wrapper { position: relative; z-index: 2; } + +#information-widget-greeting, .information-widget-greeting { + font-weight: 600 !important; + letter-spacing: 0.22em !important; + text-transform: uppercase !important; + font-size: 0.7rem !important; + color: var(--p-orange) !important; + font-family: var(--p-mono) !important; +} + +.information-widget { + border: 1px solid var(--p-border) !important; + border-radius: 10px !important; + background: var(--p-elevated) !important; + backdrop-filter: blur(14px); + box-shadow: 0 4px 24px rgba(0,0,0,0.35), inset 0 1px 0 rgba(59,130,246,0.08); +} + +button[role="tab"] { + font-family: var(--p-mono) !important; + font-size: 0.68rem !important; + letter-spacing: 0.14em !important; + text-transform: uppercase !important; + border: 1px solid var(--p-border) !important; + border-radius: 6px !important; + transition: all 0.25s ease; +} + +button[role="tab"][aria-selected="true"] { + background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(251,146,60,0.12)) !important; + border-color: var(--p-border-hot) !important; + color: var(--p-cyan) !important; + box-shadow: 0 0 20px rgba(251,146,60,0.15); +} + +.service-block, #services .service-group .service { + border: 1px solid var(--p-border) !important; + border-radius: 10px !important; + background: var(--p-elevated) !important; + backdrop-filter: blur(12px); + transition: border-color 0.2s, box-shadow 0.25s, transform 0.2s; +} + +.service-block:hover, #services .service-group .service:hover { + border-color: rgba(251,146,60,0.5) !important; + box-shadow: 0 0 28px rgba(59,130,246,0.12), 0 0 12px rgba(251,146,60,0.1); + transform: translateY(-1px); +} + +#services h2.service-group-name, .service-group-title { + font-family: var(--p-mono) !important; + font-size: 0.62rem !important; + letter-spacing: 0.2em !important; + text-transform: uppercase !important; + color: var(--p-cyan) !important; +} + +#bookmarks .bookmark-group h2, .bookmark-group-name { + font-family: var(--p-mono) !important; + font-size: 0.62rem !important; + letter-spacing: 0.18em !important; + text-transform: uppercase !important; + color: var(--p-orange) !important; +} + +#bookmarks a { + border: 1px solid var(--p-border) !important; + transition: border-color 0.2s, box-shadow 0.2s; +} + +#bookmarks a:hover { + border-color: rgba(251,146,60,0.4) !important; + box-shadow: 0 0 16px rgba(59,130,246,0.1); +} + +.rss-hub { + margin: 1rem 1.5rem 1.5rem; + padding: 1.25rem 1.5rem; + border: 1px solid var(--p-border); + border-radius: 14px; + background: linear-gradient(145deg, rgba(10,22,40,0.95), rgba(6,12,22,0.98)); + box-shadow: 0 8px 40px rgba(0,0,0,0.45), inset 0 1px 0 rgba(59,130,246,0.12); + backdrop-filter: blur(16px); + position: relative; + overflow: hidden; +} + +.rss-hub::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--p-blue), var(--p-orange), var(--p-cyan), transparent); + animation: rss-scan 4s ease-in-out infinite; +} + +@keyframes rss-scan { + 0%, 100% { opacity: 0.5; transform: scaleX(0.6); } + 50% { opacity: 1; transform: scaleX(1); } +} + +.rss-hub-title { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 0.5rem 1rem; + margin-bottom: 1.25rem; + font-family: var(--p-mono); + font-size: 0.72rem; + letter-spacing: 0.28em; + text-transform: uppercase; + color: var(--p-cyan); +} + +.rss-hub-accent { + display: inline-block; + width: 8px; height: 8px; + border-radius: 50%; + background: var(--p-orange); + box-shadow: 0 0 12px var(--p-orange); + animation: pulse-dot 2s ease-in-out infinite; +} + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(0.85); } +} + +.rss-hub-sub { + font-size: 0.58rem; + letter-spacing: 0.1em; + color: var(--p-muted); + text-transform: none; +} + +.rss-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; +} + +@media (max-width: 1280px) { .rss-grid { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 640px) { .rss-grid { grid-template-columns: 1fr; } } + +.rss-column { + border: 1px solid rgba(59,130,246,0.12); + border-radius: 10px; + padding: 0.75rem; + background: rgba(5,12,24,0.6); + min-height: 160px; +} + +.rss-column-head { + display: flex; + align-items: center; + gap: 0.5rem; + font-family: var(--p-mono); + font-size: 0.58rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--p-orange); + margin-bottom: 0.65rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--p-border); +} + +.rss-pulse { + width: 6px; height: 6px; + border-radius: 50%; + background: var(--p-cyan); + animation: pulse-dot 1.8s ease-in-out infinite; +} + +.rss-feed-block { list-style: none; margin-bottom: 0.85rem; } + +.rss-feed-name { + display: block; + font-family: var(--p-mono); + font-size: 0.55rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--p-muted); + margin-bottom: 0.35rem; +} + +.rss-items { list-style: none; padding: 0; margin: 0; } + +.rss-items li { + display: flex; + justify-content: space-between; + gap: 0.5rem; + padding: 0.28rem 0; + border-bottom: 1px solid rgba(255,255,255,0.04); + font-size: 0.72rem; + line-height: 1.35; + animation: rss-fade-in 0.4s ease forwards; + opacity: 0; +} + +.rss-items li:nth-child(1) { animation-delay: 0.05s; } +.rss-items li:nth-child(2) { animation-delay: 0.1s; } +.rss-items li:nth-child(3) { animation-delay: 0.15s; } +.rss-items li:nth-child(4) { animation-delay: 0.2s; } +.rss-items li:nth-child(5) { animation-delay: 0.25s; } + +@keyframes rss-fade-in { to { opacity: 1; } } + +.rss-items a { + color: var(--p-text) !important; + text-decoration: none; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rss-items a:hover { color: var(--p-orange) !important; } + +.rss-items time { + font-family: var(--p-mono); + font-size: 0.58rem; + color: var(--p-muted); + flex-shrink: 0; +} + +.rss-loading { color: var(--p-muted); font-style: italic; } +.rss-error a { color: var(--p-orange) !important; } + +.status-dot-status-green, .text-green-500 { + box-shadow: 0 0 8px rgba(74, 222, 128, 0.55); +} + +footer, #footer { + opacity: 0.35; + font-family: var(--p-mono); + font-size: 0.62rem; +} + + + +/* ─── Logo gradients + profile photo ─── */ +:root { + --color-logo-start: 59, 130, 246; + --color-logo-stop: 251, 146, 60; +} + +.information-widget-logo img, +.information-widget-logo a img, +#information-widgets .information-widget-logo img { + width: 52px !important; + height: 52px !important; + min-width: 52px !important; + min-height: 52px !important; + border-radius: 50% !important; + object-fit: cover !important; + border: 2px solid rgba(251, 146, 60, 0.55) !important; + box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.35), 0 4px 16px rgba(0, 0, 0, 0.4) !important; + filter: none !important; + animation: none !important; +} + +.information-widget-logo:hover img { + border-color: rgba(34, 211, 238, 0.7) !important; +} + +.service-icon { + position: relative !important; + border-radius: 8px !important; + margin: 2px 4px 2px 2px !important; + background: rgba(12, 22, 38, 0.85) !important; + border: 1px solid rgba(59, 130, 246, 0.12) !important; + box-shadow: none !important; + animation: none !important; + overflow: hidden; +} + +.service-icon::before, +.service-icon::after { + display: none !important; + content: none !important; +} + +.service-icon > div { filter: none !important; transition: transform 0.2s ease; } +.service-icon img { filter: saturate(1.12) contrast(1.02) !important; transition: transform 0.2s ease; } + +.service-card:hover .service-icon { + border-color: rgba(251, 146, 60, 0.35) !important; + box-shadow: 0 0 0 1px rgba(251, 146, 60, 0.15) !important; +} + +.service-card:hover .service-icon > div, +.service-card:hover .service-icon img { transform: scale(1.06); } + +.services-list .service:nth-child(6n+1) .service-icon { border-left: 2px solid #3b82f6 !important; } +.services-list .service:nth-child(6n+2) .service-icon { border-left: 2px solid #a855f7 !important; } +.services-list .service:nth-child(6n+3) .service-icon { border-left: 2px solid #22c55e !important; } +.services-list .service:nth-child(6n+4) .service-icon { border-left: 2px solid #06b6d4 !important; } +.services-list .service:nth-child(6n+5) .service-icon { border-left: 2px solid #f59e0b !important; } +.services-list .service:nth-child(6n+6) .service-icon { border-left: 2px solid #ec4899 !important; } + +.bookmark-icon { font-weight: 600 !important; color: #fff !important; animation: none !important; box-shadow: none !important; } +.bookmark:nth-child(4n+1) .bookmark-icon { background: linear-gradient(135deg, #2563eb, #3b82f6) !important; } +.bookmark:nth-child(4n+2) .bookmark-icon { background: linear-gradient(135deg, #ea580c, #fb923c) !important; } +.bookmark:nth-child(4n+3) .bookmark-icon { background: linear-gradient(135deg, #7c3aed, #a855f7) !important; } +.bookmark:nth-child(4n+4) .bookmark-icon { background: linear-gradient(135deg, #059669, #22c55e) !important; } + +.information-widget-resource .resource-icon { animation: none !important; filter: none !important; } +.information-widget-resource:nth-child(1) .resource-icon { color: #60a5fa !important; } +.information-widget-resource:nth-child(2) .resource-icon { color: #fb923c !important; } +.information-widget-resource:nth-child(3) .resource-icon { color: #4ade80 !important; } + +.resource-usage > div { + background: linear-gradient(90deg, #3b82f6, #fb923c) !important; + animation: none !important; + background-size: 100% 100% !important; +} + +.status-dot-status-green, .text-green-500 { + background-color: #4ade80 !important; + animation: status-pulse-green 3s ease-out infinite; +} +.status-dot-status-red, .text-red-500 { + background-color: #f87171 !important; + animation: status-pulse-red 3s ease-out infinite; +} +@keyframes status-pulse-green { + 0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.5); } + 70% { box-shadow: 0 0 0 6px rgba(74, 222, 128, 0); } +} +@keyframes status-pulse-red { + 0% { box-shadow: 0 0 0 0 rgba(248, 113, 113, 0.5); } + 70% { box-shadow: 0 0 0 6px rgba(248, 113, 113, 0); } +} + +button[role="tab"][aria-selected="true"] { animation: none !important; } +.service-card { animation: none !important; } +.rss-hub { animation: none !important; } + +@media (prefers-reduced-motion: reduce) { + body::before, body::after, .rss-hub::before, .rss-pulse, .rss-hub-accent, + .service-icon, .service-icon::before, .service-icon::after, .service-card, + .bookmark-icon, .information-widget-resource .resource-icon, button[role="tab"][aria-selected="true"], + .rss-hub, #information-widgets img { animation: none !important; } +} diff --git a/apps/homepage/config/custom.js b/apps/homepage/config/custom.js new file mode 100644 index 0000000..ff7e24a --- /dev/null +++ b/apps/homepage/config/custom.js @@ -0,0 +1,143 @@ +/* EL-KADI OPS — live RSS panels (Palantir feed hub) */ +(function () { + const FEEDS = { + "Tech News": [ + { name: "Hacker News", url: "https://hnrss.org/frontpage" }, + { name: "Ars Technica", url: "https://feeds.arstechnica.com/arstechnica/index" }, + { name: "The Verge", url: "https://www.theverge.com/rss/index.xml" }, + { name: "Lobsters", url: "https://lobste.rs/rss" }, + ], + Security: [ + { name: "BleepingComputer", url: "https://www.bleepingcomputer.com/feed/" }, + { name: "Krebs", url: "https://krebsonsecurity.com/feed/" }, + { name: "The Hacker News", url: "https://feeds.feedburner.com/TheHackersNews" }, + ], + Homelab: [ + { name: "selfh.st", url: "https://selfh.st/rss/" }, + { name: "Proxmox Forum", url: "https://forum.proxmox.com/forums/-/index.rss" }, + { name: "r/selfhosted", url: "https://www.reddit.com/r/selfhosted/.rss" }, + { name: "Docker Blog", url: "https://www.docker.com/blog/feed/" }, + { name: "LinuxServer.io", url: "https://www.linuxserver.io/blog/rss/" }, + ], + "AI & Dev": [ + { name: "OpenAI Blog", url: "https://openai.com/blog/rss.xml" }, + { name: "Hugging Face", url: "https://huggingface.co/blog/feed.xml" }, + { name: "GitHub Blog", url: "https://github.blog/feed/" }, + ], + }; + + const PROXY = "https://api.allorigins.win/raw?url="; + const REFRESH_MS = 15 * 60 * 1000; + + function esc(s) { + const d = document.createElement("div"); + d.textContent = s || ""; + return d.innerHTML; + } + + function relTime(pub) { + if (!pub) return ""; + const t = new Date(pub).getTime(); + if (Number.isNaN(t)) return ""; + const m = Math.floor((Date.now() - t) / 60000); + if (m < 60) return `${m}m`; + const h = Math.floor(m / 60); + if (h < 48) return `${h}h`; + return `${Math.floor(h / 24)}d`; + } + + async function fetchFeed(feedUrl) { + const res = await fetch(PROXY + encodeURIComponent(feedUrl), { cache: "no-store" }); + if (!res.ok) throw new Error("fetch failed"); + const xml = new DOMParser().parseFromString(await res.text(), "text/xml"); + const items = xml.querySelectorAll("item, entry"); + return Array.from(items) + .slice(0, 6) + .map((item) => { + const title = + item.querySelector("title")?.textContent?.trim() || + item.querySelector("title")?.innerHTML?.trim() || + "—"; + const link = + item.querySelector("link")?.getAttribute("href") || + item.querySelector("link")?.textContent?.trim() || + item.querySelector("id")?.textContent?.trim() || + "#"; + const pub = + item.querySelector("pubDate")?.textContent || + item.querySelector("published")?.textContent || + item.querySelector("updated")?.textContent; + return { title, link, pub }; + }); + } + + function renderColumn(cat, feeds, root) { + const col = document.createElement("section"); + col.className = "rss-column"; + col.innerHTML = `
${esc(cat)}
`; + const list = col.querySelector(".rss-list"); + root.appendChild(col); + + feeds.forEach((feed) => { + const block = document.createElement("li"); + block.className = "rss-feed-block"; + block.innerHTML = `${esc(feed.name)}`; + list.appendChild(block); + const itemsUl = block.querySelector(".rss-items"); + + fetchFeed(feed.url) + .then((items) => { + itemsUl.innerHTML = items.length + ? items + .map( + (it) => + `
  • ${esc(it.title)}
  • ` + ) + .join("") + : '
  • Geen items
  • '; + }) + .catch(() => { + itemsUl.innerHTML = `
  • Open feed →
  • `; + }); + }); + } + + function injectHub() { + if (document.getElementById("elkadi-rss-hub")) return; + + const scan = document.createElement("div"); + scan.className = "palantir-scanline"; + const noise = document.createElement("div"); + noise.className = "palantir-noise"; + document.body.appendChild(scan); + document.body.appendChild(noise); + + const hub = document.createElement("div"); + hub.id = "elkadi-rss-hub"; + hub.className = "rss-hub"; + hub.innerHTML = + '
    INTEL FEEDSLive RSS · vernieuwt elke 15 min
    '; + const grid = hub.querySelector(".rss-grid"); + + Object.entries(FEEDS).forEach(([cat, feeds]) => renderColumn(cat, feeds, grid)); + + const services = document.getElementById("services"); + if (services && services.parentNode) { + services.parentNode.insertBefore(hub, services); + } else { + document.body.prepend(hub); + } + + setInterval(() => { + hub.querySelectorAll(".rss-column").forEach((c) => c.remove()); + const g = hub.querySelector(".rss-grid"); + Object.entries(FEEDS).forEach(([cat, feeds]) => renderColumn(cat, feeds, g)); + }, REFRESH_MS); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => setTimeout(injectHub, 400)); + } else { + setTimeout(injectHub, 400); + } +})(); diff --git a/apps/homepage/config/services.yaml b/apps/homepage/config/services.yaml new file mode 100644 index 0000000..bbb9b31 --- /dev/null +++ b/apps/homepage/config/services.yaml @@ -0,0 +1,505 @@ +- Infrastructure: + - Change detection: + icon: mdi-server-network-#3b82f6 + href: http://192.168.1.117:5000 + description: Change detection + siteMonitor: http://192.168.1.117:5000 + statusStyle: dot + + - Proxmox (3090): + icon: proxmox.png + href: https://192.168.1.216:8006 + description: Proxmox (3090) + siteMonitor: https://192.168.1.216:8006 + statusStyle: dot + + - Proxmox (Dell): + icon: proxmox.png + href: https://192.168.1.56:8006 + description: Proxmox (Dell) + siteMonitor: https://192.168.1.56:8006 + statusStyle: dot + + - Minarca: + icon: mdi-server-network-#3b82f6 + href: http://192.168.1.203:8080/login + description: Minarca + siteMonitor: http://192.168.1.203:8080 + statusStyle: dot + + - Proxmox: + icon: proxmox.png + href: http://192.168.1.230:8006 + description: Proxmox + siteMonitor: http://192.168.1.230:8006 + statusStyle: dot + + - Portainer (proxmox): + icon: portainer.png + href: https://192.168.5.128:9443/#!/home + description: Portainer (proxmox) + siteMonitor: https://192.168.5.128:9443 + statusStyle: dot + + - Rackula: + icon: mdi-count-#3b82f6 + href: https://count.racku.la + description: Rackula + + - TrueNAS: + icon: truenas.png + href: http://192.168.1.185 + description: TrueNAS + siteMonitor: http://192.168.1.185 + statusStyle: dot + + - Adguard: + icon: adguard-home.png + href: http://192.168.1.211:3001 + description: Adguard + siteMonitor: http://192.168.1.211:3001 + statusStyle: dot + + - Browserless: + icon: mdi-server-network-#3b82f6 + href: http://192.168.1.150:3000 + description: Browserless + siteMonitor: http://192.168.1.150:3000 + statusStyle: dot + + - Unifi: + icon: unifi.png + href: https://192.168.1.24 + description: Unifi + siteMonitor: https://192.168.1.24 + statusStyle: dot + + - Wazuh: + icon: wazuh.png + href: https://192.168.1.73 + description: Wazuh + siteMonitor: https://192.168.1.73 + statusStyle: dot + + - Modem: + icon: mdi-server-network-#3b82f6 + href: https://192.168.10.1 + description: Modem + siteMonitor: https://192.168.10.1 + statusStyle: dot + + - Nginx: + icon: nginx-proxy-manager.png + href: http://192.168.1.173:81 + description: Nginx + siteMonitor: http://192.168.1.173:81 + statusStyle: dot + + - Webmin: + icon: mdi-server-network-#3b82f6 + href: https://192.168.5.24:10000 + description: Webmin + siteMonitor: https://192.168.5.24:10000 + statusStyle: dot + + - PVE Scripts: + icon: mdi-server-network-#3b82f6 + href: http://192.168.1.25:3000 + description: PVE Scripts + siteMonitor: http://192.168.1.25:3000 + statusStyle: dot + + - iDrac: + icon: mdi-server-network-#3b82f6 + href: http://192.168.1.227 + description: iDrac + siteMonitor: http://192.168.1.227 + statusStyle: dot + + - Remotely: + icon: remotely.png + href: http://192.168.1.211:8080 + description: Remotely + siteMonitor: http://192.168.1.211:8080 + statusStyle: dot + + - mo-nas: + icon: mdi-server-network-#3b82f6 + href: http://192.168.1.150:5000 + description: mo-nas + siteMonitor: http://192.168.1.150:5000 + statusStyle: dot + + - NAS: + icon: mdi-server-network-#3b82f6 + href: http://192.168.1.211:5000 + description: NAS + siteMonitor: http://192.168.1.211:5000 + statusStyle: dot + + - Portainer: + icon: portainer.png + href: http://192.168.1.150:9000 + description: Portainer + siteMonitor: http://192.168.1.150:9000 + statusStyle: dot + + - Portainer: + icon: portainer.png + href: http://192.168.1.211:9000 + description: Portainer + siteMonitor: http://192.168.1.211:9000 + statusStyle: dot + +- Media & TV: + - nodecast: + icon: mdi-server-network-#a855f7 + href: http://192.168.1.107:3000/#home + description: nodecast + siteMonitor: http://192.168.1.107:3000 + statusStyle: dot + + - Frigate: + icon: frigate.png + href: https://192.168.1.185:30058 + description: Frigate + siteMonitor: https://192.168.1.185:30058 + statusStyle: dot + + - Tunarr: + icon: tunarr.png + href: http://192.168.1.237:8000 + description: Tunarr + siteMonitor: http://192.168.1.237:8000 + statusStyle: dot + + - Immich: + icon: immich.png + href: http://192.168.1.17:2283 + description: Immich + siteMonitor: http://192.168.1.17:2283 + statusStyle: dot + + - Metube: + icon: metube.png + href: http://192.168.1.140:8081 + description: Metube + siteMonitor: http://192.168.1.140:8081 + statusStyle: dot + +- Smart Home: + - Mawaqit: + icon: mdi-mawaqit-#22c55e + href: https://mawaqit.net/nl/el-kadi-amsterdam-1061dj-netherlands + description: Mawaqit + + - HA: + icon: mdi-server-network-#22c55e + href: http://192.168.1.235:8123 + description: HA + siteMonitor: http://192.168.1.235:8123 + statusStyle: dot + + - Ring: + icon: mdi-account-#22c55e + href: https://account.ring.com/account/dashboard?l=24b26010-2cac-48f2-b8ec-e1ef4b5fa79e + description: Ring + + - HA Voice Ctrl: + icon: home-assistant.png + href: http://192.168.1.211:8765 + description: HA Voice Ctrl + siteMonitor: http://192.168.1.211:8765 + statusStyle: dot + +- Productivity: + - Resume Builder: + icon: mdi-server-network-#06b6d4 + href: http://192.168.1.150:9751 + description: Resume Builder + siteMonitor: http://192.168.1.150:9751 + statusStyle: dot + + - openresume: + icon: mdi-server-network-#06b6d4 + href: http://192.168.1.150:6155 + description: openresume + siteMonitor: http://192.168.1.150:6155 + statusStyle: dot + + - nextcloud: + icon: nextcloud.png + href: https://cloud.el-kadi.nl/index.php/apps/dashboard + description: nextcloud + + - Invoice Generator: + icon: mdi-server-network-#06b6d4 + href: http://192.168.1.150:3099 + description: Invoice Generator + siteMonitor: http://192.168.1.150:3099 + statusStyle: dot + + - Vert: + icon: mdi-server-network-#06b6d4 + href: http://192.168.1.150:3884 + description: Vert + siteMonitor: http://192.168.1.150:3884 + statusStyle: dot + + - Vaultwarden: + icon: vaultwarden.png + href: https://192.168.1.6:8000/#/vault + description: Vaultwarden + siteMonitor: https://192.168.1.6:8000 + statusStyle: dot + + - Moocup: + icon: mdi-server-network-#06b6d4 + href: http://192.168.1.150:3591 + description: Moocup + siteMonitor: http://192.168.1.150:3591 + statusStyle: dot + + - n8n: + icon: n8n.png + href: http://192.168.1.64:5678/home/workflows + description: n8n + siteMonitor: http://192.168.1.64:5678 + statusStyle: dot + + - Linkwarden: + icon: linkwarden.png + href: http://192.168.1.150:7461/dashboard + description: Linkwarden + siteMonitor: http://192.168.1.150:7461 + statusStyle: dot + + - Thunderbird: + icon: mdi-server-network-#06b6d4 + href: http://192.168.1.150:5800 + description: Thunderbird + siteMonitor: http://192.168.1.150:5800 + statusStyle: dot + + - Excalidraw: + icon: excalidraw.png + href: http://192.168.1.211:3765 + description: Excalidraw + siteMonitor: http://192.168.1.211:3765 + statusStyle: dot + + - CRM: + icon: mdi-server-network-#06b6d4 + href: https://www.fixaanhuis.nl/crm + description: CRM + + - Gitea: + icon: gitea.png + href: http://192.168.1.211:3000 + description: Gitea + siteMonitor: http://192.168.1.211:3000 + statusStyle: dot + + - Neo4j Browser: + icon: neo4j.png + href: http://192.168.1.211:49154 + description: Neo4j Browser + siteMonitor: http://192.168.1.211:49154 + statusStyle: dot + + - OnlyOffice: + icon: mdi-server-network-#06b6d4 + href: http://192.168.5.128/example + description: OnlyOffice + siteMonitor: http://192.168.5.128 + statusStyle: dot + + - BentoPDF: + icon: mdi-server-network-#06b6d4 + href: http://192.168.1.150:3183 + description: BentoPDF + siteMonitor: http://192.168.1.150:3183 + statusStyle: dot + + - Jellyfin Test: + icon: mdi-server-network-#06b6d4 + href: http://192.168.1.99:8096 + description: Jellyfin Test + siteMonitor: http://192.168.1.99:8096 + statusStyle: dot + +- Tools & Utils: + - Traccar: + icon: traccar.png + href: http://192.168.1.150:8082 + description: Traccar + siteMonitor: http://192.168.1.150:8082 + statusStyle: dot + + - Mytkstar: + icon: mdi-server-network-#f59e0b + href: https://www.mytkstar.net/Monitor.aspx + description: Mytkstar + + - Firefox: + icon: mdi-server-network-#f59e0b + href: https://192.168.1.150:5813 + description: Firefox + siteMonitor: https://192.168.1.150:5813 + statusStyle: dot + + - Background Remover: + icon: mdi-server-network-#f59e0b + href: http://192.168.1.150:8519 + description: Background Remover + siteMonitor: http://192.168.1.150:8519 + statusStyle: dot + + - Neko: + icon: mdi-server-network-#f59e0b + href: http://192.168.1.150:8080 + description: Neko + siteMonitor: http://192.168.1.150:8080 + statusStyle: dot + +- AI Assistants: + - Phind: + icon: mdi-server-network-#ec4899 + href: https://www.phind.com/search?home=true + description: Phind + + - Deepseek: + icon: si-deepseek-#ec4899 + href: https://chat.deepseek.com + description: Deepseek + + - Anthropic: + icon: si-anthropic-#ec4899 + href: https://console.anthropic.com/dashboard + description: Anthropic + + - Github: + icon: si-github-#ec4899 + href: https://github.com/modammer020 + description: Github + + - Lbrty: + icon: mdi-lbrty-#ec4899 + href: https://lbrty.ai + description: Lbrty + + - Grok: + icon: si-x-#ec4899 + href: https://grok.com + description: Grok + + - LMarena: + icon: mdi-lmarena-#ec4899 + href: https://lmarena.ai + description: LMarena + + - Google AI studio: + icon: mdi-aistudio-#ec4899 + href: https://aistudio.google.com/prompts/new_chat + description: Google AI studio + + - GPT4FREE: + icon: mdi-server-network-#ec4899 + href: http://192.168.1.150:6439 + description: GPT4FREE + siteMonitor: http://192.168.1.150:6439 + statusStyle: dot + + - ChatGPT: + icon: si-openai-#ec4899 + href: https://chatgpt.com + description: ChatGPT + + - Youtube: + icon: si-youtube-#ec4899 + href: https://youtube.com + description: Youtube + +- Dev & Docs: + - DSM on Proxmox: + icon: proxmox.png + href: https://blog.nootch.net + description: DSM on Proxmox + + - DSM Video station 7.2: + icon: mdi-github-#14b8a6 + href: https://github.com/007revad/Video_Station_for_DSM_722?tab=readme-ov-file + description: DSM Video station 7.2 + + - Turnkey: + icon: mdi-server-network-#14b8a6 + href: https://www.turnkeylinux.org + description: Turnkey + + - LXC: + icon: mdi-community-sc-#14b8a6 + href: https://community-scripts.github.io/ProxmoxVE + description: LXC + + - DSM auxxxilium: + icon: mdi-arc-#14b8a6 + href: https://arc.auxxxilium.tech + description: DSM auxxxilium + + - Synology stuff (Marius Hosting): + icon: mdi-mariushostin-#14b8a6 + href: https://mariushosting.com + description: Synology stuff (Marius Hosting) + + - NZB Geek: + icon: mdi-nzbgeek-#14b8a6 + href: https://nzbgeek.info/dashboard.php + description: NZB Geek + + - Noted Apps: + icon: mdi-noted-#14b8a6 + href: https://noted.lol + description: Noted Apps + + - https://jack-vanlightly.com/: + icon: mdi-jack-vanligh-#14b8a6 + href: https://jack-vanlightly.com + description: https://jack-vanlightly.com/ + + - Usenetdeal: + icon: mdi-usenetdeal-#14b8a6 + href: https://usenetdeal.com/dashboard/usenet + description: Usenetdeal + + - https://juhache.substack.com/: + icon: mdi-juhache-#14b8a6 + href: https://juhache.substack.com + description: https://juhache.substack.com/ + + - Home Control: + icon: mdi-server-network-#14b8a6 + href: http://192.168.1.211:8765/dashboard#live + description: Home Control + siteMonitor: http://192.168.1.211:8765 + statusStyle: dot + +- Web Design: + - HTML5 templates: + icon: mdi-jekyllthemes-#f97316 + href: https://jekyllthemes.io/free + description: HTML5 templates + + - HTML5 templates: + icon: mdi-jekyllrb-#f97316 + href: https://jekyllrb.com/showcase + description: HTML5 templates + + - HTML5 templates: + icon: mdi-freehtml5-#f97316 + href: https://freehtml5.co + description: HTML5 templates + + - HTML5: + icon: mdi-server-network-#f97316 + href: https://www.tooplate.com/free-templates + description: HTML5 diff --git a/apps/homepage/config/settings.yaml b/apps/homepage/config/settings.yaml new file mode 100644 index 0000000..8076477 --- /dev/null +++ b/apps/homepage/config/settings.yaml @@ -0,0 +1,77 @@ +--- +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: + 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 diff --git a/apps/homepage/config/widgets.yaml b/apps/homepage/config/widgets.yaml new file mode 100644 index 0000000..1943374 --- /dev/null +++ b/apps/homepage/config/widgets.yaml @@ -0,0 +1,27 @@ +--- +- 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 diff --git a/apps/homepage/custom.css b/apps/homepage/custom.css new file mode 100644 index 0000000..c217be0 --- /dev/null +++ b/apps/homepage/custom.css @@ -0,0 +1,431 @@ +/* EL-KADI OPS — Palantir blue + orange */ +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@400;500;600&display=swap'); + +:root { + --p-void: #02060c; + --p-deep: #051018; + --p-surface: #0a1424; + --p-elevated: rgba(10, 22, 40, 0.92); + --p-border: rgba(56, 132, 220, 0.18); + --p-border-hot: rgba(251, 146, 60, 0.45); + --p-blue: #3b82f6; + --p-cyan: #22d3ee; + --p-orange: #fb923c; + --p-amber: #fbbf24; + --p-teal: #2dd4bf; + --p-text: #e8eef7; + --p-muted: #64748b; + --p-mono: 'JetBrains Mono', ui-monospace, monospace; + --p-sans: 'DM Sans', system-ui, sans-serif; +} + +html { color-scheme: dark; } + +body { + font-family: var(--p-sans) !important; + background: var(--p-void) !important; + color: var(--p-text) !important; +} + +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(ellipse 100% 70% at 8% -15%, rgba(59, 130, 246, 0.22), transparent 52%), + radial-gradient(ellipse 80% 55% at 95% 5%, rgba(34, 211, 238, 0.12), transparent 48%), + radial-gradient(ellipse 60% 50% at 88% 75%, rgba(251, 146, 60, 0.14), transparent 45%), + radial-gradient(ellipse 50% 40% at 12% 85%, rgba(251, 146, 60, 0.08), transparent 40%), + linear-gradient(165deg, #061018 0%, #02060c 45%, #030810 100%); + pointer-events: none; + z-index: 0; + animation: bg-breathe 14s ease-in-out infinite alternate; +} + +@keyframes bg-breathe { + 0% { opacity: 1; } + 100% { opacity: 0.9; } +} + +body::after { + content: ''; + position: fixed; + inset: 0; + background-image: + linear-gradient(rgba(59, 130, 246, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(59, 130, 246, 0.04) 1px, transparent 1px); + background-size: 56px 56px; + mask-image: radial-gradient(ellipse 90% 80% at 50% 30%, black 10%, transparent 72%); + pointer-events: none; + z-index: 0; + animation: grid-drift 40s linear infinite; +} + +@keyframes grid-drift { + from { transform: translate(0, 0); } + to { transform: translate(-56px, -56px); } +} + +.palantir-scanline { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 1; + background: repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(0,0,0,0.04) 3px, rgba(0,0,0,0.04) 6px); + opacity: 0.35; +} + +.palantir-noise { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 1; + opacity: 0.03; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); +} + +#page_wrapper { position: relative; z-index: 2; } + +#information-widget-greeting, .information-widget-greeting { + font-weight: 600 !important; + letter-spacing: 0.22em !important; + text-transform: uppercase !important; + font-size: 0.7rem !important; + color: var(--p-orange) !important; + font-family: var(--p-mono) !important; +} + +.information-widget { + border: 1px solid var(--p-border) !important; + border-radius: 10px !important; + background: var(--p-elevated) !important; + backdrop-filter: blur(14px); + box-shadow: 0 4px 24px rgba(0,0,0,0.35), inset 0 1px 0 rgba(59,130,246,0.08); +} + +button[role="tab"] { + font-family: var(--p-mono) !important; + font-size: 0.68rem !important; + letter-spacing: 0.14em !important; + text-transform: uppercase !important; + border: 1px solid var(--p-border) !important; + border-radius: 6px !important; + transition: all 0.25s ease; +} + +button[role="tab"][aria-selected="true"] { + background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(251,146,60,0.12)) !important; + border-color: var(--p-border-hot) !important; + color: var(--p-cyan) !important; + box-shadow: 0 0 20px rgba(251,146,60,0.15); +} + +.service-block, #services .service-group .service { + border: 1px solid var(--p-border) !important; + border-radius: 10px !important; + background: var(--p-elevated) !important; + backdrop-filter: blur(12px); + transition: border-color 0.2s, box-shadow 0.25s, transform 0.2s; +} + +.service-block:hover, #services .service-group .service:hover { + border-color: rgba(251,146,60,0.5) !important; + box-shadow: 0 0 28px rgba(59,130,246,0.12), 0 0 12px rgba(251,146,60,0.1); + transform: translateY(-1px); +} + +#services h2.service-group-name, .service-group-title { + font-family: var(--p-mono) !important; + font-size: 0.62rem !important; + letter-spacing: 0.2em !important; + text-transform: uppercase !important; + color: var(--p-cyan) !important; +} + +#bookmarks .bookmark-group h2, .bookmark-group-name { + font-family: var(--p-mono) !important; + font-size: 0.62rem !important; + letter-spacing: 0.18em !important; + text-transform: uppercase !important; + color: var(--p-orange) !important; +} + +#bookmarks a { + border: 1px solid var(--p-border) !important; + transition: border-color 0.2s, box-shadow 0.2s; +} + +#bookmarks a:hover { + border-color: rgba(251,146,60,0.4) !important; + box-shadow: 0 0 16px rgba(59,130,246,0.1); +} + +.rss-hub { + margin: 1rem 1.5rem 1.5rem; + padding: 1.25rem 1.5rem; + border: 1px solid var(--p-border); + border-radius: 14px; + background: linear-gradient(145deg, rgba(10,22,40,0.95), rgba(6,12,22,0.98)); + box-shadow: 0 8px 40px rgba(0,0,0,0.45), inset 0 1px 0 rgba(59,130,246,0.12); + backdrop-filter: blur(16px); + position: relative; + overflow: hidden; +} + +.rss-hub::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--p-blue), var(--p-orange), var(--p-cyan), transparent); + animation: rss-scan 4s ease-in-out infinite; +} + +@keyframes rss-scan { + 0%, 100% { opacity: 0.5; transform: scaleX(0.6); } + 50% { opacity: 1; transform: scaleX(1); } +} + +.rss-hub-title { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 0.5rem 1rem; + margin-bottom: 1.25rem; + font-family: var(--p-mono); + font-size: 0.72rem; + letter-spacing: 0.28em; + text-transform: uppercase; + color: var(--p-cyan); +} + +.rss-hub-accent { + display: inline-block; + width: 8px; height: 8px; + border-radius: 50%; + background: var(--p-orange); + box-shadow: 0 0 12px var(--p-orange); + animation: pulse-dot 2s ease-in-out infinite; +} + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(0.85); } +} + +.rss-hub-sub { + font-size: 0.58rem; + letter-spacing: 0.1em; + color: var(--p-muted); + text-transform: none; +} + +.rss-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; +} + +@media (max-width: 1280px) { .rss-grid { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 640px) { .rss-grid { grid-template-columns: 1fr; } } + +.rss-column { + border: 1px solid rgba(59,130,246,0.12); + border-radius: 10px; + padding: 0.75rem; + background: rgba(5,12,24,0.6); + min-height: 160px; +} + +.rss-column-head { + display: flex; + align-items: center; + gap: 0.5rem; + font-family: var(--p-mono); + font-size: 0.58rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--p-orange); + margin-bottom: 0.65rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--p-border); +} + +.rss-pulse { + width: 6px; height: 6px; + border-radius: 50%; + background: var(--p-cyan); + animation: pulse-dot 1.8s ease-in-out infinite; +} + +.rss-feed-block { list-style: none; margin-bottom: 0.85rem; } + +.rss-feed-name { + display: block; + font-family: var(--p-mono); + font-size: 0.55rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--p-muted); + margin-bottom: 0.35rem; +} + +.rss-items { list-style: none; padding: 0; margin: 0; } + +.rss-items li { + display: flex; + justify-content: space-between; + gap: 0.5rem; + padding: 0.28rem 0; + border-bottom: 1px solid rgba(255,255,255,0.04); + font-size: 0.72rem; + line-height: 1.35; + animation: rss-fade-in 0.4s ease forwards; + opacity: 0; +} + +.rss-items li:nth-child(1) { animation-delay: 0.05s; } +.rss-items li:nth-child(2) { animation-delay: 0.1s; } +.rss-items li:nth-child(3) { animation-delay: 0.15s; } +.rss-items li:nth-child(4) { animation-delay: 0.2s; } +.rss-items li:nth-child(5) { animation-delay: 0.25s; } + +@keyframes rss-fade-in { to { opacity: 1; } } + +.rss-items a { + color: var(--p-text) !important; + text-decoration: none; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rss-items a:hover { color: var(--p-orange) !important; } + +.rss-items time { + font-family: var(--p-mono); + font-size: 0.58rem; + color: var(--p-muted); + flex-shrink: 0; +} + +.rss-loading { color: var(--p-muted); font-style: italic; } +.rss-error a { color: var(--p-orange) !important; } + +.status-dot-status-green, .text-green-500 { + box-shadow: 0 0 8px rgba(74, 222, 128, 0.55); +} + +footer, #footer { + opacity: 0.35; + font-family: var(--p-mono); + font-size: 0.62rem; +} + + + +/* ─── Logo gradients + profile photo ─── */ +:root { + --color-logo-start: 59, 130, 246; + --color-logo-stop: 251, 146, 60; +} + +.information-widget-logo img, +.information-widget-logo a img, +#information-widgets .information-widget-logo img { + width: 52px !important; + height: 52px !important; + min-width: 52px !important; + min-height: 52px !important; + border-radius: 50% !important; + object-fit: cover !important; + border: 2px solid rgba(251, 146, 60, 0.55) !important; + box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.35), 0 4px 16px rgba(0, 0, 0, 0.4) !important; + filter: none !important; + animation: none !important; +} + +.information-widget-logo:hover img { + border-color: rgba(34, 211, 238, 0.7) !important; +} + +.service-icon { + position: relative !important; + border-radius: 8px !important; + margin: 2px 4px 2px 2px !important; + background: rgba(12, 22, 38, 0.85) !important; + border: 1px solid rgba(59, 130, 246, 0.12) !important; + box-shadow: none !important; + animation: none !important; + overflow: hidden; +} + +.service-icon::before, +.service-icon::after { + display: none !important; + content: none !important; +} + +.service-icon > div { filter: none !important; transition: transform 0.2s ease; } +.service-icon img { filter: saturate(1.12) contrast(1.02) !important; transition: transform 0.2s ease; } + +.service-card:hover .service-icon { + border-color: rgba(251, 146, 60, 0.35) !important; + box-shadow: 0 0 0 1px rgba(251, 146, 60, 0.15) !important; +} + +.service-card:hover .service-icon > div, +.service-card:hover .service-icon img { transform: scale(1.06); } + +.services-list .service:nth-child(6n+1) .service-icon { border-left: 2px solid #3b82f6 !important; } +.services-list .service:nth-child(6n+2) .service-icon { border-left: 2px solid #a855f7 !important; } +.services-list .service:nth-child(6n+3) .service-icon { border-left: 2px solid #22c55e !important; } +.services-list .service:nth-child(6n+4) .service-icon { border-left: 2px solid #06b6d4 !important; } +.services-list .service:nth-child(6n+5) .service-icon { border-left: 2px solid #f59e0b !important; } +.services-list .service:nth-child(6n+6) .service-icon { border-left: 2px solid #ec4899 !important; } + +.bookmark-icon { font-weight: 600 !important; color: #fff !important; animation: none !important; box-shadow: none !important; } +.bookmark:nth-child(4n+1) .bookmark-icon { background: linear-gradient(135deg, #2563eb, #3b82f6) !important; } +.bookmark:nth-child(4n+2) .bookmark-icon { background: linear-gradient(135deg, #ea580c, #fb923c) !important; } +.bookmark:nth-child(4n+3) .bookmark-icon { background: linear-gradient(135deg, #7c3aed, #a855f7) !important; } +.bookmark:nth-child(4n+4) .bookmark-icon { background: linear-gradient(135deg, #059669, #22c55e) !important; } + +.information-widget-resource .resource-icon { animation: none !important; filter: none !important; } +.information-widget-resource:nth-child(1) .resource-icon { color: #60a5fa !important; } +.information-widget-resource:nth-child(2) .resource-icon { color: #fb923c !important; } +.information-widget-resource:nth-child(3) .resource-icon { color: #4ade80 !important; } + +.resource-usage > div { + background: linear-gradient(90deg, #3b82f6, #fb923c) !important; + animation: none !important; + background-size: 100% 100% !important; +} + +.status-dot-status-green, .text-green-500 { + background-color: #4ade80 !important; + animation: status-pulse-green 3s ease-out infinite; +} +.status-dot-status-red, .text-red-500 { + background-color: #f87171 !important; + animation: status-pulse-red 3s ease-out infinite; +} +@keyframes status-pulse-green { + 0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.5); } + 70% { box-shadow: 0 0 0 6px rgba(74, 222, 128, 0); } +} +@keyframes status-pulse-red { + 0% { box-shadow: 0 0 0 0 rgba(248, 113, 113, 0.5); } + 70% { box-shadow: 0 0 0 6px rgba(248, 113, 113, 0); } +} + +button[role="tab"][aria-selected="true"] { animation: none !important; } +.service-card { animation: none !important; } +.rss-hub { animation: none !important; } + +@media (prefers-reduced-motion: reduce) { + body::before, body::after, .rss-hub::before, .rss-pulse, .rss-hub-accent, + .service-icon, .service-icon::before, .service-icon::after, .service-card, + .bookmark-icon, .information-widget-resource .resource-icon, button[role="tab"][aria-selected="true"], + .rss-hub, #information-widgets img { animation: none !important; } +} diff --git a/apps/homepage/deploy-to-pve.sh b/apps/homepage/deploy-to-pve.sh new file mode 100644 index 0000000..629c5f6 --- /dev/null +++ b/apps/homepage/deploy-to-pve.sh @@ -0,0 +1,64 @@ +#!/bin/sh +# Deploy Homepage LXC (CT 120) on pve 192.168.1.216 +set -e +PW="${PROXMOX_PASSWORD:-WaQTUw2t}" +PVE=192.168.1.216 +VMID=120 +APP="${HOMEPAGE_DIR:-/volume1/docker/homelab-configs/apps/homepage}" +STORAGE="${PVE_STORAGE:-Storage}" + +ssh_pve() { + docker run --rm -v "$APP:/src:ro" alpine sh -c " + apk add --no-cache openssh-client sshpass rsync >/dev/null 2>&1 + sshpass -p '$PW' ssh -o StrictHostKeyChecking=no root@$PVE \"$1\" + " +} + +echo "=== Proxmox: LXC $VMID homepage ===" +if ssh_pve "pct status $VMID" 2>/dev/null | grep -q running; then + echo "CT $VMID bestaat al en draait" +else + if ssh_pve "pct status $VMID" 2>/dev/null | grep -q stopped; then + echo "Start bestaande CT $VMID" + ssh_pve "pct start $VMID" + else + echo "Maak CT $VMID aan..." + ssh_pve "pct create $VMID local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \ + --arch amd64 --cores 2 --memory 2048 --swap 512 \ + --hostname homepage --password '$PW' \ + --rootfs ${STORAGE}:8 \ + --net0 name=eth0,bridge=vmbr0,ip=dhcp,type=veth \ + --features nesting=1,keyctl=1 --unprivileged 1 --onboot 1" + ssh_pve "pct start $VMID" + fi +fi + +echo "Wacht op netwerk..." +sleep 8 + +echo "=== Docker installeren ===" +ssh_pve "pct exec $VMID -- bash -c 'export DEBIAN_FRONTEND=noninteractive; \ + apt-get update -qq && apt-get install -y -qq docker.io docker-compose curl ca-certificates 2>/dev/null; \ + systemctl enable --now docker'" + +echo "=== Config genereren ===" +python3 "$APP/generate-config.py" 2>/dev/null || true + +echo "=== Config uploaden ===" +# tar config and push +tar -C "$APP" -czf /tmp/homepage-deploy.tgz docker-compose.yml config public websites-add.txt +docker run --rm -v /tmp:/tmp alpine sh -c " + apk add --no-cache openssh-client sshpass >/dev/null 2>&1 + sshpass -p '$PW' scp -o StrictHostKeyChecking=no /tmp/homepage-deploy.tgz root@$PVE:/tmp/ +" +ssh_pve "pct exec $VMID -- mkdir -p /opt/homepage && pct push $VMID /tmp/homepage-deploy.tgz /tmp/homepage-deploy.tgz" +ssh_pve "pct exec $VMID -- bash -c 'cd /opt/homepage && tar xzf /tmp/homepage-deploy.tgz && rm /tmp/homepage-deploy.tgz'" + +echo "=== Homepage starten ===" +ssh_pve "pct exec $VMID -- bash -c 'cd /opt/homepage && docker compose pull && docker compose up -d'" + +echo "=== IP ophalen ===" +IP=$(ssh_pve "pct exec $VMID -- hostname -I" | awk '{print $1}') +echo "" +echo "Klaar: http://${IP:-192.168.1.216}:3000" +echo "Of via pve-host als poort 3000 op CT staat: check Proxmox → CT $VMID" diff --git a/apps/homepage/docker-compose.admin.yml b/apps/homepage/docker-compose.admin.yml new file mode 100644 index 0000000..b622f8c --- /dev/null +++ b/apps/homepage/docker-compose.admin.yml @@ -0,0 +1,17 @@ +# Homepage Admin UI — draait op de NAS (grafisch sites toevoegen) +# Start: docker compose -f docker-compose.admin.yml up -d --build +services: + homepage-admin: + build: ./admin + container_name: homepage-admin + restart: unless-stopped + ports: + - "3010:3010" + environment: + PROXMOX_PASSWORD: ${PROXMOX_PASSWORD:-WaQTUw2t} + HOMEPAGE_DIR: /apps/homepage + volumes: + - /volume1/docker/homelab-configs/apps:/apps:rw + - /var/run/docker.sock:/var/run/docker.sock + working_dir: /apps/homepage/admin + command: python server.py diff --git a/apps/homepage/docker-compose.yml b/apps/homepage/docker-compose.yml new file mode 100644 index 0000000..7fea8ab --- /dev/null +++ b/apps/homepage/docker-compose.yml @@ -0,0 +1,18 @@ +services: + homepage: + image: ghcr.io/gethomepage/homepage:latest + container_name: homepage + restart: unless-stopped + ports: + - "3000:3000" + environment: + # Homelab LAN: * = geen host-check (DHCP/NPM hostnames wisselen) + HOMEPAGE_ALLOWED_HOSTS: "*" + PUID: 0 + PGID: 0 + volumes: + - ./config:/app/config + - ./config/custom.css:/app/config/custom.css:ro + - ./config/custom.js:/app/config/custom.js:ro + - ./public/images:/app/public/images:ro + - /var/run/docker.sock:/var/run/docker.sock:ro diff --git a/apps/homepage/generate-config.py b/apps/homepage/generate-config.py new file mode 100644 index 0000000..71ee941 --- /dev/null +++ b/apps/homepage/generate-config.py @@ -0,0 +1,372 @@ +#!/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() diff --git a/apps/homepage/public/images/README.md b/apps/homepage/public/images/README.md new file mode 100644 index 0000000..eac0863 --- /dev/null +++ b/apps/homepage/public/images/README.md @@ -0,0 +1,18 @@ +# Profielfoto (logo) + +Plaats je foto hier als **`logo.jpg`** (vierkant werkt het best, min. 128×128 px). + +```bash +cp /pad/naar/jouw-foto.jpg logo.jpg +cd /volume1/docker/homelab-configs/apps/homepage +./deploy-to-pve.sh +``` + +Of gebruik het script: + +```bash +./set-logo.sh /pad/naar/foto.jpg +./deploy-to-pve.sh +``` + +Daarna op Homepage: refresh-icoon rechtsonder, of Ctrl+Shift+R. diff --git a/apps/homepage/public/images/logo.jpg b/apps/homepage/public/images/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b80d7efd01f96bec25d1bc305de53a0efc02b53c GIT binary patch literal 10131 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/dev/null 2>&1; then + docker compose -f docker-compose.admin.yml up -d --build +else + docker-compose -f docker-compose.admin.yml up -d --build +fi +echo "" +echo "Admin UI: http://192.168.1.211:3010" +echo "Dashboard: http://192.168.1.192:3000" diff --git a/apps/homepage/websites-add.txt b/apps/homepage/websites-add.txt new file mode 100644 index 0000000..3dfc93a --- /dev/null +++ b/apps/homepage/websites-add.txt @@ -0,0 +1,7 @@ +# Extra websites — één per regel: groep|naam|url +# Voorbeeld: +# Productivity|Jellyfin|http://192.168.1.10:8096 +# AI Assistants|Claude|https://claude.ai +# +# Of gebruik: ./add-website.sh +Productivity|Jellyfin Test|http://192.168.1.99:8096 diff --git a/apps/homepage/websites.yaml b/apps/homepage/websites.yaml new file mode 100644 index 0000000..e9924c0 --- /dev/null +++ b/apps/homepage/websites.yaml @@ -0,0 +1,2 @@ +# (optioneel) Zie websites-add.txt — dat is het makkelijkste bestand om te bewerken. +# Gebruik: ./add-website.sh of bewerk websites-add.txt diff --git a/apps/proxmox/hosts/pve/lxc/120.conf b/apps/proxmox/hosts/pve/lxc/120.conf new file mode 100644 index 0000000..a5c4336 --- /dev/null +++ b/apps/proxmox/hosts/pve/lxc/120.conf @@ -0,0 +1,11 @@ +arch: amd64 +cores: 2 +features: nesting=1,keyctl=1 +hostname: homepage +memory: 2048 +net0: name=eth0,bridge=vmbr0,hwaddr=BC:24:11:D3:74:DD,ip=dhcp,type=veth +onboot: 1 +ostype: debian +rootfs: Storage:vm-120-disk-0,size=8G +swap: 512 +unprivileged: 1 diff --git a/apps/proxmox/lxc-inventory.md b/apps/proxmox/lxc-inventory.md index c82d32d..54a88c3 100644 --- a/apps/proxmox/lxc-inventory.md +++ b/apps/proxmox/lxc-inventory.md @@ -33,6 +33,7 @@ Configs: `hosts//lxc/*.conf` · App-configs: `apps//` | 117 | Proxy | 192.168.1.165 | [proxy](../proxy/) | **running** | | 118 | paymenter | 192.168.1.45 | [paymenter](../paymenter/) | **running** | | 119 | nodecast-tv | 192.168.1.99 | [nodecast-tv](../nodecast-tv/) | **running** | +| 120 | homepage | 192.168.1.192 | [homepage](../homepage/) | **running** | ## Host: dell-proxmox (192.168.1.56)