Add source links to homelab blog

This commit is contained in:
juvdiaz 2026-05-25 19:31:10 -06:00
parent 0ad1018d40
commit e242de3eec
11 changed files with 126 additions and 122 deletions

View File

@ -1,6 +1,5 @@
FROM alpine:3.19
# Install Apache, PHP 8.2, and the SQLite extensions
RUN apk update && apk add --no-cache \
apache2 \
php82 \
@ -11,10 +10,8 @@ RUN apk update && apk add --no-cache \
curl \
shadow
# Symlink php82 to php so scripts run naturally if needed
RUN ln -sf /usr/bin/php82 /usr/bin/php
# Alpine keeps Apache site configs here instead of a2enmod
RUN sed -i 's/#LoadModule rewrite_module/LoadModule rewrite_module/' /etc/apache2/httpd.conf && \
sed -i 's/#LoadModule headers_module/LoadModule headers_module/' /etc/apache2/httpd.conf && \
sed -i 's/DirectoryIndex index.html/DirectoryIndex index.php index.html/' /etc/apache2/httpd.conf && \
@ -27,17 +24,14 @@ RUN sed -i 's/#LoadModule rewrite_module/LoadModule rewrite_module/' /etc/apache
printf '\nPidFile /tmp/httpd.pid\n' >> /etc/apache2/httpd.conf; \
fi
# Copy files directly into Alpine's default web root
COPY . /var/www/localhost/htdocs/
RUN rm -f /var/www/localhost/htdocs/index.html
# Set up the database directory permissions
RUN mkdir -p /var/www/localhost/htdocs/db && \
chown -R apache:apache /var/www/localhost/htdocs/db && \
chmod -R 755 /var/www/localhost/htdocs/db
# Match local user permissions for the runtime user (Alpine uses 'apache' instead of 'www-data')
RUN usermod -u 1000 apache && \
groupmod -g 1000 apache && \
mkdir -p /run/apache2 /var/log/apache2 /tmp/website-lang && \
@ -49,5 +43,4 @@ USER apache
EXPOSE 8080
# Start Apache in the foreground
CMD ["/usr/sbin/httpd", "-D", "FOREGROUND"]

View File

@ -28,7 +28,86 @@ $todoKeys = [
'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);
$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; ?>">
@ -470,50 +549,15 @@ $treeHref = 'homelab-tree.php?lang=' . urlencode($lang);
<?php echo $text['blog_stack_title']; ?>
</h2>
<ul>
<li data-translate data-key="blog_stack_1"
data-en="<?php echo htmlspecialchars($en['blog_stack_1']); ?>">
<?php echo $text['blog_stack_1']; ?>
</li>
<li data-translate data-key="blog_stack_2"
data-en="<?php echo htmlspecialchars($en['blog_stack_2']); ?>">
<?php echo $text['blog_stack_2']; ?>
</li>
<li data-translate data-key="blog_stack_3"
data-en="<?php echo htmlspecialchars($en['blog_stack_3']); ?>">
<?php echo $text['blog_stack_3']; ?>
</li>
<li data-translate data-key="blog_stack_4"
data-en="<?php echo htmlspecialchars($en['blog_stack_4']); ?>">
<?php echo $text['blog_stack_4']; ?>
</li>
<li data-translate data-key="blog_stack_5"
data-en="<?php echo htmlspecialchars($en['blog_stack_5']); ?>">
<?php echo $text['blog_stack_5']; ?>
</li>
<li data-translate data-key="blog_stack_6"
data-en="<?php echo htmlspecialchars($en['blog_stack_6']); ?>">
<?php echo $text['blog_stack_6']; ?>
</li>
<li data-translate data-key="blog_stack_7"
data-en="<?php echo htmlspecialchars($en['blog_stack_7']); ?>">
<?php echo $text['blog_stack_7']; ?>
</li>
<li data-translate data-key="blog_stack_8"
data-en="<?php echo htmlspecialchars($en['blog_stack_8']); ?>">
<?php echo $text['blog_stack_8']; ?>
</li>
<li data-translate data-key="blog_stack_9"
data-en="<?php echo htmlspecialchars($en['blog_stack_9']); ?>">
<?php echo $text['blog_stack_9']; ?>
</li>
<li data-translate data-key="blog_stack_10"
data-en="<?php echo htmlspecialchars($en['blog_stack_10']); ?>">
<?php echo $text['blog_stack_10']; ?>
</li>
<li data-translate data-key="blog_stack_11"
data-en="<?php echo htmlspecialchars($en['blog_stack_11']); ?>">
<?php echo $text['blog_stack_11']; ?>
</li>
<?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>

View File

@ -14,7 +14,6 @@ function storeCvTheme(theme) {
try {
localStorage.setItem(cvThemeStorageKey, theme);
} catch (_error) {
// Theme switching should still work when storage is blocked.
}
}

View File

@ -92,7 +92,6 @@
</section>
<?php
// Tell translation.js to also translate these pages in the background
?>
<script>
const OTHER_PAGES = ['cv.php', 'blog.php'];

View File

@ -4,13 +4,11 @@ return [
'job_title' => 'Site Reliability Developer',
'contacts' => 'Contacts: +52 449 217 6833, juvenaldiaz522@gmail.com',
// Nav
'nav_home' => 'Home',
'nav_cv' => 'CV',
'nav_blog' => 'Blog',
'nav_demos' => 'Demos',
// Index bio
'bio_intro' => 'I work in infrastructure and reliability, focusing on building systems that are stable, scalable, and easy to operate.',
'bio_story_1' => 'My interest in technology started with a simple curiosity about how systems behave — especially when they fail. Over time, that curiosity evolved into working with Linux environments, troubleshooting production systems, and improving how services run at scale.',
'bio_story_2' => "I've spent more than a decade working across cloud platforms and distributed systems. My work has gradually shifted from reactive support to designing and maintaining platforms used by thousands of users, where reliability and clarity matter just as much as performance.",
@ -18,7 +16,6 @@ return [
'bio_cta' => 'For a detailed breakdown of my experience, see my',
'bio_cta_link' => 'CV',
// CV sections
'cv_summary_title' => 'Professional Summary',
'cv_summary' => 'IT Professional with 12+ years of experience, specializing in Linux but also proficient in team management (local and global teams) and user satisfaction. My greatest strength is a sense of urgency which enables me to tackle issues in the most fast and efficient way, always focusing on continuous improvement and service excellence. I also enjoy learning new technologies as required.',
'cv_theme_label' => 'CV theme',
@ -56,7 +53,6 @@ return [
'cv_job7_title' => 'Customer Support Agent Teleperformance | Comcast',
'cv_job7_desc' => 'Provided customer support services taking calls from the US Southwest area to troubleshoot cable, phone, and internet services.',
// Blog
'blog_kicker' => 'Homelab field notes',
'blog_title' => 'I accidentally built a tiny CI/CD platform',
'blog_subtitle' => 'A casual conversation about how a Debian box, a Raspberry Pi, an OCI edge host, and a suspicious amount of stubbornness became a repeatable Kubernetes delivery path.',
@ -128,7 +124,6 @@ return [
'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',
'demos_title' => 'Demo Apps',
'demos_subtitle' => 'A growing shelf of small apps shipped as separate static demo artifacts. The website stays light; each demo gets its own page under /demo-apps/.',

View File

@ -1,35 +1,25 @@
<?php
// Nahuatl (Nawatl) translation
// Classical/Modern Nahuatl — best effort, low-resource language
// CV technical terms are kept in English/Spanish as they have no Nahuatl equivalents
return [
'name' => 'Juvenal Diaz',
'job_title' => 'Tlapixqui Tlahtoa Tlacuilolli', // Guardian of reliable systems
'job_title' => 'Tlapixqui Tlahtoa Tlacuilolli',
'contacts' => 'Tlatemoliztli: +52 449 217 6833, juvenaldiaz522@gmail.com',
// Nav
'nav_home' => 'Nochan', // My home
'nav_cv' => 'Notlahcuilol', // My document/record
'nav_blog' => 'Notlahtol', // My words
'nav_home' => 'Nochan',
'nav_cv' => 'Notlahcuilol',
'nav_blog' => 'Notlahtol',
'nav_demos' => 'Tlayeyecoliztli',
// Index bio
'bio_intro' => 'Nitlatequitia ipan tlatecpanaliztli ihuan tlayeyecoliztli, niquitta in quenin tiquitasque tlapatlaliztli tlayecoliztli tlamantli nemiztli.',
// I work in infrastructure and reliability, seeing how we build stable, scalable systems
'bio_story_1' => 'Notlahtlaniliztli ipan āmantēcayōtl ōpeuh inic niquitta in quenin tlamantli mochihua — oc cequi in quenin polihui. Ic cauitl, in notlahtlaniliztli omochiuh inic nitlatequitia ipan Linux tlamantli, nitlapoa tlaneltoquiliztli, ihuan niquimati in quenin tlatequipanoa tlamantli ipan huey altepetl.',
// My curiosity about technology started by seeing how things work — especially how they fail.
'bio_story_2' => 'Ōnimacoc matlactli xihuitl ihuan achi ic tlatequitia ipan mixtlan tlamantilyotl ihuan nepapan tlamantli. Notequitl ōmoyolcuep in tlapalehuiloni itech inic niquitta ihuan niquimati in tlamantli mochihua ipan miec tlacame, in canin tlayeyecoliztli ihuan tlanextiliztli quinequi iuhqui in quenami tlatequipanoliztli.',
// I have spent a decade working on cloud platforms and distributed systems.
'bio_story_3' => 'Niquitta tlaneltoquiliztli inic niquixehua tlaneltoquiliztli — oc cequi ipan huehcauh tlapatlaliztli — niquitta in quenin ticchihua tlamantli nemiztli, tiquixehua quezqui tlamantli, ihuan ticmati in quenin ahmo mochihua occeppa.',
// I approach problems with urgency — also focusing on long-term improvement.
'bio_cta' => 'Inic ticita notequitl moch, xiquitta',
'bio_cta_link' => 'Notlahcuilol',
// CV sections
'cv_summary_title' => 'Notequitl Tlahcuilolli',
'cv_summary' => 'Tlapixqui āmantēcayōtl inic matlactli omome xihuitl, motemachtia Linux ihuan quimatia tlatecpanaliztli (ipan altepetl ihuan tlalpan) ihuan tlahtoa tlacame. Nohueyitequitl ic tlaneltoquiliztli niquixehua tlaneltoquiliztli inic achi ic niquichihua, moch ica tlapatlaliztli ihuan tlatequipanoliztli. Nixpampa nimati āmantēcayōtl yancuic quenin monequi.',
'cv_theme_label' => 'CV theme',
@ -67,7 +57,6 @@ return [
'cv_job7_title' => 'Tlapalehuiani Tlacame Teleperformance | Comcast',
'cv_job7_desc' => 'Nitlapalehua tlacame ipan US inic cable, tepoztli, ihuan tlahtoa tlamantli.',
// Blog
'blog_kicker' => 'Homelab tlahcuilolli',
'blog_title' => 'Tlatecpanaliztli homelab CI/CD pipeline',
'blog_subtitle' => 'Ce tlahtolli in quenin Debian server, Raspberry Pi, ihuan OCI edge box mochihua ce Kubernetes tlatequipanoliztli.',
@ -100,7 +89,6 @@ return [
'blog_stack_10' => 'ML demos monequi client-side Wasm/ONNX/Transformers.js, ahmo server-side jobs.',
'blog_stack_11' => 'Demo code axcan quichihua demos-static image ihuan Argo CD app, exposed ipan /demo-apps/. PHP website zan catalog.',
// Demos
'demos_kicker' => 'Tepiton tools ipan browser',
'demos_title' => 'Demo Apps',
'demos_subtitle' => 'Tepiton apps ipan separate static demo artifacts. Website mocahua light; demos cateh ipan /demo-apps/.',

View File

@ -1,7 +1,4 @@
<?php
// lang_helper.php
// Include this at the top of every page.
// Provides: $lang, $text, $en, $availableLangs
$staticLangDir = __DIR__ . '/lang';
$runtimeLangDir = getenv('WEBSITE_LANG_WRITE_DIR') ?: null;
@ -34,6 +31,5 @@ if (!file_exists($file)) {
$file = "$staticLangDir/nah.php";
}
// Always load English as translation source
$en = include "$staticLangDir/en.php";
$text = array_replace($en, include $file);

View File

@ -1,7 +1,4 @@
<?php
// partials/translation_ui.php
// Include at the bottom of every page just before </body>
// Requires: $lang, $availableLangs — provided by lang_helper.php
?>
<div id="translate-bar" style="

View File

@ -1,7 +1,4 @@
<?php
// save_lang.php
// Receives: { "lang": "fr", "translations": { "nav_home": "Accueil", ... } }
// Merges with en.php base so all keys are always present, then saves lang/fr.php
header('Content-Type: application/json');
@ -28,17 +25,14 @@ if (strlen($lang) < 2 || strlen($lang) > 5) {
exit;
}
// Load English as base — ensures every key exists even if not translated
$base = include __DIR__ . '/lang/en.php';
// Overwrite only keys that were translated
foreach ($translations as $key => $value) {
if (array_key_exists($key, $base)) {
$base[$key] = $value;
}
}
// Build PHP file content
$lines = ["<?php", "return ["];
foreach ($base as $key => $value) {
$key = addslashes($key);

View File

@ -14,7 +14,6 @@ body {
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
/* Hero Section Styles */
.hero-section {
display: flex;
align-items: center;
@ -72,7 +71,6 @@ body {
color: #333;
}
/* Welcome Section */
.welcome {
text-align: center;
margin-bottom: 40px;
@ -115,7 +113,6 @@ body {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
/* Overview Grid */
.overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
@ -144,7 +141,6 @@ body {
line-height: 1.5;
}
/* Responsive Design */
@media (max-width: 768px) {
body {
margin: 20px;
@ -386,6 +382,39 @@ body {
line-height: 1.55;
}
.source-links {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-top: 10px;
}
.source-links span {
color: #627d98;
font-size: 0.82rem;
font-weight: 800;
}
.source-links a {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 9px;
border: 1px solid #bcccdc;
border-radius: 999px;
background: #f8fbff;
color: #004085;
font-size: 0.82rem;
font-weight: 800;
text-decoration: none;
}
.source-links a:hover {
border-color: #004085;
background: #eef5ff;
}
.architecture-section,
.activity-log,
.homelab-todo,
@ -895,7 +924,6 @@ body {
}
}
/* CV theme toggle */
.cv-theme-toolbar {
max-width: 900px;
margin: 0 auto 18px;
@ -1152,7 +1180,6 @@ body.home-page.cv-fancy {
}
}
/* Demos page */
.demos-page {
max-width: 1040px;
margin: 0 auto;

View File

@ -1,9 +1,3 @@
// translation.js
// Shared translation logic for all pages.
// Requires these vars injected by partials/translation_ui.php before this file loads:
// OLLAMA_HOST, OLLAMA_MODEL, SAVE_URL, STATIC_LANGS, CURRENT_LANG
// Optional per-page var:
// OTHER_PAGES — array of URLs to also translate in background (e.g. ['/cv.php'])
const LANG_NAMES = {
en: 'English', es: 'Spanish', hu: 'Hungarian',
@ -37,7 +31,6 @@ function hidePrompt() {
if (prompt) prompt.style.display = 'none';
}
// Send all texts in one Ollama request, get back a JSON array
async function translateBatch(texts, targetLang) {
const name = LANG_NAMES[targetLang] || targetLang;
const prompt = `Translate each item to ${name}.
@ -69,7 +62,6 @@ Input: ${JSON.stringify(texts)}`;
return result;
}
// Save translated keys — merges with en.php base on the server
async function saveLang(lang, translations) {
try {
const res = await fetch(SAVE_URL, {
@ -88,8 +80,6 @@ async function saveLang(lang, translations) {
}
}
// Collect translations from any document's [data-translate] elements
// Always reads data-en (English source) regardless of displayed language
async function collectPageTranslations(doc, targetLang) {
const elements = [...doc.querySelectorAll('[data-translate]')];
if (!elements.length) return {};
@ -105,7 +95,6 @@ async function collectPageTranslations(doc, targetLang) {
return result;
}
// Main translation flow — triggered by button click
async function doTranslation() {
hidePrompt();
if (btn) btn.disabled = true;
@ -118,8 +107,6 @@ async function doTranslation() {
const allTranslations = {};
// Step 1 — batch translate current page elements live on screen
// Always uses data-en (English) as source, not whatever is displayed
try {
const texts = elements.map(el => el.getAttribute('data-en') || el.textContent.trim());
const translated = await translateBatch(texts, browserLang);
@ -138,7 +125,6 @@ async function doTranslation() {
elements.forEach(el => el.style.opacity = '1');
// Step 2 — fetch and batch translate OTHER_PAGES in virtual DOM
if (typeof OTHER_PAGES !== 'undefined' && OTHER_PAGES.length > 0) {
for (const url of OTHER_PAGES) {
showBadge(`Translating ${url}...`);
@ -155,25 +141,13 @@ async function doTranslation() {
}
}
// Step 3 — save everything in one call
await saveLang(browserLang, allTranslations);
showBadge(`Translated by Ollama / ${OLLAMA_MODEL}`);
}
// Decide what to show based on three cases:
//
// Case 1 — Page is Nahuatl AND browser is not Nahuatl
// → "Switch to English" button (redirect, no Ollama)
//
// Case 2 — Browser lang already has a static file
// → nothing shown (PHP already served it)
//
// Case 3 — Browser lang is unsupported
// → "Translate to <lang>" button (calls Ollama, saves file)
function initTranslation() {
// Case 1 — Nahuatl default page, non-Nahuatl visitor
if (CURRENT_LANG === 'nah' && browserLang !== 'nah') {
if (actionSpan) actionSpan.textContent = 'Switch to';
if (detectedSpan) detectedSpan.textContent = 'English';
@ -184,11 +158,9 @@ function initTranslation() {
return;
}
// Case 2 — already supported, PHP served it
if (STATIC_LANGS.includes(browserLang)) return;
// Case 3 — unsupported lang, offer Ollama translation
if (browserLang === 'nah') return; // actual Nahuatl browser, nothing to do
if (browserLang === 'nah') return;
if (actionSpan) actionSpan.textContent = 'Translate to';
if (detectedSpan) detectedSpan.textContent = langName;
if (prompt) prompt.style.display = 'block';