900 lines
38 KiB
PHP
900 lines
38 KiB
PHP
<?php
|
|
require_once __DIR__ . '/lang_helper.php';
|
|
require_once __DIR__ . '/ideas_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',
|
|
'blog_activity_10',
|
|
'blog_activity_11',
|
|
'blog_activity_12',
|
|
'blog_activity_13',
|
|
];
|
|
|
|
$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',
|
|
'blog_todo_13',
|
|
'blog_todo_14',
|
|
'blog_todo_15',
|
|
];
|
|
|
|
$stackKeys = [
|
|
'blog_stack_1',
|
|
'blog_stack_2',
|
|
'blog_stack_3',
|
|
'blog_stack_4',
|
|
'blog_stack_5',
|
|
'blog_stack_6',
|
|
'blog_stack_7',
|
|
'blog_stack_8',
|
|
'blog_stack_9',
|
|
'blog_stack_10',
|
|
'blog_stack_11',
|
|
'blog_stack_12',
|
|
'blog_stack_13',
|
|
'blog_stack_14',
|
|
'blog_stack_15',
|
|
'blog_stack_16',
|
|
];
|
|
|
|
$caseStudyCards = [
|
|
[
|
|
'label' => 'case_platform_label',
|
|
'title' => 'case_platform_title',
|
|
'body' => 'case_platform_desc',
|
|
'href' => '#architecture-title',
|
|
'link' => 'case_platform_link',
|
|
],
|
|
[
|
|
'label' => 'case_sre_label',
|
|
'title' => 'case_sre_title',
|
|
'body' => 'case_sre_desc',
|
|
'href' => 'cv.php?lang=' . urlencode($lang),
|
|
'link' => 'case_sre_link',
|
|
],
|
|
[
|
|
'label' => 'case_mlops_label',
|
|
'title' => 'case_mlops_title',
|
|
'body' => 'case_mlops_desc',
|
|
'href' => '/demo-apps/mlops-platform/',
|
|
'link' => 'case_mlops_link',
|
|
],
|
|
];
|
|
|
|
$treeHref = 'homelab-tree.php?lang=' . urlencode($lang);
|
|
$ideaStatus = visitor_idea_clean((string) ($_GET['idea'] ?? ''), 20);
|
|
$visitorIdeas = visitor_ideas_read();
|
|
$giteaSourceBase = 'https://lab2025.duckdns.org/git/jv/my-homelab-configs/src/branch/main/';
|
|
$stackSourceLinks = [
|
|
'blog_stack_1' => [
|
|
['label' => 'lab.sh', 'path' => 'lab.sh'],
|
|
['label' => 'cluster/main.tf', 'path' => 'bootstrap/cluster/main.tf'],
|
|
['label' => 'provisioning/main.tf', 'path' => 'bootstrap/provisioning/main.tf'],
|
|
],
|
|
'blog_stack_2' => [
|
|
['label' => 'cluster/main.tf', 'path' => 'bootstrap/cluster/main.tf'],
|
|
['label' => 'provisioning/main.tf', 'path' => 'bootstrap/provisioning/main.tf'],
|
|
],
|
|
'blog_stack_3' => [
|
|
['label' => 'lab.sh', 'path' => 'lab.sh'],
|
|
['label' => 'cluster', 'path' => 'bootstrap/cluster/main.tf'],
|
|
['label' => 'platform', 'path' => 'bootstrap/platform/main.tf'],
|
|
['label' => 'apps', 'path' => 'bootstrap/apps/main.tf'],
|
|
['label' => 'provisioning', 'path' => 'bootstrap/provisioning/main.tf'],
|
|
['label' => 'edge', 'path' => 'bootstrap/edge/main.tf'],
|
|
],
|
|
'blog_stack_4' => [
|
|
['label' => 'platform/main.tf', 'path' => 'bootstrap/platform/main.tf'],
|
|
],
|
|
'blog_stack_5' => [
|
|
['label' => 'apps/main.tf', 'path' => 'bootstrap/apps/main.tf'],
|
|
],
|
|
'blog_stack_6' => [
|
|
['label' => 'edge/main.tf', 'path' => 'bootstrap/edge/main.tf'],
|
|
['label' => 'nginx template', 'path' => 'bootstrap/edge/templates/default.conf.tftpl'],
|
|
['label' => 'HAProxy template', 'path' => 'bootstrap/edge/templates/haproxy.cfg.tftpl'],
|
|
],
|
|
'blog_stack_7' => [
|
|
['label' => 'theme script', 'path' => 'apps/website/cv-theme.js'],
|
|
['label' => 'theme toolbar', 'path' => 'apps/website/partials/theme_toolbar.php'],
|
|
['label' => 'website CSS', 'path' => 'apps/website/styles.css'],
|
|
['label' => 'demo theme script', 'path' => 'apps/demos-static/public/theme.js'],
|
|
],
|
|
'blog_stack_8' => [
|
|
['label' => 'media-cruncher.js', 'path' => 'apps/demos-static/public/media-cruncher/media-cruncher.js'],
|
|
['label' => 'media-cruncher page', 'path' => 'apps/demos-static/public/media-cruncher/index.html'],
|
|
],
|
|
'blog_stack_9' => [
|
|
['label' => 'demo catalog', 'path' => 'apps/demos-static/public/index.html'],
|
|
['label' => 'network-quality.js', 'path' => 'apps/demos-static/public/network-quality/network-quality.js'],
|
|
['label' => 'dev-toolbelt.js', 'path' => 'apps/demos-static/public/dev-toolbelt/dev-toolbelt.js'],
|
|
['label' => 'architecture-simulator.js', 'path' => 'apps/demos-static/public/architecture-simulator/architecture-simulator.js'],
|
|
],
|
|
'blog_stack_10' => [
|
|
['label' => 'MLOps placeholder page', 'path' => 'apps/demos-static/public/mlops-platform/index.html'],
|
|
['label' => 'sentiment-sandbox.js', 'path' => 'apps/demos-static/public/sentiment-sandbox/sentiment-sandbox.js'],
|
|
['label' => 'model-drift.js', 'path' => 'apps/demos-static/public/model-drift/model-drift.js'],
|
|
['label' => 'privacy-redactor.js', 'path' => 'apps/demos-static/public/privacy-redactor/privacy-redactor.js'],
|
|
],
|
|
'blog_stack_11' => [
|
|
['label' => 'demos Dockerfile', 'path' => 'apps/demos-static/Dockerfile'],
|
|
['label' => 'demos web-app.yaml', 'path' => 'apps/demos-static/web-app.yaml'],
|
|
['label' => 'website demos.php', 'path' => 'apps/website/demos.php'],
|
|
],
|
|
'blog_stack_12' => [
|
|
['label' => 'provisioning/main.tf', 'path' => 'bootstrap/provisioning/main.tf'],
|
|
['label' => 'lab.sh Pimox pipeline', 'path' => 'lab.sh'],
|
|
['label' => 'provisioning README', 'path' => 'bootstrap/provisioning/README.md'],
|
|
],
|
|
'blog_stack_13' => [
|
|
['label' => 'preseed.cfg', 'path' => 'bootstrap/provisioning/templates/preseed.cfg.tftpl'],
|
|
['label' => 'golden-node prepare', 'path' => 'bootstrap/provisioning/templates/golden-node-prepare.sh.tftpl'],
|
|
['label' => 'prepare-template', 'path' => 'bootstrap/provisioning/templates/prepare-template.sh.tftpl'],
|
|
],
|
|
'blog_stack_14' => [
|
|
['label' => 'lab.sh worker storage guardrail', 'path' => 'lab.sh'],
|
|
['label' => 'README Pimox defaults', 'path' => 'README.md'],
|
|
],
|
|
'blog_stack_15' => [
|
|
['label' => 'lab.sh OpenWrt pipeline', 'path' => 'lab.sh'],
|
|
['label' => 'README OpenWrt notes', 'path' => 'README.md'],
|
|
],
|
|
'blog_stack_16' => [
|
|
['label' => 'platform/main.tf', 'path' => 'bootstrap/platform/main.tf'],
|
|
['label' => 'README monitoring', 'path' => 'README.md'],
|
|
],
|
|
];
|
|
|
|
function renderStackSourceLinks(string $stackKey, array $sourceLinks, string $sourceBase): void {
|
|
if (!isset($sourceLinks[$stackKey])) {
|
|
return;
|
|
}
|
|
echo '<div class="source-links"><span>Source:</span>';
|
|
foreach ($sourceLinks[$stackKey] as $sourceLink) {
|
|
$href = $sourceBase . $sourceLink['path'];
|
|
echo '<a href="' . htmlspecialchars($href) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($sourceLink['label']) . '</a>';
|
|
}
|
|
echo '</div>';
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="<?php echo $lang; ?>">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title><?php echo $text['blog_title']; ?> - <?php echo $text['name']; ?></title>
|
|
<link rel="stylesheet" href="styles.css">
|
|
</head>
|
|
<body class="theme-dark">
|
|
|
|
<nav class="top-nav">
|
|
<div class="nav-left">Juvenal Diaz</div>
|
|
<div class="nav-right">
|
|
<?php foreach ($availableLangs as $code): ?>
|
|
<a href="blog.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>
|
|
|
|
<?php require __DIR__ . '/partials/theme_toolbar.php'; ?>
|
|
|
|
<main class="blog-page">
|
|
<section class="blog-hero">
|
|
<p class="blog-kicker"
|
|
data-translate data-key="blog_kicker"
|
|
data-en="<?php echo htmlspecialchars($en['blog_kicker']); ?>">
|
|
<?php echo $text['blog_kicker']; ?>
|
|
</p>
|
|
<h1 data-translate data-key="blog_title"
|
|
data-en="<?php echo htmlspecialchars($en['blog_title']); ?>">
|
|
<?php echo $text['blog_title']; ?>
|
|
</h1>
|
|
<p class="blog-subtitle"
|
|
data-translate data-key="blog_subtitle"
|
|
data-en="<?php echo htmlspecialchars($en['blog_subtitle']); ?>">
|
|
<?php echo $text['blog_subtitle']; ?>
|
|
</p>
|
|
</section>
|
|
|
|
<section class="case-studies" aria-labelledby="case-studies-title">
|
|
<div class="section-heading">
|
|
<p class="section-kicker"
|
|
data-translate data-key="case_studies_kicker"
|
|
data-en="<?php echo htmlspecialchars($en['case_studies_kicker']); ?>">
|
|
<?php echo $text['case_studies_kicker']; ?>
|
|
</p>
|
|
<h2 id="case-studies-title"
|
|
data-translate data-key="case_studies_title"
|
|
data-en="<?php echo htmlspecialchars($en['case_studies_title']); ?>">
|
|
<?php echo $text['case_studies_title']; ?>
|
|
</h2>
|
|
<p data-translate data-key="case_studies_intro"
|
|
data-en="<?php echo htmlspecialchars($en['case_studies_intro']); ?>">
|
|
<?php echo $text['case_studies_intro']; ?>
|
|
</p>
|
|
</div>
|
|
<div class="case-study-grid">
|
|
<?php foreach ($caseStudyCards as $card): ?>
|
|
<article class="case-card">
|
|
<span data-translate data-key="<?php echo htmlspecialchars($card['label']); ?>"
|
|
data-en="<?php echo htmlspecialchars($en[$card['label']]); ?>">
|
|
<?php echo $text[$card['label']]; ?>
|
|
</span>
|
|
<h3 data-translate data-key="<?php echo htmlspecialchars($card['title']); ?>"
|
|
data-en="<?php echo htmlspecialchars($en[$card['title']]); ?>">
|
|
<?php echo $text[$card['title']]; ?>
|
|
</h3>
|
|
<p data-translate data-key="<?php echo htmlspecialchars($card['body']); ?>"
|
|
data-en="<?php echo htmlspecialchars($en[$card['body']]); ?>">
|
|
<?php echo $text[$card['body']]; ?>
|
|
</p>
|
|
<a href="<?php echo htmlspecialchars($card['href']); ?>"
|
|
data-translate data-key="<?php echo htmlspecialchars($card['link']); ?>"
|
|
data-en="<?php echo htmlspecialchars($en[$card['link']]); ?>">
|
|
<?php echo $text[$card['link']]; ?>
|
|
</a>
|
|
</article>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</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 960" 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 app images, OpenTofu manages cluster and provisioning layers, Debian keeps the control plane and PXE services, Pimox app workers run Argo CD, Kyverno, and app workloads on NVMe-backed VMs, OpenWrt can run as an opt-in firewall VM, and the OCI edge routes traffic 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="880" 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="880" rx="12"></rect>
|
|
<text class="diagram-zone-title" x="404" y="74">Control plane and provisioning</text>
|
|
|
|
<rect class="diagram-zone diagram-zone-runtime" x="770" y="40" width="326" height="880" rx="12"></rect>
|
|
<text class="diagram-zone-title" x="792" y="74">Workers, edge, and workloads</text>
|
|
|
|
<g class="diagram-node node-accent-blue" transform="translate(54 100)">
|
|
<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 208)">
|
|
<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 318)">
|
|
<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">runs validated deploys</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-red" transform="translate(54 430)">
|
|
<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 542)">
|
|
<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 92)">
|
|
<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">manual infra apply path</text>
|
|
<text class="diagram-small" x="18" y="68">apps command for CI deploys</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-orange" transform="translate(412 202)">
|
|
<rect width="280" height="96" rx="8"></rect>
|
|
<text x="18" y="28">PXE + preseed service</text>
|
|
<text class="diagram-small" x="18" y="52">dnsmasq TFTP, nginx HTTP</text>
|
|
<text class="diagram-small" x="18" y="70">Debian 13 arm64 netboot</text>
|
|
<text class="diagram-small" x="18" y="88">golden-node prep scripts</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-blue" transform="translate(412 326)">
|
|
<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 and control loops</text>
|
|
<text class="diagram-small" x="18" y="68">workloads pushed to app workers</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-teal" transform="translate(412 438)">
|
|
<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 550)">
|
|
<rect width="280" height="82" rx="8"></rect>
|
|
<text x="18" y="27">GitOps + policy controllers</text>
|
|
<text class="diagram-small" x="18" y="50">Argo CD and Kyverno</text>
|
|
<text class="diagram-small" x="18" y="68">pinned to app workers</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-purple" transform="translate(412 662)">
|
|
<rect width="280" height="82" rx="8"></rect>
|
|
<text x="18" y="27">Monitoring stack</text>
|
|
<text class="diagram-small" x="18" y="50">Prometheus, Grafana, Loki</text>
|
|
<text class="diagram-small" x="18" y="68">Promtail, node-exporter, KSM</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-orange" transform="translate(412 774)">
|
|
<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 PVs</text>
|
|
<text class="diagram-small" x="18" y="68">Gitea dumps and monitoring data</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-blue" transform="translate(800 92)">
|
|
<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 218)">
|
|
<rect width="258" height="82" rx="8"></rect>
|
|
<text x="18" y="27">Tailscale + edge routes</text>
|
|
<text class="diagram-small" x="18" y="50">30080 website, 30081 demos</text>
|
|
<text class="diagram-small" x="18" y="68">3000 Gitea on Raspberry Pi</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-green" transform="translate(800 330)">
|
|
<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">external Gitea Docker service</text>
|
|
<text class="diagram-small" x="18" y="70">optional edge-app worker</text>
|
|
<text class="diagram-small" x="18" y="88">repo home and backup source</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-red" transform="translate(800 466)">
|
|
<rect width="258" height="112" rx="8"></rect>
|
|
<text x="18" y="28">Orange Pi 5 Plus Pimox</text>
|
|
<text class="diagram-small" x="18" y="52">pimox-worker app nodes</text>
|
|
<text class="diagram-small" x="18" y="70">workers on nvme_thin_pool</text>
|
|
<text class="diagram-small" x="18" y="88">Argo CD, Kyverno, apps</text>
|
|
<text class="diagram-small" x="18" y="106">idempotent qm automation</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-orange" transform="translate(800 616)">
|
|
<rect width="258" height="112" rx="8"></rect>
|
|
<text x="18" y="28">OpenWrt firewall VM</text>
|
|
<text class="diagram-small" x="18" y="52">VM 9050, opt-in only</text>
|
|
<text class="diagram-small" x="18" y="70">vmbr0 WAN, vmbr1 LAN</text>
|
|
<text class="diagram-small" x="18" y="88">simple firewall path</text>
|
|
<text class="diagram-small" x="18" y="106">DHCP optional, VLANs later</text>
|
|
</g>
|
|
|
|
<g class="diagram-node node-accent-teal" transform="translate(800 766)">
|
|
<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 app workers</text>
|
|
</g>
|
|
|
|
<path class="diagram-link" d="M174 174 L174 208"></path>
|
|
<path class="diagram-link" d="M174 290 L174 318"></path>
|
|
<path class="diagram-link" d="M174 400 L174 430"></path>
|
|
<path class="diagram-link" d="M174 512 L174 542"></path>
|
|
<path class="diagram-link" d="M294 577 C346 577 352 133 412 133"></path>
|
|
<path class="diagram-link" d="M294 577 C372 577 722 807 800 807"></path>
|
|
<path class="diagram-link" d="M294 359 C340 359 360 133 412 133"></path>
|
|
<path class="diagram-link" d="M294 249 C348 249 360 479 412 479"></path>
|
|
<path class="diagram-link" d="M552 174 L552 202"></path>
|
|
<path class="diagram-link" d="M552 298 L552 326"></path>
|
|
<path class="diagram-link" d="M552 520 L552 550"></path>
|
|
<path class="diagram-link" d="M552 632 L552 662"></path>
|
|
<path class="diagram-link" d="M552 744 L552 774"></path>
|
|
<path class="diagram-link" d="M692 250 C746 250 746 522 800 522"></path>
|
|
<path class="diagram-link" d="M692 133 C748 133 748 672 800 672"></path>
|
|
<path class="diagram-link" d="M692 591 C748 591 744 378 800 378"></path>
|
|
<path class="diagram-link" d="M929 188 L929 218"></path>
|
|
<path class="diagram-link" d="M929 300 L929 330"></path>
|
|
<path class="diagram-link" d="M929 766 L929 426"></path>
|
|
|
|
<text class="diagram-link-label" x="184" y="198">push</text>
|
|
<text class="diagram-link-label" x="196" y="312">workflow</text>
|
|
<text class="diagram-link-label" x="200" y="424">scan</text>
|
|
<text class="diagram-link-label" x="205" y="536">build</text>
|
|
<text class="diagram-link-label" x="320" y="124">manual infra</text>
|
|
<text class="diagram-link-label" x="344" y="386">validated Git</text>
|
|
<text class="diagram-link-label" x="562" y="195">serve boot</text>
|
|
<text class="diagram-link-label" x="566" y="320">join path</text>
|
|
<text class="diagram-link-label" x="710" y="494">Pimox template</text>
|
|
<text class="diagram-link-label" x="710" y="626">firewall VM</text>
|
|
<text class="diagram-link-label" x="710" y="420">policy + GitOps</text>
|
|
<text class="diagram-link-label" x="934" y="212">secure tunnel</text>
|
|
<text class="diagram-link-label" x="934" y="322">service traffic</text>
|
|
<text class="diagram-link-label" x="946" y="754">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"
|
|
data-translate data-key="blog_speaker_question"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
|
|
<?php echo $text['blog_speaker_question']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_q1"
|
|
data-en="<?php echo htmlspecialchars($en['blog_q1']); ?>">
|
|
<?php echo $text['blog_q1']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message answer">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_answer"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
|
|
<?php echo $text['blog_speaker_answer']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_a1"
|
|
data-en="<?php echo htmlspecialchars($en['blog_a1']); ?>">
|
|
<?php echo $text['blog_a1']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message question">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_question"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
|
|
<?php echo $text['blog_speaker_question']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_q2"
|
|
data-en="<?php echo htmlspecialchars($en['blog_q2']); ?>">
|
|
<?php echo $text['blog_q2']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message answer">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_answer"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
|
|
<?php echo $text['blog_speaker_answer']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_a2"
|
|
data-en="<?php echo htmlspecialchars($en['blog_a2']); ?>">
|
|
<?php echo $text['blog_a2']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message question">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_question"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
|
|
<?php echo $text['blog_speaker_question']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_q3"
|
|
data-en="<?php echo htmlspecialchars($en['blog_q3']); ?>">
|
|
<?php echo $text['blog_q3']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message answer">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_answer"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
|
|
<?php echo $text['blog_speaker_answer']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_a3"
|
|
data-en="<?php echo htmlspecialchars($en['blog_a3']); ?>">
|
|
<?php echo $text['blog_a3']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message question">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_question"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
|
|
<?php echo $text['blog_speaker_question']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_q4"
|
|
data-en="<?php echo htmlspecialchars($en['blog_q4']); ?>">
|
|
<?php echo $text['blog_q4']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message answer">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_answer"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
|
|
<?php echo $text['blog_speaker_answer']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_a4"
|
|
data-en="<?php echo htmlspecialchars($en['blog_a4']); ?>">
|
|
<?php echo $text['blog_a4']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message question">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_question"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
|
|
<?php echo $text['blog_speaker_question']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_q5"
|
|
data-en="<?php echo htmlspecialchars($en['blog_q5']); ?>">
|
|
<?php echo $text['blog_q5']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message answer">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_answer"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
|
|
<?php echo $text['blog_speaker_answer']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_a5"
|
|
data-en="<?php echo htmlspecialchars($en['blog_a5']); ?>">
|
|
<?php echo $text['blog_a5']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message question">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_question"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
|
|
<?php echo $text['blog_speaker_question']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_q6"
|
|
data-en="<?php echo htmlspecialchars($en['blog_q6']); ?>">
|
|
<?php echo $text['blog_q6']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message answer">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_answer"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
|
|
<?php echo $text['blog_speaker_answer']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_a6"
|
|
data-en="<?php echo htmlspecialchars($en['blog_a6']); ?>">
|
|
<?php echo $text['blog_a6']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message question">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_question"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
|
|
<?php echo $text['blog_speaker_question']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_q7"
|
|
data-en="<?php echo htmlspecialchars($en['blog_q7']); ?>">
|
|
<?php echo $text['blog_q7']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message answer">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_answer"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
|
|
<?php echo $text['blog_speaker_answer']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_a7"
|
|
data-en="<?php echo htmlspecialchars($en['blog_a7']); ?>">
|
|
<?php echo $text['blog_a7']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message question">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_question"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
|
|
<?php echo $text['blog_speaker_question']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_q8"
|
|
data-en="<?php echo htmlspecialchars($en['blog_q8']); ?>">
|
|
<?php echo $text['blog_q8']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message answer">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_answer"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
|
|
<?php echo $text['blog_speaker_answer']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_a8"
|
|
data-en="<?php echo htmlspecialchars($en['blog_a8']); ?>">
|
|
<?php echo $text['blog_a8']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message question">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_question"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
|
|
<?php echo $text['blog_speaker_question']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_q9"
|
|
data-en="<?php echo htmlspecialchars($en['blog_q9']); ?>">
|
|
<?php echo $text['blog_q9']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message answer">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_answer"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
|
|
<?php echo $text['blog_speaker_answer']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_a9"
|
|
data-en="<?php echo htmlspecialchars($en['blog_a9']); ?>">
|
|
<?php echo $text['blog_a9']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message question">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_question"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
|
|
<?php echo $text['blog_speaker_question']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_q10"
|
|
data-en="<?php echo htmlspecialchars($en['blog_q10']); ?>">
|
|
<?php echo $text['blog_q10']; ?>
|
|
</p>
|
|
</article>
|
|
|
|
<article class="message answer">
|
|
<div class="speaker"
|
|
data-translate data-key="blog_speaker_answer"
|
|
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
|
|
<?php echo $text['blog_speaker_answer']; ?>
|
|
</div>
|
|
<p data-translate data-key="blog_a10"
|
|
data-en="<?php echo htmlspecialchars($en['blog_a10']); ?>">
|
|
<?php echo $text['blog_a10']; ?>
|
|
</p>
|
|
</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']); ?>">
|
|
<?php echo $text['blog_stack_title']; ?>
|
|
</h2>
|
|
<ul>
|
|
<?php foreach ($stackKeys as $stackKey): ?>
|
|
<li>
|
|
<span data-translate data-key="<?php echo htmlspecialchars($stackKey); ?>"
|
|
data-en="<?php echo htmlspecialchars($en[$stackKey]); ?>">
|
|
<?php echo $text[$stackKey]; ?>
|
|
</span>
|
|
<?php renderStackSourceLinks($stackKey, $stackSourceLinks, $giteaSourceBase); ?>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</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>
|
|
|
|
<div class="visitor-ideas" id="visitor-ideas">
|
|
<div class="section-heading">
|
|
<p class="section-kicker"
|
|
data-translate data-key="blog_ideas_kicker"
|
|
data-en="<?php echo htmlspecialchars($en['blog_ideas_kicker']); ?>">
|
|
<?php echo $text['blog_ideas_kicker']; ?>
|
|
</p>
|
|
<h3 data-translate data-key="blog_ideas_title"
|
|
data-en="<?php echo htmlspecialchars($en['blog_ideas_title']); ?>">
|
|
<?php echo $text['blog_ideas_title']; ?>
|
|
</h3>
|
|
<p data-translate data-key="blog_ideas_intro"
|
|
data-en="<?php echo htmlspecialchars($en['blog_ideas_intro']); ?>">
|
|
<?php echo $text['blog_ideas_intro']; ?>
|
|
</p>
|
|
</div>
|
|
|
|
<?php if (in_array($ideaStatus, ['thanks', 'invalid', 'slow', 'error'], true)): ?>
|
|
<p class="idea-status idea-status-<?php echo htmlspecialchars($ideaStatus); ?>"
|
|
data-translate data-key="blog_idea_status_<?php echo htmlspecialchars($ideaStatus); ?>"
|
|
data-en="<?php echo htmlspecialchars($en['blog_idea_status_' . $ideaStatus]); ?>">
|
|
<?php echo $text['blog_idea_status_' . $ideaStatus]; ?>
|
|
</p>
|
|
<?php endif; ?>
|
|
|
|
<form class="idea-form" action="save_idea.php" method="post">
|
|
<input type="hidden" name="lang" value="<?php echo htmlspecialchars($lang); ?>">
|
|
<label>
|
|
<span data-translate data-key="blog_ideas_name_label"
|
|
data-en="<?php echo htmlspecialchars($en['blog_ideas_name_label']); ?>">
|
|
<?php echo $text['blog_ideas_name_label']; ?>
|
|
</span>
|
|
<input name="visitor_name" type="text" maxlength="80" autocomplete="name">
|
|
</label>
|
|
<label>
|
|
<span data-translate data-key="blog_ideas_text_label"
|
|
data-en="<?php echo htmlspecialchars($en['blog_ideas_text_label']); ?>">
|
|
<?php echo $text['blog_ideas_text_label']; ?>
|
|
</span>
|
|
<textarea name="visitor_idea" maxlength="600" required rows="4"></textarea>
|
|
</label>
|
|
<label class="idea-honey">
|
|
Website
|
|
<input name="company_site" type="text" tabindex="-1" autocomplete="off">
|
|
</label>
|
|
<button type="submit"
|
|
data-translate data-key="blog_ideas_submit"
|
|
data-en="<?php echo htmlspecialchars($en['blog_ideas_submit']); ?>">
|
|
<?php echo $text['blog_ideas_submit']; ?>
|
|
</button>
|
|
</form>
|
|
|
|
<?php if ($visitorIdeas): ?>
|
|
<div class="visitor-idea-list">
|
|
<h4 data-translate data-key="blog_ideas_recent_title"
|
|
data-en="<?php echo htmlspecialchars($en['blog_ideas_recent_title']); ?>">
|
|
<?php echo $text['blog_ideas_recent_title']; ?>
|
|
</h4>
|
|
<?php foreach ($visitorIdeas as $visitorIdea): ?>
|
|
<article class="visitor-idea-card">
|
|
<p><?php echo nl2br(htmlspecialchars($visitorIdea['idea'])); ?></p>
|
|
<footer>
|
|
<span><?php echo htmlspecialchars($visitorIdea['name']); ?></span>
|
|
<time datetime="<?php echo htmlspecialchars($visitorIdea['created_at']); ?>">
|
|
<?php echo htmlspecialchars(substr($visitorIdea['created_at'], 0, 10)); ?>
|
|
</time>
|
|
</footer>
|
|
</article>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
const OTHER_PAGES = ['/index.php', '/cv.php', '/homelab-tree.php'];
|
|
</script>
|
|
<script src="cv-theme.js"></script>
|
|
<?php require_once __DIR__ . '/partials/translation_ui.php'; ?>
|
|
|
|
</body>
|
|
</html>
|