Files
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

144 lines
5.4 KiB
JavaScript

/* 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 = `<header class="rss-column-head"><span class="rss-pulse"></span>${esc(cat)}</header><ul class="rss-list"></ul>`;
const list = col.querySelector(".rss-list");
root.appendChild(col);
feeds.forEach((feed) => {
const block = document.createElement("li");
block.className = "rss-feed-block";
block.innerHTML = `<span class="rss-feed-name">${esc(feed.name)}</span><ul class="rss-items"><li class="rss-loading">Laden…</li></ul>`;
list.appendChild(block);
const itemsUl = block.querySelector(".rss-items");
fetchFeed(feed.url)
.then((items) => {
itemsUl.innerHTML = items.length
? items
.map(
(it) =>
`<li><a href="${esc(it.link)}" target="_blank" rel="noreferrer">${esc(it.title)}</a><time>${relTime(it.pub)}</time></li>`
)
.join("")
: '<li class="rss-empty">Geen items</li>';
})
.catch(() => {
itemsUl.innerHTML = `<li class="rss-error"><a href="${esc(feed.url)}" target="_blank" rel="noreferrer">Open feed →</a></li>`;
});
});
}
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 =
'<div class="rss-hub-title"><span class="rss-hub-accent"></span>INTEL FEEDS<span class="rss-hub-sub">Live RSS · vernieuwt elke 15 min</span></div><div class="rss-grid"></div>';
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);
}
})();