678 lines
28 KiB
PHP
678 lines
28 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',
|
|
];
|
|
|
|
$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',
|
|
];
|
|
|
|
$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',
|
|
];
|
|
|
|
$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'],
|
|
],
|
|
'blog_stack_2' => [
|
|
['label' => 'cluster/main.tf', 'path' => 'bootstrap/cluster/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' => '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' => 'cv-theme.js', 'path' => 'apps/website/cv-theme.js'],
|
|
['label' => 'cv.php', 'path' => 'apps/website/cv.php'],
|
|
['label' => 'styles.css', 'path' => 'apps/website/styles.css'],
|
|
],
|
|
'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' => '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'],
|
|
],
|
|
];
|
|
|
|
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>
|
|
|
|
<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>
|
|
|
|
<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="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"
|
|
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>
|
|
</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>
|
|
<?php require_once __DIR__ . '/partials/translation_ui.php'; ?>
|
|
|
|
</body>
|
|
</html>
|