43c4ed7a6d
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>
144 lines
5.4 KiB
JavaScript
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);
|
|
}
|
|
})();
|