stack/landing/site/index.html
tim a729ff14e9 Add mini/mid/gaming hardware profiles and always-on full stack
Profiles select CPU vs Vulkan and Bonsai-27B vs Qwen3.5-9B Abliterated
Q4_K_M. OpenHands is no longer optional; compose deploys every service.
download-models.sh fetches both GGUF families as the profile requests.
2026-07-22 19:00:59 -07:00

428 lines
12 KiB
HTML
Raw 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" />
<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);
--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); }
/* Visual map lanes */
.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(240px, 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.75rem;
color: var(--accent);
background: var(--accent-soft);
padding: 0.25rem 0.45rem;
border-radius: 6px;
display: inline-block;
word-break: break-all;
}
.card-howto {
margin-top: 0.65rem;
padding-top: 0.55rem;
border-top: 1px dashed var(--border);
font-size: 0.8rem;
color: var(--dim);
}
.card.offline {
opacity: 0.55;
pointer-events: none;
}
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);
}
@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"></strong></div>
</header>
<div class="map" id="map">
<!-- filled by JS so host IP stays correct on any LAN address -->
</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 (optional; start it if the link fails).</li>
<li><strong>Only on this WiFi / LAN</strong> — these links use the servers local address. They wont work from the mobile internet unless you set up remote access later.</li>
</ol>
</section>
<footer>
Served by Caddy · arr-stack · dark map for LAN browsers
</footer>
</div>
<script>
(function () {
const host = location.hostname || "192.168.8.123";
document.getElementById("host").textContent = host;
const u = (port, path) =>
"http://" + host + ":" + port + (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,
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,
how: "Add a series → it searches and grabs new episodes automatically.",
},
{
name: "Radarr",
what: "Movie manager — finds and downloads films",
port: 7878,
how: "Add a movie → it searches and downloads when available.",
},
{
name: "Lidarr",
what: "Music manager — albums & artists",
port: 8686,
how: "Add an artist/album → downloads to the music library.",
},
{
name: "Prowlarr",
what: "Search hub — talks to torrent/Usenet indexers",
port: 9696,
how: "Usually leave alone; Sonarr/Radarr use it under the hood.",
},
{
name: "Bazarr",
what: "Subtitles for TV & movies",
port: 6767,
how: "Fetches subs once Sonarr/Radarr have the files.",
},
{
name: "qBittorrent",
what: "Torrent downloads (VPN-protected)",
port: 8080,
how: "Check the queue if a torrent is stuck. Login: admin (see server docs).",
},
{
name: "SABnzbd",
what: "Usenet / NZB downloads",
port: 8085,
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,
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 (optional service)",
port: 3001,
how: "AI coding agent — 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,
how: "Sign in → browse repos (e.g. tim/stack). Clone over HTTP or SSH port 2222.",
},
],
},
];
const map = document.getElementById("map");
map.innerHTML = lanes
.map((lane) => {
const cards = lane.items
.map((item) => {
const href = u(item.port, item.path || "");
const pathLabel = item.path ? item.path : "";
return (
'<a class="card" href="' +
href +
'" target="_blank" rel="noopener">' +
'<div class="card-title">' +
item.name +
"</div>" +
'<div class="card-what">' +
item.what +
"</div>" +
'<span class="card-url">:' +
item.port +
pathLabel +
"</span>" +
'<div class="card-howto">' +
item.how +
"</div>" +
"</a>"
);
})
.join("");
return (
'<section class="lane ' +
lane.id +
'">' +
'<div class="lane-head"><h2><span class="dot"></span>' +
lane.title +
'</h2><span class="hint">' +
lane.hint +
"</span></div>" +
'<div class="grid">' +
cards +
"</div></section>"
);
})
.join("");
})();
</script>
</body>
</html>