Add homelab diagrams and roadmap

This commit is contained in:
juvdiaz 2026-05-25 18:58:25 -06:00
parent 4bf61e7490
commit 0ad1018d40
4 changed files with 1063 additions and 3 deletions

View File

@ -1,4 +1,35 @@
<?php require_once __DIR__ . '/lang_helper.php'; ?>
<?php
require_once __DIR__ . '/lang_helper.php';
$activityKeys = [
'blog_activity_1',
'blog_activity_2',
'blog_activity_3',
'blog_activity_4',
'blog_activity_5',
'blog_activity_6',
'blog_activity_7',
'blog_activity_8',
'blog_activity_9',
];
$todoKeys = [
'blog_todo_1',
'blog_todo_2',
'blog_todo_3',
'blog_todo_4',
'blog_todo_5',
'blog_todo_6',
'blog_todo_7',
'blog_todo_8',
'blog_todo_9',
'blog_todo_10',
'blog_todo_11',
'blog_todo_12',
];
$treeHref = 'homelab-tree.php?lang=' . urlencode($lang);
?>
<!DOCTYPE html>
<html lang="<?php echo $lang; ?>">
<head>
@ -56,6 +87,183 @@
</p>
</section>
<section class="architecture-section" aria-labelledby="architecture-title">
<div class="section-heading">
<p class="section-kicker"
data-translate data-key="blog_arch_kicker"
data-en="<?php echo htmlspecialchars($en['blog_arch_kicker']); ?>">
<?php echo $text['blog_arch_kicker']; ?>
</p>
<h2 id="architecture-title"
data-translate data-key="blog_arch_title"
data-en="<?php echo htmlspecialchars($en['blog_arch_title']); ?>">
<?php echo $text['blog_arch_title']; ?>
</h2>
<p data-translate data-key="blog_arch_intro"
data-en="<?php echo htmlspecialchars($en['blog_arch_intro']); ?>">
<?php echo $text['blog_arch_intro']; ?>
</p>
</div>
<div class="diagram-shell" aria-label="Professional homelab architecture diagram">
<svg class="homelab-map" viewBox="0 0 1120 720" role="img" aria-labelledby="homelab-map-title homelab-map-desc">
<title id="homelab-map-title">Homelab architecture map</title>
<desc id="homelab-map-desc">Git push enters Gitea, Gitea Actions validates and builds images, OpenTofu manages the cluster, Argo CD syncs manifests, and the OCI edge routes traffic over Tailscale into Kubernetes services.</desc>
<defs>
<marker id="map-arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
<path d="M2,2 L10,6 L2,10 Z" fill="#2b6cb0"></path>
</marker>
</defs>
<rect class="diagram-zone diagram-zone-source" x="24" y="40" width="310" height="640" rx="12"></rect>
<text class="diagram-zone-title" x="46" y="74">Source, validation, and images</text>
<rect class="diagram-zone diagram-zone-platform" x="382" y="40" width="340" height="640" rx="12"></rect>
<text class="diagram-zone-title" x="404" y="74">Debian node 192.168.100.68</text>
<rect class="diagram-zone diagram-zone-runtime" x="770" y="40" width="326" height="640" rx="12"></rect>
<text class="diagram-zone-title" x="792" y="74">Edge access and workloads</text>
<g class="diagram-node node-accent-blue" transform="translate(54 110)">
<rect width="240" height="74" rx="8"></rect>
<text x="18" y="28">Developer laptop</text>
<text class="diagram-small" x="18" y="52">edit, test, push main</text>
</g>
<g class="diagram-node node-accent-blue" transform="translate(54 218)">
<rect width="240" height="82" rx="8"></rect>
<text x="18" y="27">Gitea repository</text>
<text class="diagram-small" x="18" y="50">https://lab2025.duckdns.org/git/</text>
<text class="diagram-small" x="18" y="68">main is the release branch</text>
</g>
<g class="diagram-node node-accent-teal" transform="translate(54 338)">
<rect width="240" height="82" rx="8"></rect>
<text x="18" y="27">Gitea Actions runner</text>
<text class="diagram-small" x="18" y="50">Debian hosted runner</text>
<text class="diagram-small" x="18" y="68">custom checkout for /git/ path</text>
</g>
<g class="diagram-node node-accent-red" transform="translate(54 458)">
<rect width="240" height="82" rx="8"></rect>
<text x="18" y="27">Validation gates</text>
<text class="diagram-small" x="18" y="50">Gitleaks secret scan</text>
<text class="diagram-small" x="18" y="68">Trivy IaC and image posture</text>
</g>
<g class="diagram-node node-accent-green" transform="translate(54 578)">
<rect width="240" height="70" rx="8"></rect>
<text x="18" y="27">Buildx image build</text>
<text class="diagram-small" x="18" y="50">linux/arm64 website + demos</text>
</g>
<g class="diagram-node node-accent-purple" transform="translate(412 108)">
<rect width="280" height="82" rx="8"></rect>
<text x="18" y="27">OpenTofu + lab.sh</text>
<text class="diagram-small" x="18" y="50">infra, platform, apps, edge</text>
<text class="diagram-small" x="18" y="68">repeatable apply path</text>
</g>
<g class="diagram-node node-accent-blue" transform="translate(412 222)">
<rect width="280" height="82" rx="8"></rect>
<text x="18" y="27">kubeadm control plane</text>
<text class="diagram-small" x="18" y="50">API server, scheduler, controller</text>
<text class="diagram-small" x="18" y="68">Calico pod networking</text>
</g>
<g class="diagram-node node-accent-teal" transform="translate(412 336)">
<rect width="280" height="82" rx="8"></rect>
<text x="18" y="27">GitOps mirror</text>
<text class="diagram-small" x="18" y="50">validated commit copied locally</text>
<text class="diagram-small" x="18" y="68">Argo CD reads deploy state</text>
</g>
<g class="diagram-node node-accent-green" transform="translate(412 450)">
<rect width="280" height="82" rx="8"></rect>
<text x="18" y="27">Argo CD</text>
<text class="diagram-small" x="18" y="50">container-registry, website</text>
<text class="diagram-small" x="18" y="68">gitea and demos-static apps</text>
</g>
<g class="diagram-node node-accent-orange" transform="translate(412 564)">
<rect width="280" height="82" rx="8"></rect>
<text x="18" y="27">Storage and backups</text>
<text class="diagram-small" x="18" y="50">OpenEBS retained hostpath PVs</text>
<text class="diagram-small" x="18" y="68">Gitea dumps and external SSD</text>
</g>
<g class="diagram-node node-accent-blue" transform="translate(800 106)">
<rect width="258" height="96" rx="8"></rect>
<text x="18" y="28">OCI edge host</text>
<text class="diagram-small" x="18" y="52">nginx, HAProxy, Varnish, Squid</text>
<text class="diagram-small" x="18" y="70">TLS, routing, caching</text>
<text class="diagram-small" x="18" y="88">public DNS entry point</text>
</g>
<g class="diagram-node node-accent-purple" transform="translate(800 246)">
<rect width="258" height="82" rx="8"></rect>
<text x="18" y="27">Tailscale + NodePorts</text>
<text class="diagram-small" x="18" y="50">30080 website, 30081 demos</text>
<text class="diagram-small" x="18" y="68">30300 Gitea service path</text>
</g>
<g class="diagram-node node-accent-green" transform="translate(800 366)">
<rect width="258" height="96" rx="8"></rect>
<text x="18" y="28">Raspberry Pi 192.168.100.89</text>
<text class="diagram-small" x="18" y="52">arm64 Kubernetes worker</text>
<text class="diagram-small" x="18" y="70">website-production pods</text>
<text class="diagram-small" x="18" y="88">demos-static and lab apps</text>
</g>
<g class="diagram-node node-accent-teal" transform="translate(800 506)">
<rect width="258" height="82" rx="8"></rect>
<text x="18" y="27">Local registry :30500</text>
<text class="diagram-small" x="18" y="50">php-website and demos-static</text>
<text class="diagram-small" x="18" y="68">pulled by arm64 workloads</text>
</g>
<path class="diagram-link" d="M174 184 L174 218"></path>
<path class="diagram-link" d="M174 300 L174 338"></path>
<path class="diagram-link" d="M174 420 L174 458"></path>
<path class="diagram-link" d="M174 540 L174 578"></path>
<path class="diagram-link" d="M294 616 C346 616 352 149 412 149"></path>
<path class="diagram-link" d="M294 616 C372 616 722 560 800 547"></path>
<path class="diagram-link" d="M294 379 C340 379 360 149 412 149"></path>
<path class="diagram-link" d="M294 259 C348 259 360 377 412 377"></path>
<path class="diagram-link" d="M552 304 L552 336"></path>
<path class="diagram-link" d="M552 418 L552 450"></path>
<path class="diagram-link" d="M692 491 C748 491 744 414 800 414"></path>
<path class="diagram-link" d="M929 202 L929 246"></path>
<path class="diagram-link" d="M929 328 L929 366"></path>
<path class="diagram-link" d="M929 506 L929 462"></path>
<text class="diagram-link-label" x="184" y="205">push</text>
<text class="diagram-link-label" x="196" y="327">workflow</text>
<text class="diagram-link-label" x="200" y="448">scan</text>
<text class="diagram-link-label" x="205" y="568">build</text>
<text class="diagram-link-label" x="320" y="124">apply</text>
<text class="diagram-link-label" x="346" y="346">validated Git</text>
<text class="diagram-link-label" x="710" y="462">sync apps</text>
<text class="diagram-link-label" x="934" y="232">secure tunnel</text>
<text class="diagram-link-label" x="934" y="352">service traffic</text>
<text class="diagram-link-label" x="946" y="492">image pulls</text>
</svg>
</div>
<div class="diagram-actions">
<p class="diagram-caption"
data-translate data-key="blog_arch_caption"
data-en="<?php echo htmlspecialchars($en['blog_arch_caption']); ?>">
<?php echo $text['blog_arch_caption']; ?>
</p>
<a class="diagram-fun-link" href="<?php echo htmlspecialchars($treeHref); ?>"
data-translate data-key="blog_arch_fun_link"
data-en="<?php echo htmlspecialchars($en['blog_arch_fun_link']); ?>">
<?php echo $text['blog_arch_fun_link']; ?>
</a>
</div>
</section>
<section class="conversation" aria-label="Homelab build conversation">
<article class="message question">
<div class="speaker"
@ -226,6 +434,36 @@
</article>
</section>
<section class="activity-log" aria-labelledby="activity-log-title">
<div class="section-heading">
<p class="section-kicker"
data-translate data-key="blog_activity_kicker"
data-en="<?php echo htmlspecialchars($en['blog_activity_kicker']); ?>">
<?php echo $text['blog_activity_kicker']; ?>
</p>
<h2 id="activity-log-title"
data-translate data-key="blog_activity_title"
data-en="<?php echo htmlspecialchars($en['blog_activity_title']); ?>">
<?php echo $text['blog_activity_title']; ?>
</h2>
<p data-translate data-key="blog_activity_intro"
data-en="<?php echo htmlspecialchars($en['blog_activity_intro']); ?>">
<?php echo $text['blog_activity_intro']; ?>
</p>
</div>
<ol class="activity-list">
<?php foreach ($activityKeys as $index => $activityKey): ?>
<li>
<span class="activity-number"><?php echo str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT); ?></span>
<span data-translate data-key="<?php echo htmlspecialchars($activityKey); ?>"
data-en="<?php echo htmlspecialchars($en[$activityKey]); ?>">
<?php echo $text[$activityKey]; ?>
</span>
</li>
<?php endforeach; ?>
</ol>
</section>
<section class="tech-notes">
<h2 data-translate data-key="blog_stack_title"
data-en="<?php echo htmlspecialchars($en['blog_stack_title']); ?>">
@ -278,10 +516,40 @@
</li>
</ul>
</section>
<section class="homelab-todo" aria-labelledby="homelab-todo-title">
<div class="section-heading">
<p class="section-kicker"
data-translate data-key="blog_todo_kicker"
data-en="<?php echo htmlspecialchars($en['blog_todo_kicker']); ?>">
<?php echo $text['blog_todo_kicker']; ?>
</p>
<h2 id="homelab-todo-title"
data-translate data-key="blog_todo_title"
data-en="<?php echo htmlspecialchars($en['blog_todo_title']); ?>">
<?php echo $text['blog_todo_title']; ?>
</h2>
<p data-translate data-key="blog_todo_intro"
data-en="<?php echo htmlspecialchars($en['blog_todo_intro']); ?>">
<?php echo $text['blog_todo_intro']; ?>
</p>
</div>
<ul class="todo-list">
<?php foreach ($todoKeys as $todoKey): ?>
<li>
<span class="todo-checkbox" aria-hidden="true"></span>
<span data-translate data-key="<?php echo htmlspecialchars($todoKey); ?>"
data-en="<?php echo htmlspecialchars($en[$todoKey]); ?>">
<?php echo $text[$todoKey]; ?>
</span>
</li>
<?php endforeach; ?>
</ul>
</section>
</main>
<script>
const OTHER_PAGES = ['/index.php', '/cv.php'];
const OTHER_PAGES = ['/index.php', '/cv.php', '/homelab-tree.php'];
</script>
<?php require_once __DIR__ . '/partials/translation_ui.php'; ?>

View File

@ -0,0 +1,261 @@
<?php
require_once __DIR__ . '/lang_helper.php';
$blogHref = 'blog.php?lang=' . urlencode($lang);
?>
<!DOCTYPE html>
<html lang="<?php echo $lang; ?>">
<head>
<meta charset="UTF-8">
<title><?php echo $text['tree_title']; ?> - <?php echo $text['name']; ?></title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav class="top-nav">
<div class="nav-left">Juvenal Diaz</div>
<div class="nav-right">
<?php foreach ($availableLangs as $code): ?>
<a href="homelab-tree.php?lang=<?php echo $code; ?>"><?php echo strtoupper($code); ?></a>
<?php endforeach; ?>
|
<a href="index.php?lang=<?php echo $lang; ?>"
data-translate data-key="nav_home"
data-en="<?php echo htmlspecialchars($en['nav_home']); ?>">
<?php echo $text['nav_home']; ?>
</a>
<a href="cv.php?lang=<?php echo $lang; ?>"
data-translate data-key="nav_cv"
data-en="<?php echo htmlspecialchars($en['nav_cv']); ?>">
<?php echo $text['nav_cv']; ?>
</a>
<a href="blog.php?lang=<?php echo $lang; ?>"
data-translate data-key="nav_blog"
data-en="<?php echo htmlspecialchars($en['nav_blog']); ?>">
<?php echo $text['nav_blog']; ?>
</a>
<a href="demos.php?lang=<?php echo $lang; ?>"
data-translate data-key="nav_demos"
data-en="<?php echo htmlspecialchars($en['nav_demos']); ?>">
<?php echo $text['nav_demos']; ?>
</a>
</div>
</nav>
<main class="tree-page">
<section class="tree-hero" aria-labelledby="tree-title">
<div class="section-heading">
<p class="section-kicker"
data-translate data-key="tree_kicker"
data-en="<?php echo htmlspecialchars($en['tree_kicker']); ?>">
<?php echo $text['tree_kicker']; ?>
</p>
<h1 id="tree-title"
data-translate data-key="tree_title"
data-en="<?php echo htmlspecialchars($en['tree_title']); ?>">
<?php echo $text['tree_title']; ?>
</h1>
<p data-translate data-key="tree_subtitle"
data-en="<?php echo htmlspecialchars($en['tree_subtitle']); ?>">
<?php echo $text['tree_subtitle']; ?>
</p>
<a class="tree-back-link" href="<?php echo htmlspecialchars($blogHref); ?>"
data-translate data-key="tree_back_to_blog"
data-en="<?php echo htmlspecialchars($en['tree_back_to_blog']); ?>">
<?php echo $text['tree_back_to_blog']; ?>
</a>
</div>
<div class="tree-stage" aria-label="Christmas tree version of the homelab architecture">
<svg class="christmas-homelab" viewBox="0 0 1040 940" role="img" aria-labelledby="christmas-map-title christmas-map-desc">
<title id="christmas-map-title">Homelab Christmas tree map</title>
<desc id="christmas-map-desc">A Christmas tree where each tree part represents a homelab component, from DNS and TLS at the star to storage and backups in the roots.</desc>
<rect class="tree-sky" x="0" y="0" width="1040" height="940" rx="24"></rect>
<circle class="tree-light tree-light-blue" cx="120" cy="92" r="5"></circle>
<circle class="tree-light tree-light-gold" cx="240" cy="150" r="4"></circle>
<circle class="tree-light tree-light-red" cx="842" cy="94" r="5"></circle>
<circle class="tree-light tree-light-green" cx="910" cy="178" r="4"></circle>
<circle class="tree-light tree-light-blue" cx="752" cy="126" r="4"></circle>
<g class="tree-star">
<polygon points="520,42 538,88 588,88 548,118 564,166 520,136 476,166 492,118 452,88 502,88"></polygon>
<text x="520" y="105">Public DNS + TLS</text>
<text class="tree-small" x="520" y="126">lab2025.duckdns.org</text>
</g>
<path class="garland garland-blue" d="M300 238 C410 284 622 196 742 244"></path>
<path class="garland garland-gold" d="M234 372 C384 440 674 310 820 382"></path>
<path class="garland garland-red" d="M168 540 C348 628 720 452 884 552"></path>
<text class="tree-garland-label" x="732" y="232">Tailscale mesh</text>
<text class="tree-garland-label" x="802" y="366">NodePorts</text>
<text class="tree-garland-label" x="830" y="526">GitOps sync</text>
<polygon class="tree-layer tree-layer-top" points="520,140 314,320 430,320 254,470 396,470 182,650 858,650 644,470 786,470 610,320 726,320"></polygon>
<polygon class="tree-layer tree-layer-shadow" points="520,214 358,346 452,346 306,470 428,470 250,620 792,620 612,470 734,470 588,346 682,346"></polygon>
<g class="tree-ornament ornament-blue" transform="translate(430 266)">
<circle r="38"></circle>
<text y="-7">Gitea</text>
<text class="tree-small" y="14">repo + web UI</text>
</g>
<g class="tree-ornament ornament-gold" transform="translate(592 276)">
<circle r="38"></circle>
<text y="-7">Actions</text>
<text class="tree-small" y="14">runner</text>
</g>
<g class="tree-ornament ornament-red" transform="translate(346 392)">
<circle r="40"></circle>
<text y="-8">Gitleaks</text>
<text class="tree-small" y="14">secrets</text>
</g>
<g class="tree-ornament ornament-purple" transform="translate(520 398)">
<circle r="44"></circle>
<text y="-10">OpenTofu</text>
<text class="tree-small" y="12">lab.sh apply</text>
</g>
<g class="tree-ornament ornament-teal" transform="translate(696 394)">
<circle r="40"></circle>
<text y="-8">Trivy</text>
<text class="tree-small" y="14">posture</text>
</g>
<g class="tree-ornament ornament-green" transform="translate(278 534)">
<circle r="42"></circle>
<text y="-8">Calico</text>
<text class="tree-small" y="14">network</text>
</g>
<g class="tree-ornament ornament-blue" transform="translate(430 548)">
<circle r="42"></circle>
<text y="-8">Argo CD</text>
<text class="tree-small" y="14">sync loop</text>
</g>
<g class="tree-ornament ornament-red" transform="translate(592 548)">
<circle r="42"></circle>
<text y="-8">Registry</text>
<text class="tree-small" y="14">:30500</text>
</g>
<g class="tree-ornament ornament-gold" transform="translate(750 532)">
<circle r="42"></circle>
<text y="-8">Buildx</text>
<text class="tree-small" y="14">arm64</text>
</g>
<g class="tree-ornament ornament-teal" transform="translate(350 646)">
<circle r="44"></circle>
<text y="-9">Website</text>
<text class="tree-small" y="13">portfolio pod</text>
</g>
<g class="tree-ornament ornament-purple" transform="translate(522 664)">
<circle r="46"></circle>
<text y="-9">Gitea app</text>
<text class="tree-small" y="13">Git service</text>
</g>
<g class="tree-ornament ornament-green" transform="translate(700 646)">
<circle r="44"></circle>
<text y="-9">Demos</text>
<text class="tree-small" y="13">static app</text>
</g>
<g class="tree-bell" transform="translate(226 442)">
<path d="M0,-24 C24,-18 30,12 22,30 L-22,30 C-30,12 -24,-18 0,-24 Z"></path>
<circle cy="34" r="7"></circle>
<text x="0" y="56">probes</text>
</g>
<g class="tree-bell" transform="translate(812 446)">
<path d="M0,-24 C24,-18 30,12 22,30 L-22,30 C-30,12 -24,-18 0,-24 Z"></path>
<circle cy="34" r="7"></circle>
<text x="0" y="56">health checks</text>
</g>
<rect class="tree-trunk" x="468" y="650" width="104" height="142" rx="12"></rect>
<text class="tree-trunk-text" x="520" y="700">Debian</text>
<text class="tree-trunk-text tree-small" x="520" y="722">control plane</text>
<text class="tree-trunk-text tree-small" x="520" y="744">192.168.100.68</text>
<path class="tree-root" d="M520 790 C456 814 390 822 310 822"></path>
<path class="tree-root" d="M520 790 C584 816 660 824 740 822"></path>
<path class="tree-root" d="M520 790 C506 830 486 852 438 870"></path>
<path class="tree-root" d="M520 790 C536 830 566 852 612 870"></path>
<text class="tree-root-label" x="306" y="850">OpenEBS retained PVs</text>
<text class="tree-root-label" x="734" y="850">external SSD</text>
<text class="tree-root-label" x="520" y="894">Gitea dumps + restore drills</text>
<g class="tree-gift gift-blue" transform="translate(118 738)">
<rect width="128" height="84" rx="8"></rect>
<path d="M64 0 L64 84 M0 30 L128 30"></path>
<text x="64" y="56">OCI edge</text>
<text class="tree-small" x="64" y="74">nginx/HAProxy</text>
</g>
<g class="tree-gift gift-red" transform="translate(760 738)">
<rect width="148" height="84" rx="8"></rect>
<path d="M74 0 L74 84 M0 30 L148 30"></path>
<text x="74" y="56">Raspberry Pi</text>
<text class="tree-small" x="74" y="74">arm64 worker</text>
</g>
<g class="tree-gift gift-green" transform="translate(256 800)">
<rect width="132" height="72" rx="8"></rect>
<path d="M66 0 L66 72 M0 26 L132 26"></path>
<text x="66" y="48">backlog</text>
<text class="tree-small" x="66" y="64">roadmap gifts</text>
</g>
<g class="tree-gift gift-gold" transform="translate(642 800)">
<rect width="142" height="72" rx="8"></rect>
<path d="M71 0 L71 72 M0 26 L142 26"></path>
<text x="71" y="48">monitoring</text>
<text class="tree-small" x="71" y="64">future lights</text>
</g>
</svg>
</div>
</section>
<section class="tree-key" aria-labelledby="tree-key-title">
<div class="section-heading">
<p class="section-kicker"
data-translate data-key="tree_key_kicker"
data-en="<?php echo htmlspecialchars($en['tree_key_kicker']); ?>">
<?php echo $text['tree_key_kicker']; ?>
</p>
<h2 id="tree-key-title"
data-translate data-key="tree_key_title"
data-en="<?php echo htmlspecialchars($en['tree_key_title']); ?>">
<?php echo $text['tree_key_title']; ?>
</h2>
<p data-translate data-key="tree_key_intro"
data-en="<?php echo htmlspecialchars($en['tree_key_intro']); ?>">
<?php echo $text['tree_key_intro']; ?>
</p>
</div>
<ul class="tree-key-list">
<li><strong>Star:</strong> public DNS, TLS, and the entry point users actually type.</li>
<li><strong>Garlands:</strong> Tailscale routing, NodePorts, and the GitOps sync loop connecting the layers.</li>
<li><strong>Branches:</strong> Kubernetes namespaces and workloads that carry the visible services.</li>
<li><strong>Ornaments:</strong> Gitea, Actions, Argo CD, Buildx, Trivy, Gitleaks, Calico, registry, website, demos, and the Gitea app.</li>
<li><strong>Bells:</strong> probes and health checks that make noise before users do.</li>
<li><strong>Trunk:</strong> the Debian control-plane node that holds the platform upright.</li>
<li><strong>Roots:</strong> OpenEBS retained volumes, external SSD storage, Gitea dumps, and restore discipline.</li>
<li><strong>Gifts:</strong> the OCI edge host, Raspberry Pi worker, monitoring ideas, and the improvement backlog.</li>
</ul>
</section>
</main>
<script>
const OTHER_PAGES = ['/index.php', '/cv.php', '/blog.php'];
</script>
<?php require_once __DIR__ . '/partials/translation_ui.php'; ?>
</body>
</html>

View File

@ -88,6 +88,45 @@ return [
'blog_stack_9' => 'The newer demos cover network jitter graphs, local JSON/JWT/log tools, an architecture simulator, an offline traveler converter, a redactor prototype, sentiment analysis, and model-drift simulation.',
'blog_stack_10' => 'The heavier ML demos are designed as client-side Wasm/ONNX/Transformers.js candidates, not server-side jobs. That keeps the homelab app boring to operate, which is secretly the whole point.',
'blog_stack_11' => 'The demo code now builds into its own demos-static image and Argo CD app, exposed at /demo-apps/. The PHP website only owns the catalog link, which is much less cursed.',
'blog_arch_kicker' => 'Architecture map',
'blog_arch_title' => 'The homelab, end to end',
'blog_arch_intro' => 'The current delivery path starts with a push to Gitea, runs local validation, builds arm64 images, syncs the validated commit into the GitOps mirror, and lets Argo CD reconcile the Kubernetes workloads while the OCI edge routes public traffic back through the private path.',
'blog_arch_caption' => 'The diagram is intentionally operational: it shows the control flow, image flow, storage boundary, and public traffic path without hiding the practical bits that make a small lab behave like a platform.',
'blog_arch_fun_link' => 'Open the Christmas-tree version',
'blog_activity_kicker' => 'Recent activity log',
'blog_activity_title' => 'What changed since the first build',
'blog_activity_intro' => 'The lab moved from a working Kubernetes experiment into a more complete self-hosted delivery system. The latest work focused on trust, repeatability, and making deploys match the exact commit that passed validation.',
'blog_activity_1' => 'Brought Gitea online as the local Git service, including persistent storage and the public /git/ route through the edge stack.',
'blog_activity_2' => 'Installed and validated a Debian-hosted Gitea Actions runner so pushes to main can build, scan, and deploy without depending on a laptop session.',
'blog_activity_3' => 'Added a custom checkout flow for the /git/ subpath and kept a persistent Debian checkout for the deployment scripts.',
'blog_activity_4' => 'Added Gitleaks secret scanning and Trivy scanning, with scoped exceptions only where the lab intentionally accepts a known Gitea workload shape.',
'blog_activity_5' => 'Changed deployment so the validated commit is pushed into the local GitOps mirror before lab.sh runs, preventing Argo CD from reconciling an older tree.',
'blog_activity_6' => 'Hardened the website, demos-static, and registry workloads with non-root containers, read-only root filesystems, resource limits, and explicit writable volumes.',
'blog_activity_7' => 'Split the demos into a dedicated demos-static image and Argo CD application so the PHP website stays small and boring.',
'blog_activity_8' => 'Fixed Gitea operational details around probes, service paths, backup dumps, and the user context used for safe backup execution.',
'blog_activity_9' => 'Validated the full main-branch deployment path: fetch main, apply OpenTofu layers, build and push arm64 images, refresh Argo CD, and confirm the runner completes successfully.',
'blog_todo_kicker' => 'Improvement backlog',
'blog_todo_title' => 'Todo list for the next homelab pass',
'blog_todo_intro' => 'These are improvement proposals, not chores for the sake of chores. Each item either reduces rebuild risk, tightens supply-chain hygiene, or makes the platform easier to operate when something fails.',
'blog_todo_1' => 'Move Gitea to a rootless runtime image and remove the remaining privileged assumptions from the Git service.',
'blog_todo_2' => 'Point Argo CD directly at Gitea once bootstrap is stable, then retire or simplify the local bare GitOps mirror.',
'blog_todo_3' => 'Add a real OpenTofu remote state backend with backup, locking, and a documented recovery path.',
'blog_todo_4' => 'Replace mutable latest image references with immutable tags or digest pins for website and demo workloads.',
'blog_todo_5' => 'Generate SBOMs and sign images so the local registry can prove what it is serving.',
'blog_todo_6' => 'Add Renovate or Dependabot-style dependency updates for base images, Helm charts, and GitHub/Gitea Actions.',
'blog_todo_7' => 'Enforce baseline Kubernetes policy with Kyverno or Gatekeeper: non-root, read-only roots, resource requests, and allowed registries.',
'blog_todo_8' => 'Install observability that fits the hardware: Prometheus, Grafana, Loki, node-exporter, and a few high-signal alerts.',
'blog_todo_9' => 'Schedule backup restore drills for Gitea and OpenEBS volumes, then write the exact restore runbook.',
'blog_todo_10' => 'Tighten TLS, SSH, and token rotation around the OCI edge, Gitea, registry, and runner credentials.',
'blog_todo_11' => 'Design the next storage step before adding more apps: NAS, replicated storage, or a clearly documented single-node tradeoff.',
'blog_todo_12' => 'Move sensitive app configuration into Sealed Secrets, External Secrets, or another explicit secret-management path.',
'tree_kicker' => 'Fun architecture mode',
'tree_title' => 'The Homelab Christmas Tree',
'tree_subtitle' => 'Same platform, less serious outfit: every part of the homelab becomes a tree part, from the public DNS star down to the storage roots and backup gifts.',
'tree_back_to_blog' => 'Back to the professional diagram',
'tree_key_kicker' => 'Tree legend',
'tree_key_title' => 'What each festive part means',
'tree_key_intro' => 'The joke still maps to the real architecture: each visual part has one operational job in the homelab.',
// Demos
'demos_kicker' => 'Small tools, real browser work',

View File

@ -386,7 +386,474 @@ body {
line-height: 1.55;
}
.architecture-section,
.activity-log,
.homelab-todo,
.tree-key {
margin-top: 34px;
padding: 24px;
background: #fff;
border: 1px solid #d9e2ec;
border-radius: 8px;
}
.section-heading {
max-width: 820px;
margin-bottom: 18px;
}
.section-heading h1,
.section-heading h2 {
margin: 0 0 10px;
color: #102a43;
}
.section-heading p {
margin: 0 0 12px;
color: #52606d;
line-height: 1.6;
}
.section-kicker {
color: #004085;
font-size: 0.82rem;
font-weight: 800;
text-transform: uppercase;
}
.diagram-shell {
overflow-x: auto;
border: 1px solid #d9e2ec;
border-radius: 8px;
background: #f8fbff;
}
.homelab-map {
display: block;
width: 100%;
min-width: 920px;
height: auto;
}
.diagram-zone {
fill: #f7fafc;
stroke: #cbd5e1;
stroke-width: 1.2;
}
.diagram-zone-source {
fill: #f7fbff;
}
.diagram-zone-platform {
fill: #f7fff9;
}
.diagram-zone-runtime {
fill: #fffaf2;
}
.diagram-zone-title {
fill: #102a43;
font-size: 17px;
font-weight: 800;
}
.diagram-node rect {
fill: #fff;
stroke-width: 1.5;
}
.diagram-node text {
fill: #102a43;
font-size: 16px;
font-weight: 800;
}
.diagram-node .diagram-small,
.diagram-small {
fill: #52606d;
font-size: 13px;
font-weight: 600;
}
.node-accent-blue rect {
stroke: #2b6cb0;
}
.node-accent-teal rect {
stroke: #2c7a7b;
}
.node-accent-red rect {
stroke: #c53030;
}
.node-accent-green rect {
stroke: #2f855a;
}
.node-accent-purple rect {
stroke: #6b46c1;
}
.node-accent-orange rect {
stroke: #b7791f;
}
.diagram-link {
fill: none;
stroke: #2b6cb0;
stroke-width: 2.2;
marker-end: url(#map-arrow);
}
.diagram-link-label {
fill: #334e68;
font-size: 13px;
font-weight: 800;
}
.diagram-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 16px;
}
.diagram-caption {
margin: 0;
color: #52606d;
line-height: 1.55;
}
.diagram-fun-link,
.tree-back-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 0 16px;
border-radius: 6px;
background: #004085;
color: #fff;
text-decoration: none;
font-weight: 800;
white-space: nowrap;
}
.diagram-fun-link:hover,
.tree-back-link:hover {
background: #002752;
}
.activity-list {
display: grid;
gap: 10px;
padding: 0;
margin: 0;
list-style: none;
}
.activity-list li {
display: grid;
grid-template-columns: 52px 1fr;
gap: 14px;
align-items: start;
padding: 14px;
border: 1px solid #e6edf5;
border-radius: 8px;
background: #fbfdff;
line-height: 1.55;
}
.activity-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 32px;
border-radius: 999px;
background: #e8f1ff;
color: #004085;
font-weight: 800;
}
.todo-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
padding: 0;
margin: 0;
list-style: none;
}
.todo-list li {
display: flex;
gap: 12px;
align-items: flex-start;
min-height: 76px;
padding: 14px;
border: 1px solid #e6edf5;
border-radius: 8px;
background: #fbfdff;
line-height: 1.5;
}
.todo-checkbox {
width: 18px;
height: 18px;
flex: 0 0 auto;
margin-top: 2px;
border: 2px solid #9fb3c8;
border-radius: 4px;
background: #fff;
}
.tree-page {
max-width: 1120px;
margin: 0 auto;
}
.tree-hero {
margin-bottom: 34px;
}
.tree-hero .section-heading h1 {
font-size: 2.4rem;
}
.tree-stage {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #1f3a4d;
background: #132635;
}
.christmas-homelab {
display: block;
width: 100%;
min-width: 900px;
height: auto;
}
.tree-sky {
fill: #132635;
}
.tree-light {
opacity: 0.95;
}
.tree-light-blue {
fill: #9ed7ff;
}
.tree-light-gold {
fill: #ffd166;
}
.tree-light-red {
fill: #ff7a7a;
}
.tree-light-green {
fill: #9cffb1;
}
.tree-star polygon {
fill: #ffd166;
stroke: #f2a900;
stroke-width: 4;
}
.tree-star text,
.tree-ornament text,
.tree-bell text,
.tree-gift text,
.tree-trunk-text,
.tree-root-label,
.tree-garland-label {
text-anchor: middle;
font-size: 16px;
font-weight: 800;
}
.tree-star text {
fill: #412500;
}
.tree-small {
font-size: 12px;
font-weight: 700;
}
.tree-layer {
stroke: #164f35;
stroke-width: 3;
}
.tree-layer-top {
fill: #1f7a4f;
}
.tree-layer-shadow {
fill: #17613f;
opacity: 0.9;
}
.garland {
fill: none;
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 12 12;
}
.garland-blue {
stroke: #9ed7ff;
}
.garland-gold {
stroke: #ffd166;
}
.garland-red {
stroke: #ff7a7a;
}
.tree-garland-label {
fill: #f8fbff;
font-size: 14px;
}
.tree-ornament circle {
stroke: rgba(255, 255, 255, 0.85);
stroke-width: 3;
}
.tree-ornament text {
fill: #102a43;
}
.ornament-blue circle {
fill: #b9e3ff;
}
.ornament-gold circle {
fill: #ffe29a;
}
.ornament-red circle {
fill: #ffb3b3;
}
.ornament-purple circle {
fill: #d6c8ff;
}
.ornament-teal circle {
fill: #aee9e4;
}
.ornament-green circle {
fill: #bdedc6;
}
.tree-bell path {
fill: #ffd166;
stroke: #f2a900;
stroke-width: 3;
}
.tree-bell circle {
fill: #f2a900;
}
.tree-bell text {
fill: #f8fbff;
font-size: 13px;
}
.tree-trunk {
fill: #7b4a2a;
stroke: #4a2b16;
stroke-width: 3;
}
.tree-trunk-text {
fill: #fff5df;
}
.tree-root {
fill: none;
stroke: #7b4a2a;
stroke-width: 8;
stroke-linecap: round;
}
.tree-root-label {
fill: #f8fbff;
font-size: 14px;
}
.tree-gift rect {
stroke: rgba(255, 255, 255, 0.85);
stroke-width: 3;
}
.tree-gift path {
stroke: rgba(255, 255, 255, 0.85);
stroke-width: 5;
}
.tree-gift text {
fill: #102a43;
}
.gift-blue rect {
fill: #b9e3ff;
}
.gift-red rect {
fill: #ffb3b3;
}
.gift-green rect {
fill: #bdedc6;
}
.gift-gold rect {
fill: #ffe29a;
}
.tree-key-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
padding: 0;
margin: 0;
list-style: none;
}
.tree-key-list li {
min-height: 86px;
padding: 14px;
border: 1px solid #e6edf5;
border-radius: 8px;
background: #fbfdff;
line-height: 1.5;
}
.tree-key-list strong {
color: #004085;
}
@media (max-width: 768px) {
body {
margin: 24px;
}
.top-nav {
align-items: flex-start;
flex-direction: column;
@ -400,6 +867,32 @@ body {
.message.answer {
justify-self: stretch;
}
.architecture-section,
.activity-log,
.homelab-todo,
.tree-key {
padding: 18px;
}
.diagram-actions {
align-items: stretch;
flex-direction: column;
}
.diagram-fun-link,
.tree-back-link {
white-space: normal;
text-align: center;
}
.activity-list li {
grid-template-columns: 1fr;
}
.tree-hero .section-heading h1 {
font-size: 2rem;
}
}
/* CV theme toggle */
@ -919,4 +1412,3 @@ body.home-page.cv-fancy {
font-size: 2.35rem;
}
}