stack/landing/site/index.html
tim 60f8a89c87 Landing page: always use absolute http:// service URLs
Show full http://host:port links on every card, fall back from localhost
to the LAN IP, and warn about HTTPS-only browsers breaking plain HTTP.
2026-07-22 19:15:30 -07:00

513 lines
17 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Homelab · Resource map</title>
<meta name="color-scheme" content="dark" />
<!-- These services are plain HTTP on the LAN (no TLS certs). Do not use https:// -->
<style>
:root {
--bg: #0c0e12;
--bg-elev: #141820;
--bg-card: #1a1f2a;
--bg-card-hover: #222936;
--border: #2a3344;
--text: #e8ecf4;
--muted: #8b95a8;
--dim: #5c667a;
--accent: #6ea8fe;
--accent-soft: rgba(110, 168, 254, 0.12);
--warn: #f0b429;
--media: #7dd3a0;
--download: #f0b429;
--ai: #c4a1ff;
--code: #5eead4;
--radius: 14px;
--font: "Segoe UI", system-ui, -apple-system, sans-serif;
--mono: ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
background-image:
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(110, 168, 254, 0.12), transparent),
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(196, 161, 255, 0.06), transparent);
}
.wrap {
max-width: 1100px;
margin: 0 auto;
padding: 2rem 1.25rem 4rem;
}
header {
margin-bottom: 2rem;
border-bottom: 1px solid var(--border);
padding-bottom: 1.5rem;
}
header h1 {
font-size: 1.65rem;
font-weight: 650;
letter-spacing: -0.02em;
}
header p {
color: var(--muted);
margin-top: 0.4rem;
max-width: 42rem;
}
.host-pill {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.85rem;
padding: 0.35rem 0.75rem;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: 999px;
font-family: var(--mono);
font-size: 0.85rem;
color: var(--accent);
}
.host-pill span { color: var(--dim); }
.http-note {
margin-top: 0.85rem;
padding: 0.65rem 0.85rem;
border-radius: 10px;
border: 1px solid rgba(240, 180, 41, 0.35);
background: rgba(240, 180, 41, 0.08);
color: var(--warn);
font-size: 0.88rem;
max-width: 44rem;
}
.http-note code {
font-family: var(--mono);
font-size: 0.85em;
color: var(--text);
}
.map {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
.lane {
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: calc(var(--radius) + 4px);
padding: 1.15rem 1.15rem 1.25rem;
}
.lane-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.9rem;
padding-bottom: 0.65rem;
border-bottom: 1px solid var(--border);
}
.lane-head h2 {
font-size: 0.95rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.lane-head h2 .dot {
display: inline-block;
width: 0.55rem;
height: 0.55rem;
border-radius: 50%;
margin-right: 0.45rem;
vertical-align: middle;
}
.lane.media h2 .dot { background: var(--media); box-shadow: 0 0 10px var(--media); }
.lane.download h2 .dot { background: var(--download); box-shadow: 0 0 10px var(--download); }
.lane.ai h2 .dot { background: var(--ai); box-shadow: 0 0 10px var(--ai); }
.lane.code h2 .dot { background: var(--code); box-shadow: 0 0 10px var(--code); }
.lane-head .hint {
font-size: 0.8rem;
color: var(--dim);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 0.75rem;
}
a.card {
display: block;
text-decoration: none;
color: inherit;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem 1.05rem;
transition: background 0.15s, border-color 0.15s, transform 0.15s;
}
a.card:hover {
background: var(--bg-card-hover);
border-color: var(--accent);
transform: translateY(-1px);
}
a.card:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.card-title {
font-weight: 600;
font-size: 1.02rem;
margin-bottom: 0.2rem;
}
.card-what {
font-size: 0.88rem;
color: var(--muted);
margin-bottom: 0.55rem;
}
.card-url {
font-family: var(--mono);
font-size: 0.72rem;
color: var(--accent);
background: var(--accent-soft);
padding: 0.3rem 0.45rem;
border-radius: 6px;
display: block;
word-break: break-all;
line-height: 1.35;
}
.card-howto {
margin-top: 0.65rem;
padding-top: 0.55rem;
border-top: 1px dashed var(--border);
font-size: 0.8rem;
color: var(--dim);
}
section.guide {
margin-top: 2.5rem;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: calc(var(--radius) + 4px);
padding: 1.35rem 1.4rem 1.5rem;
}
section.guide h2 {
font-size: 1.15rem;
margin-bottom: 0.75rem;
}
section.guide ol {
padding-left: 1.25rem;
color: var(--muted);
font-size: 0.95rem;
}
section.guide li { margin-bottom: 0.55rem; }
section.guide li strong { color: var(--text); }
section.guide code {
font-family: var(--mono);
font-size: 0.85em;
background: var(--bg-card);
padding: 0.1rem 0.35rem;
border-radius: 4px;
color: var(--accent);
}
footer {
margin-top: 2rem;
text-align: center;
font-size: 0.8rem;
color: var(--dim);
}
noscript .grid { margin-top: 0.75rem; }
@media (max-width: 560px) {
.lane-head { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Homelab resource map</h1>
<p>
Everything below is available to PCs on this home network.
Click a card to open it. New here? Read the short guide at the bottom.
</p>
<div class="host-pill"><span>server</span> <strong id="host">192.168.8.123</strong></div>
<p class="http-note">
All service links use plain <code>http://</code> (no HTTPS certificates on the LAN).
If a link fails, check the browser is not forcing HTTPS / “HTTPS-Only mode” for local IPs.
</p>
</header>
<div class="map" id="map">
<!-- Static fallback (no JS): explicit http:// URLs -->
<noscript>
<section class="lane media">
<div class="lane-head"><h2><span class="dot"></span>Watch &amp; enjoy</h2></div>
<div class="grid">
<a class="card" href="http://192.168.8.123:8096/"><div class="card-title">Jellyfin</div><span class="card-url">http://192.168.8.123:8096/</span></a>
</div>
</section>
<section class="lane download">
<div class="lane-head"><h2><span class="dot"></span>Find &amp; download</h2></div>
<div class="grid">
<a class="card" href="http://192.168.8.123:8989/"><div class="card-title">Sonarr</div><span class="card-url">http://192.168.8.123:8989/</span></a>
<a class="card" href="http://192.168.8.123:7878/"><div class="card-title">Radarr</div><span class="card-url">http://192.168.8.123:7878/</span></a>
<a class="card" href="http://192.168.8.123:8686/"><div class="card-title">Lidarr</div><span class="card-url">http://192.168.8.123:8686/</span></a>
<a class="card" href="http://192.168.8.123:9696/"><div class="card-title">Prowlarr</div><span class="card-url">http://192.168.8.123:9696/</span></a>
<a class="card" href="http://192.168.8.123:6767/"><div class="card-title">Bazarr</div><span class="card-url">http://192.168.8.123:6767/</span></a>
<a class="card" href="http://192.168.8.123:8080/"><div class="card-title">qBittorrent</div><span class="card-url">http://192.168.8.123:8080/</span></a>
<a class="card" href="http://192.168.8.123:8085/"><div class="card-title">SABnzbd</div><span class="card-url">http://192.168.8.123:8085/</span></a>
</div>
</section>
<section class="lane ai">
<div class="lane-head"><h2><span class="dot"></span>Local AI</h2></div>
<div class="grid">
<a class="card" href="http://192.168.8.123:3000/"><div class="card-title">Open WebUI</div><span class="card-url">http://192.168.8.123:3000/</span></a>
<a class="card" href="http://192.168.8.123:8081/v1"><div class="card-title">Bonsai API</div><span class="card-url">http://192.168.8.123:8081/v1</span></a>
<a class="card" href="http://192.168.8.123:3001/"><div class="card-title">OpenHands</div><span class="card-url">http://192.168.8.123:3001/</span></a>
</div>
</section>
<section class="lane code">
<div class="lane-head"><h2><span class="dot"></span>Code &amp; git</h2></div>
<div class="grid">
<a class="card" href="http://192.168.8.123:3002/"><div class="card-title">Forgejo</div><span class="card-url">http://192.168.8.123:3002/</span></a>
</div>
</section>
</noscript>
</div>
<section class="guide">
<h2>Quick start for humans</h2>
<ol>
<li><strong>Watch movies &amp; shows</strong> — open <em>Jellyfin</em>. Create a user if asked. Libraries are already on the server.</li>
<li><strong>Request / automate media</strong> — use <em>Sonarr</em> (TV), <em>Radarr</em> (movies), <em>Lidarr</em> (music). They search via <em>Prowlarr</em> and download with torrents or Usenet.</li>
<li><strong>Downloads</strong><em>qBittorrent</em> is for torrents (goes through the VPN). <em>SABnzbd</em> is for Usenet/NZB files. You usually dont open these unless something is stuck.</li>
<li><strong>Talk to the local AI</strong><em>Open WebUI</em> is the chat box. The model runs on this mini PC (private, no cloud).</li>
<li><strong>Code &amp; git</strong><em>Forgejo</em> is our GitHub-like site. <em>OpenHands</em> is an AI coding agent.</li>
<li><strong>HTTP only</strong> — use <code>http://</code> links as shown. There are no TLS certificates on these ports yet; <code>https://</code> will fail.</li>
<li><strong>Only on this WiFi / LAN</strong> — these links wont work from mobile data unless you set up remote access later.</li>
</ol>
</section>
<footer>
Served by Caddy · arr-stack · plain HTTP on the LAN
</footer>
</div>
<script>
(function () {
// Prefer the host you used to open this page; never upgrade to https.
// If opened via localhost on the server, fall back to the LAN IP.
const DEFAULT_HOST = "192.168.8.123";
let host = (location.hostname || DEFAULT_HOST).trim();
if (!host || host === "localhost" || host === "127.0.0.1" || host === "::1") {
host = DEFAULT_HOST;
}
document.getElementById("host").textContent = host;
/** Always absolute plain-HTTP URL with trailing slash for UIs */
function httpUrl(port, path) {
path = path || "/";
if (path.charAt(0) !== "/") path = "/" + path;
// APIs like /v1 should not force trailing slash doubling
var base = "http://" + host + ":" + String(port);
if (path === "/") return base + "/";
return base + path;
}
const lanes = [
{
id: "media",
title: "Watch & enjoy",
hint: "Day-to-day media",
items: [
{
name: "Jellyfin",
what: "TV, movies & music player (like Netflix at home)",
port: 8096,
path: "/",
how: "Open → pick a library → play. Best starting point for family.",
},
],
},
{
id: "download",
title: "Find & download",
hint: "Automation + grabbers",
items: [
{
name: "Sonarr",
what: "TV show manager — finds and downloads episodes",
port: 8989,
path: "/",
how: "Add a series → it searches and grabs new episodes automatically.",
},
{
name: "Radarr",
what: "Movie manager — finds and downloads films",
port: 7878,
path: "/",
how: "Add a movie → it searches and downloads when available.",
},
{
name: "Lidarr",
what: "Music manager — albums & artists",
port: 8686,
path: "/",
how: "Add an artist/album → downloads to the music library.",
},
{
name: "Prowlarr",
what: "Search hub — talks to torrent/Usenet indexers",
port: 9696,
path: "/",
how: "Usually leave alone; Sonarr/Radarr use it under the hood.",
},
{
name: "Bazarr",
what: "Subtitles for TV & movies",
port: 6767,
path: "/",
how: "Fetches subs once Sonarr/Radarr have the files.",
},
{
name: "qBittorrent",
what: "Torrent downloads (VPN-protected)",
port: 8080,
path: "/",
how: "Check the queue if a torrent is stuck. Login: admin (see server docs).",
},
{
name: "SABnzbd",
what: "Usenet / NZB downloads",
port: 8085,
path: "/",
how: "Queue for NZB jobs from Prowlarr/Sonarr. SSL to the provider.",
},
],
},
{
id: "ai",
title: "Local AI",
hint: "Runs on this mini PC",
items: [
{
name: "Open WebUI",
what: "Chat with the local AI (docs, questions, coding help)",
port: 3000,
path: "/",
how: "Create an account on first visit. Chat stays on your network.",
},
{
name: "Bonsai API",
what: "Raw AI API for apps (OpenAI-compatible)",
port: 8081,
path: "/v1",
how: "For developers: base URL ends with /v1. Not a chat UI.",
},
{
name: "OpenHands",
what: "AI coding agent",
port: 3001,
path: "/",
how: "Always on with this stack. Point it at /workspace for projects.",
},
],
},
{
id: "code",
title: "Code & git",
hint: "Repos and projects",
items: [
{
name: "Forgejo",
what: "Git hosting — our private GitHub",
port: 3002,
path: "/",
how: "Sign in → browse repos (e.g. tim/stack). Clone over HTTP or SSH port 2222.",
},
],
},
];
function esc(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
const map = document.getElementById("map");
map.innerHTML = lanes
.map(function (lane) {
const cards = lane.items
.map(function (item) {
const href = httpUrl(item.port, item.path);
return (
'<a class="card" href="' +
esc(href) +
'" target="_blank" rel="noopener noreferrer">' +
'<div class="card-title">' +
esc(item.name) +
"</div>" +
'<div class="card-what">' +
esc(item.what) +
"</div>" +
'<span class="card-url">' +
esc(href) +
"</span>" +
'<div class="card-howto">' +
esc(item.how) +
"</div>" +
"</a>"
);
})
.join("");
return (
'<section class="lane ' +
esc(lane.id) +
'">' +
'<div class="lane-head"><h2><span class="dot"></span>' +
esc(lane.title) +
'</h2><span class="hint">' +
esc(lane.hint) +
"</span></div>" +
'<div class="grid">' +
cards +
"</div></section>"
);
})
.join("");
})();
</script>
</body>
</html>