feat: initial homelab configuration layout

This commit is contained in:
jv 2026-05-18 00:30:47 -05:00
parent d72d8f385d
commit 7ae96b4654
28 changed files with 1452 additions and 1 deletions

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
# Ignore OpenTofu / Terraform state and backups
*.tfstate
*.tfstate.backup
.terraform/
.terraform.lock.hcl
# Ignore local archive dumps and backups
*.tar
*.zip
gittea/gittea-docker-backup
# Ignore older source iterations
*.old

135
argocd/main.tf Normal file
View File

@ -0,0 +1,135 @@
resource "kubernetes_namespace" "argocd" {
metadata {
name = "argocd"
}
}
data "http" "argocd_manifest" {
url = "https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml"
}
resource "kubernetes_manifest" "argocd_core" {
for_each = { for idx, doc in provider::kubernetes::manifest_decode_multi(data.http.argocd_manifest.response_body) : idx => doc }
manifest = merge(
each.value,
contains(["ClusterRole", "ClusterRoleBinding", "CustomResourceDefinition", "Namespace"], lookup(each.value, "kind", "")) ? {} : {
metadata = merge(
try(each.value.metadata, {}),
{
namespace = kubernetes_namespace.argocd.metadata[0].name
}
)
},
lookup(each.value, "kind", "") == "Service" && lookup(try(each.value.metadata, {}), "name", "") == "argocd-server" ? {
spec = merge(
try(each.value.spec, {}),
{
type = "NodePort"
ports = [
{
name = "http"
port = 80
protocol = "TCP"
targetPort = 8080
nodePort = 30501
},
{
name = "https"
port = 443
protocol = "TCP"
targetPort = 8080
}
]
}
)
} : {}
)
field_manager {
force_conflicts = true
}
depends_on = [kubernetes_namespace.argocd]
}
resource "kubernetes_secret_v1" "argocd_private_repo" {
metadata {
name = "my-homelab-repo-secret"
namespace = kubernetes_namespace.argocd.metadata[0].name
labels = {
"argocd.argoproj.io/secret-type" = "repository"
}
}
data = {
type = "git"
url = "http://192.168.100.68:30300/jv/my-homelab-configs"
username = "jv"
password = "Summer12#$"
}
depends_on = [kubernetes_manifest.argocd_core]
}
resource "kubernetes_manifest" "argocd_app_registry" {
manifest = {
apiVersion = "argoproj.io/v1alpha1"
kind = "Application"
metadata = {
name = "container-registry"
namespace = "argocd"
}
spec = {
project = "default"
source = {
repoURL = "http://192.168.100.68:30300/jv/my-homelab-configs"
targetRevision = "HEAD"
path = "container-registry" # Points to the folder containing your registry YAMLs
}
destination = {
server = "https://kubernetes.default.svc"
namespace = "container-registry" # Deploys into this namespace
}
syncPolicy = {
automated = {
prune = true
selfHeal = true
}
syncOptions = ["CreateNamespace=true"]
}
}
}
depends_on = [kubernetes_manifest.argocd_core]
}
resource "kubernetes_manifest" "argocd_app_web_app" {
manifest = {
apiVersion = "argoproj.io/v1alpha1"
kind = "Application"
metadata = {
name = "php-web-app"
namespace = "argocd"
}
spec = {
project = "default"
source = {
repoURL = "http://192.168.100.68:30300/jv/my-homelab-configs"
targetRevision = "HEAD"
path = "web-app" # ArgoCD ignores the PHP/Docker files and grabs web-app.yaml
}
destination = {
server = "https://kubernetes.default.svc"
namespace = "default"
}
syncPolicy = {
automated = {
prune = true
selfHeal = true
}
syncOptions = ["CreateNamespace=true"]
}
}
}
depends_on = [kubernetes_manifest.argocd_core]
}

13
argocd/providers.tf Normal file
View File

@ -0,0 +1,13 @@
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.24"
}
}
}
provider "kubernetes" {
config_path = "~/.kube/config"
config_context = "kubernetes-admin@kubernetes"
}

View File

@ -0,0 +1,43 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: local-registry
namespace: container-registry
labels:
app: local-registry
spec:
replicas: 1
selector:
matchLabels:
app: local-registry
template:
metadata:
labels:
app: local-registry
spec:
containers:
- name: registry
image: registry:2
ports:
- containerPort: 5000
volumeMounts:
- name: registry-vol
mountPath: /var/lib/registry
volumes:
- name: registry-vol
persistentVolumeClaim:
claimName: registry-pvc
---
apiVersion: v1
kind: Service
metadata:
name: local-registry-svc
namespace: container-registry
spec:
type: NodePort
ports:
- port: 5000
targetPort: 5000
nodePort: 30500
selector:
app: local-registry

View File

@ -0,0 +1,27 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: registry-pv
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/home/k8s-storage/registry-data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: registry-pvc
namespace: container-registry
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi

View File

@ -0,0 +1,33 @@
# Pull down the Gitea image locally via the Pi's Docker engine
resource "docker_image" "gitea_backup" {
name = "gitea/gitea:1.21.7"
keep_locally = true
}
# Fire up the standalone container wrapper
resource "docker_container" "gitea_backup" {
name = "gitea-backup"
image = docker_image.gitea_backup.image_id
restart = "always"
env = [
"USER_UID=1000",
"USER_GID=1000",
"GITEA__database__DB_TYPE=sqlite3"
]
ports {
internal = 3000
external = 3000
}
ports {
internal = 22
external = 2222
}
volumes {
host_path = "/home/pi/gitea-backup-data"
container_path = "/data"
}
}

18
gittea/host_prep.tf Normal file
View File

@ -0,0 +1,18 @@
resource "null_resource" "node_prep" {
for_each = var.nodes
connection {
type = "ssh"
user = each.value.user
host = each.value.ip
private_key = file(var.ssh_key_path)
}
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y open-iscsi util-linux",
"sudo systemctl enable --now iscsid"
]
}
}

134
gittea/k8s_gitea.tf Normal file
View File

@ -0,0 +1,134 @@
resource "kubernetes_namespace" "gitea" {
metadata {
name = "gitea-system"
}
}
resource "null_resource" "install_longhorn" {
depends_on = [null_resource.node_prep]
provisioner "local-exec" {
command = "kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.6.2/deploy/longhorn.yaml"
}
}
resource "kubernetes_persistent_volume_claim" "gitea_pvc" {
depends_on = [null_resource.install_longhorn]
metadata {
name = "gitea-pvc"
namespace = kubernetes_namespace.gitea.metadata[0].name
}
spec {
access_modes = ["ReadWriteOnce"]
storage_class_name = "longhorn"
resources {
requests = {
storage = "10Gi"
}
}
}
}
resource "kubernetes_deployment" "gitea_prod" {
metadata {
name = "gitea-prod"
namespace = kubernetes_namespace.gitea.metadata[0].name
}
spec {
replicas = 1
selector {
match_labels = {
app = "gitea"
}
}
template {
metadata {
labels = {
app = "gitea"
}
}
spec {
container {
name = "gitea"
image = "gitea/gitea:1.21.7"
port {
container_port = 3000
name = "http"
}
port {
container_port = 22
name = "ssh"
}
env {
name = "USER_UID"
value = "1000"
}
env {
name = "USER_GID"
value = "1000"
}
env {
name = "GITEA__database__DB_TYPE"
value = "sqlite3"
}
env {
name = "GITEA__repository__ENABLE_PUSH_MIRROR"
value = "true"
}
env {
name = "GITEA__repository__ENABLE_PUSH_MIRROR"
value = "true"
}
# ADD THIS NEW BLOCK:
env {
name = "GITEA__migrations__ALLOW_LOCALNETWORKS"
value = "true"
}
volume_mount {
mount_path = "/data"
name = "gitea-storage"
}
}
volume {
name = "gitea-storage"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.gitea_pvc.metadata[0].name
}
}
}
}
}
}
resource "kubernetes_service" "gitea_service" {
metadata {
name = "gitea-service"
namespace = kubernetes_namespace.gitea.metadata[0].name
}
spec {
type = "NodePort"
selector = {
app = "gitea"
}
port {
name = "http"
port = 3000
target_port = 3000
node_port = 30300
}
port {
name = "ssh"
port = 22
target_port = 22
node_port = 32222
}
}
}

31
gittea/providers.tf Normal file
View File

@ -0,0 +1,31 @@
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.2"
}
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.23"
}
}
}
# Core Cluster API access
provider "kubernetes" {
config_path = "~/.kube/config"
config_context = "kubernetes-admin@kubernetes"
}
# Remote Docker control over the Raspberry Pi
provider "docker" {
host = "ssh://jv@192.168.100.89:22"
ssh_opts = [
"-o", "StrictHostKeyChecking=no",
"-i", var.ssh_key_path
]
}

26
gittea/variables.tf Normal file
View File

@ -0,0 +1,26 @@
variable "home_dir" {
type = string
default = "/home/jv"
}
variable "ssh_key_path" {
type = string
default = "/home/jv/.ssh/id_ed25519"
}
variable "nodes" {
type = map(object({
ip = string
user = string
}))
default = {
raspberrypi = {
ip = "192.168.100.89"
user = "jv"
}
debian_laptop = {
ip = "192.168.100.68"
user = "jv"
}
}
}

1
test
View File

@ -1 +0,0 @@
test

36
web-app/Dockerfile Normal file
View File

@ -0,0 +1,36 @@
FROM alpine:3.19
# Install Apache, PHP 8.2, and the SQLite extensions
RUN apk update && apk add --no-cache \
apache2 \
php82 \
php82-apache2 \
php82-pdo \
php82-pdo_sqlite \
php82-curl \
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
# Copy files directly into Alpine's default web root
COPY . /var/www/localhost/htdocs/
# 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
EXPOSE 80
# Start Apache in the foreground
CMD ["/usr/sbin/httpd", "-D", "FOREGROUND"]

7
web-app/buildkitd.toml Normal file
View File

@ -0,0 +1,7 @@
[registry."host.docker.internal:30500"]
http = true
insecure = true
[registry."192.168.100.68:30500"]
http = true
insecure = true

134
web-app/cv.php Normal file
View File

@ -0,0 +1,134 @@
<?php require_once __DIR__ . '/lang_helper.php'; ?>
<!DOCTYPE html>
<html lang="<?php echo $lang; ?>">
<head>
<meta charset="UTF-8">
<title>CV - <?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="cv.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="#"
data-translate data-key="nav_blog"
data-en="<?php echo htmlspecialchars($en['nav_blog']); ?>">
<?php echo $text['nav_blog']; ?>
</a>
</div>
</nav>
<div class="container">
<h1><?php echo $text['name']; ?></h1>
<p data-translate data-key="job_title"
data-en="<?php echo htmlspecialchars($en['job_title']); ?>">
<?php echo $text['job_title']; ?>
</p>
<p><?php echo $text['contacts']; ?></p>
<h2 data-translate data-key="cv_summary_title"
data-en="<?php echo htmlspecialchars($en['cv_summary_title']); ?>">
<?php echo $text['cv_summary_title']; ?>
</h2>
<p data-translate data-key="cv_summary"
data-en="<?php echo htmlspecialchars($en['cv_summary']); ?>">
<?php echo $text['cv_summary']; ?>
</p>
<h2 data-translate data-key="cv_employment_title"
data-en="<?php echo htmlspecialchars($en['cv_employment_title']); ?>">
<?php echo $text['cv_employment_title']; ?>
</h2>
<p><strong><?php echo $text['cv_job1_period']; ?></strong></p>
<p><strong data-translate data-key="cv_job1_title"
data-en="<?php echo htmlspecialchars($en['cv_job1_title']); ?>">
<?php echo $text['cv_job1_title']; ?>
</strong></p>
<p data-translate data-key="cv_job1_desc"
data-en="<?php echo htmlspecialchars($en['cv_job1_desc']); ?>">
<?php echo $text['cv_job1_desc']; ?>
</p>
<p><strong><?php echo $text['cv_job2_period']; ?></strong></p>
<p><strong data-translate data-key="cv_job2_title"
data-en="<?php echo htmlspecialchars($en['cv_job2_title']); ?>">
<?php echo $text['cv_job2_title']; ?>
</strong></p>
<p data-translate data-key="cv_job2_desc"
data-en="<?php echo htmlspecialchars($en['cv_job2_desc']); ?>">
<?php echo $text['cv_job2_desc']; ?>
</p>
<p><strong><?php echo $text['cv_job3_period']; ?></strong></p>
<p><strong data-translate data-key="cv_job3_title"
data-en="<?php echo htmlspecialchars($en['cv_job3_title']); ?>">
<?php echo $text['cv_job3_title']; ?>
</strong></p>
<p data-translate data-key="cv_job3_desc"
data-en="<?php echo htmlspecialchars($en['cv_job3_desc']); ?>">
<?php echo $text['cv_job3_desc']; ?>
</p>
<p><strong><?php echo $text['cv_job4_period']; ?></strong></p>
<p><strong data-translate data-key="cv_job4_title"
data-en="<?php echo htmlspecialchars($en['cv_job4_title']); ?>">
<?php echo $text['cv_job4_title']; ?>
</strong></p>
<p data-translate data-key="cv_job4_desc"
data-en="<?php echo htmlspecialchars($en['cv_job4_desc']); ?>">
<?php echo $text['cv_job4_desc']; ?>
</p>
<p><strong><?php echo $text['cv_job5_period']; ?></strong></p>
<p><strong data-translate data-key="cv_job5_title"
data-en="<?php echo htmlspecialchars($en['cv_job5_title']); ?>">
<?php echo $text['cv_job5_title']; ?>
</strong></p>
<p data-translate data-key="cv_job5_desc"
data-en="<?php echo htmlspecialchars($en['cv_job5_desc']); ?>">
<?php echo $text['cv_job5_desc']; ?>
</p>
<p><strong><?php echo $text['cv_job6_period']; ?></strong></p>
<p><strong data-translate data-key="cv_job6_title"
data-en="<?php echo htmlspecialchars($en['cv_job6_title']); ?>">
<?php echo $text['cv_job6_title']; ?>
</strong></p>
<p data-translate data-key="cv_job6_desc"
data-en="<?php echo htmlspecialchars($en['cv_job6_desc']); ?>">
<?php echo $text['cv_job6_desc']; ?>
</p>
<p><strong><?php echo $text['cv_job7_period']; ?></strong></p>
<p><strong data-translate data-key="cv_job7_title"
data-en="<?php echo htmlspecialchars($en['cv_job7_title']); ?>">
<?php echo $text['cv_job7_title']; ?>
</strong></p>
<p data-translate data-key="cv_job7_desc"
data-en="<?php echo htmlspecialchars($en['cv_job7_desc']); ?>">
<?php echo $text['cv_job7_desc']; ?>
</p>
</div>
<?php require_once __DIR__ . '/partials/translation_ui.php'; ?>
</body>
</html>

BIN
web-app/gnu.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
web-app/images/jv.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
web-app/images/profile.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

81
web-app/index.php Normal file
View File

@ -0,0 +1,81 @@
<?php require_once __DIR__ . '/lang_helper.php'; ?>
<!DOCTYPE html>
<html lang="<?php echo $lang; ?>">
<head>
<meta charset="UTF-8">
<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="index.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="#"
data-translate data-key="nav_blog"
data-en="<?php echo htmlspecialchars($en['nav_blog']); ?>">
<?php echo $text['nav_blog']; ?>
</a>
</div>
</nav>
<section class="hero">
<div class="hero-text">
<h1><?php echo $text['name']; ?></h1>
<p class="bio-intro"
data-translate data-key="bio_intro"
data-en="<?php echo htmlspecialchars($en['bio_intro']); ?>">
<?php echo $text['bio_intro']; ?>
</p>
<p class="bio-story"
data-translate data-key="bio_story_1"
data-en="<?php echo htmlspecialchars($en['bio_story_1']); ?>">
<?php echo $text['bio_story_1']; ?>
</p>
<p class="bio-story"
data-translate data-key="bio_story_2"
data-en="<?php echo htmlspecialchars($en['bio_story_2']); ?>">
<?php echo $text['bio_story_2']; ?>
</p>
<p class="bio-story"
data-translate data-key="bio_story_3"
data-en="<?php echo htmlspecialchars($en['bio_story_3']); ?>">
<?php echo $text['bio_story_3']; ?>
</p>
<p class="cta">
<span data-translate data-key="bio_cta"
data-en="<?php echo htmlspecialchars($en['bio_cta']); ?>">
<?php echo $text['bio_cta']; ?>
</span>
<a href="cv.php?lang=<?php echo $lang; ?>"><?php echo $text['bio_cta_link']; ?></a>.
</p>
</div>
<div class="hero-image">
<img src="images/profile.webp" alt="profile">
</div>
</section>
<?php
// Tell translation.js to also translate these pages in the background
?>
<script>
const OTHER_PAGES = ['/cv.php'];
</script>
<?php require_once __DIR__ . '/partials/translation_ui.php'; ?>
</body>
</html>

53
web-app/lang/en.php Normal file
View File

@ -0,0 +1,53 @@
<?php
return [
'name' => 'Juvenal Diaz',
'job_title' => 'Site Reliability Developer',
'contacts' => 'Contacts: +52 449 217 6833, juvenaldiaz522@gmail.com',
// Nav
'nav_home' => 'Home',
'nav_cv' => 'CV',
'nav_blog' => 'Blog',
// 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.",
'bio_story_3' => 'I tend to approach problems with a strong sense of urgency, but also with a focus on long-term improvement — removing friction, simplifying systems, and preventing issues from recurring.',
'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_employment_title' => 'Employment History / Activities',
'cv_job1_period' => 'Aug 2024 → Current',
'cv_job1_title' => 'Site Reliability Developer Oracle | Spectra',
'cv_job1_desc' => 'Manage a platform as a service (PaaS) that allows developers to build, run, and operate applications in a cloud environment, this service is used by 20,000+ users from internal development teams, it is based on Kubernetes / Terraform. Daily activities include planned maintenance of the platform, emergency changes, continuous improvement of internal tooling and documentation creation.',
'cv_job2_period' => 'June 2022 → July 2024',
'cv_job2_title' => 'Site Reliability Developer Oracle | Analytics',
'cv_job2_desc' => 'Attend incidents for Oracle Analytics Cloud reported through Jira for 10,000+ external customers, related to general usage, Linux troubleshooting, SQL query tuning, and services/jobs configuration. Development of internal automation tools using Bash, Python, Ansible, and REST APIs in Bitbucket. SOP update and creation, working in a Scrum/Agile environment leading Continuous Improvement and Automation Epics. Top performer (Low TTM). Part of the onboarding team for new hires. Proposed on-call rotation improvement initiative (vNext).',
'cv_job3_period' => 'July 2021 → June 2022',
'cv_job3_title' => 'Linux Support Engineer - Rackspace',
'cv_job3_desc' => 'Attend incidents reported through phone calls and internal ticketing systems for several clients related to troubleshooting Linux, MySQL, Apache, NGINX, Varnish, PHP, VMware, DoS attacks, Storage, Backups, Firewalls, etc. Top performer (number of cases/tickets solved) of the MX and US team. Part of the onboarding team for new hires.',
'cv_job4_period' => 'March 2020 → July 2021',
'cv_job4_title' => 'Linux Support Engineer - Softtek | Electronic Arts',
'cv_job4_desc' => 'Provide infrastructure support for a PCI-compliant platform that handles 4M+ requests per minute with 30+ microservices using containers and orchestration technologies, using DevOps practices. Alerts creation and tuning.',
'cv_job5_period' => 'August 2017 → March 2020',
'cv_job5_title' => 'Cross Functional Manager - Softtek | Electronic Arts',
'cv_job5_desc' => 'Incident, Problem, Asset Management, and Automation (ITIL-based) process implementation, Continuous Improvement Assessments.',
'cv_job6_period' => 'September 2015 → August 2017',
'cv_job6_title' => 'Linux Support Engineer / Tech Lead - Softtek | General Electric',
'cv_job6_desc' => 'Incident, Change management, and monitoring for internal applications. Promoted to tech lead after one year in support position.',
'cv_job7_period' => 'February 2013 → August 2015',
'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.',
];

64
web-app/lang/nah.php Normal file
View File

@ -0,0 +1,64 @@
<?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
'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
// 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_employment_title' => 'Notequitl Tlahcuilolli / Tlatequipanoliztli',
'cv_job1_period' => 'Ago 2024 → Axcan',
'cv_job1_title' => 'Tlapixqui Tlahtoa Tlacuilolli Oracle | Spectra',
'cv_job1_desc' => 'Nitlapiya ce tlamantli inic tlatequipanoliztli (PaaS) in quimatia tlatecpanime inic quichihuasque, quimochihuiltisque ihuan quipixque tlamantli ipan mixtlan. In tlatequipanoliztli quimatia matlactli ompoalli tlamantilyotl ipan Kubernetes / Terraform. Cemilhuitl tlatequipanoliztli: tlachihualiztli, tlapatlaliztli, ihuan tlahcuilolli.',
'cv_job2_period' => 'Junio 2022 → Julio 2024',
'cv_job2_title' => 'Tlapixqui Tlahtoa Tlacuilolli Oracle | Analytics',
'cv_job2_desc' => 'Nitlatoa tlaneltoquiliztli ipan Oracle Analytics Cloud inic matlactli tlamantilyotl tlacame, Linux, SQL, ihuan tlatequipanoliztli. Nitlachihua tlamantli inic Bash, Python, Ansible, ihuan REST APIs ipan Bitbucket. Scrum/Agile tlatequipanoliztli.',
'cv_job3_period' => 'Julio 2021 → Junio 2022',
'cv_job3_title' => 'Linux Tlapalehuiani - Rackspace',
'cv_job3_desc' => 'Nitlatoa tlaneltoquiliztli inic miec tlacame ipan Linux, MySQL, Apache, NGINX, Varnish, PHP, VMware, DoS, ihuan occequi. Huey tlapalehuiani ipan MX ihuan US.',
'cv_job4_period' => 'Marzo 2020 → Julio 2021',
'cv_job4_title' => 'Linux Tlapalehuiani - Softtek | Electronic Arts',
'cv_job4_desc' => 'Nitlapalehua tlamantilyotl inic PCI-compliant tlamantli in quichihua nauhpoalli tlamantilyotl ipan cempoallamatl inic DevOps.',
'cv_job5_period' => 'Agosto 2017 → Marzo 2020',
'cv_job5_title' => 'Tlatecpanqui - Softtek | Electronic Arts',
'cv_job5_desc' => 'ITIL tlatecpanaliztli, tlapatlaliztli, ihuan tlamantli tlatequipanoliztli.',
'cv_job6_period' => 'Septiembre 2015 → Agosto 2017',
'cv_job6_title' => 'Linux Tlapalehuiani / Tech Lead - Softtek | General Electric',
'cv_job6_desc' => 'Tlaneltoquiliztli, tlapatlaliztli, ihuan tlachihualiztli ipan tlamantli. Omotlacxitilli tech lead inic ce xihuitl.',
'cv_job7_period' => 'Febrero 2013 → Agosto 2015',
'cv_job7_title' => 'Tlapalehuiani Tlacame Teleperformance | Comcast',
'cv_job7_desc' => 'Nitlapalehua tlacame ipan US inic cable, tepoztli, ihuan tlahtoa tlamantli.',
];

30
web-app/lang_helper.php Normal file
View File

@ -0,0 +1,30 @@
<?php
// lang_helper.php
// Include this at the top of every page.
// Provides: $lang, $text, $en, $availableLangs
$availableLangs = array_map(
fn($f) => basename($f, '.php'),
glob(__DIR__ . '/lang/*.php')
);
function getLang($supported) {
if (isset($_GET['lang']) && in_array($_GET['lang'], $supported)) {
return $_GET['lang'];
}
$browser = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'nah', 0, 2);
return in_array($browser, $supported) ? $browser : 'nah';
}
$lang = getLang($availableLangs);
$file = __DIR__ . "/lang/$lang.php";
if (!file_exists($file)) {
$lang = 'nah';
$file = __DIR__ . "/lang/nah.php";
}
$text = include $file;
// Always load English as translation source
$en = include __DIR__ . '/lang/en.php';

View File

@ -0,0 +1,3 @@
<footer style="margin-top:40px; text-align:center; font-size:0.9em; color:#777;">
© <?php echo date("Y"); ?> Juvenal Diaz
</footer>

View File

@ -0,0 +1,11 @@
<nav class="top-nav">
<div class="nav-left">Juvenal Diaz</div>
<div class="nav-right">
<a href="index.php?lang=<?php echo $lang; ?>">Home</a>
<a href="cv.php?lang=<?php echo $lang; ?>">CV</a>
<a href="#">Blog</a>
|
<a href="?lang=en">EN</a>
<a href="?lang=es">ES</a>
</div>
</nav>

View File

@ -0,0 +1,52 @@
<?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="
position: fixed;
bottom: 16px;
right: 16px;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
font-family: sans-serif;
font-size: 13px;
">
<div id="translate-prompt" style="display:none;">
<button id="translate-btn" style="
background: #2563eb;
color: #fff;
border: none;
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
">
<span id="translate-action"></span> <span id="detected-lang-name"></span>
</button>
</div>
<div id="translation-badge" style="
display: none;
background: #222;
color: #fff;
padding: 8px 14px;
border-radius: 6px;
font-size: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
"></div>
</div>
<script>
const OLLAMA_HOST = 'http://192.168.100.68:11434';
const OLLAMA_MODEL = 'llama3.2:3b';
const SAVE_URL = '/save_lang.php';
const CURRENT_LANG = '<?php echo $lang; ?>';
const STATIC_LANGS = <?php echo json_encode(array_values($availableLangs)); ?>;
if (typeof OTHER_PAGES === 'undefined') { var OTHER_PAGES = []; }
</script>
<script src="/translation.js"></script>

58
web-app/save_lang.php Normal file
View File

@ -0,0 +1,58 @@
<?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');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
$body = json_decode(file_get_contents('php://input'), true);
if (!isset($body['lang'], $body['translations'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing lang or translations']);
exit;
}
$lang = preg_replace('/[^a-z]/', '', strtolower($body['lang']));
$translations = $body['translations'];
if (strlen($lang) < 2 || strlen($lang) > 5) {
http_response_code(400);
echo json_encode(['error' => 'Invalid lang code']);
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);
$value = addslashes($value);
$lines[] = " '$key' => '$value',";
}
$lines[] = "];";
$content = implode("\n", $lines) . "\n";
$path = __DIR__ . "/lang/$lang.php";
if (file_put_contents($path, $content) === false) {
http_response_code(500);
echo json_encode(['error' => 'Could not write file — check permissions on lang/']);
exit;
}
echo json_encode(['success' => true, 'lang' => $lang, 'path' => "lang/$lang.php"]);

205
web-app/styles.css Normal file
View File

@ -0,0 +1,205 @@
body {
font-family: Arial, sans-serif;
margin: 40px;
background-color: #f8f9fa;
color: #333;
}
.container {
max-width: 900px;
margin: auto;
background: #fff;
padding: 30px;
border-radius: 12px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
/* Hero Section Styles */
.hero-section {
display: flex;
align-items: center;
gap: 40px;
margin-bottom: 30px;
padding: 20px 0;
}
.profile-container {
flex-shrink: 0;
}
.profile-img {
width: 200px;
height: 200px;
border-radius: 50%;
object-fit: cover;
border: 5px solid #004085;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.hero-content {
flex: 1;
}
.hero-content h1 {
font-size: 2.5em;
margin-bottom: 10px;
color: #004085;
}
.hero-content .title {
font-size: 1.3em;
color: #004085;
font-weight: bold;
margin: 10px 0;
}
.hero-content .tagline {
font-size: 1.1em;
color: #666;
font-style: italic;
margin: 10px 0 20px 0;
}
.contact-info {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #004085;
}
.contact-info p {
margin: 5px 0;
color: #333;
}
/* Welcome Section */
.welcome {
text-align: center;
margin-bottom: 40px;
padding: 30px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.welcome h2 {
color: #004085;
margin-bottom: 20px;
}
.welcome p {
font-size: 1.1em;
line-height: 1.6;
max-width: 800px;
margin: 0 auto 30px auto;
}
.navigation {
margin: 30px 0;
}
.cv-link {
display: inline-block;
background-color: #004085;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
transition: all 0.3s ease;
font-size: 1.1em;
}
.cv-link:hover {
background-color: #002752;
transform: translateY(-2px);
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));
gap: 20px;
margin-top: 20px;
}
.overview-item {
background-color: #f8f9fa;
padding: 25px;
border-radius: 8px;
border-left: 4px solid #004085;
text-align: center;
}
.overview-item h3 {
color: #004085;
margin-top: 0;
margin-bottom: 15px;
font-size: 1.2em;
}
.overview-item p {
margin: 0;
color: #666;
line-height: 1.5;
}
/* Responsive Design */
@media (max-width: 768px) {
body {
margin: 20px;
}
.hero-section {
flex-direction: column;
text-align: center;
gap: 20px;
}
.profile-img {
width: 150px;
height: 150px;
}
.hero-content h1 {
font-size: 2em;
}
.overview-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.container {
padding: 20px;
}
body {
margin: 10px;
}
}
.interview-calendar, .wasm-demo {
background: #f8f9fa;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
}
.slot-item {
background: white;
padding: 15px;
margin: 10px 0;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.hero-image img {
width: 500px;
height: 500px;
object-fit: cover;
border-radius: 50%;
}

198
web-app/translation.js Normal file
View File

@ -0,0 +1,198 @@
// 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',
ro: 'Romanian', hi: 'Hindi', fr: 'French',
de: 'German', pt: 'Portuguese', it: 'Italian',
zh: 'Chinese', ja: 'Japanese', ko: 'Korean',
ar: 'Arabic', ru: 'Russian', pl: 'Polish',
tr: 'Turkish', sv: 'Swedish', nl: 'Dutch',
fi: 'Finnish', cs: 'Czech', sk: 'Slovak',
nah: 'Nahuatl',
};
const urlLang = new URLSearchParams(window.location.search).get('lang');
const browserLang = (urlLang || navigator.language).slice(0, 2).toLowerCase();
const langName = LANG_NAMES[browserLang] || browserLang.toUpperCase();
const badge = document.getElementById('translation-badge');
const prompt = document.getElementById('translate-prompt');
const btn = document.getElementById('translate-btn');
const actionSpan = document.getElementById('translate-action');
const detectedSpan = document.getElementById('detected-lang-name');
function showBadge(msg) {
if (badge) {
badge.textContent = msg;
badge.style.display = 'block';
}
}
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}.
Return ONLY a valid JSON array of translated strings in the same order, no explanations, no markdown.
Example input: ["Hello", "How are you"]
Example output: ["Bonjour", "Comment allez-vous"]
Input: ${JSON.stringify(texts)}`;
const response = await fetch(`${OLLAMA_HOST}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: OLLAMA_MODEL,
prompt,
stream: false
}),
signal: AbortSignal.timeout(120000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
const raw = data.response.trim().replace(/```json|```/g, '').trim();
const result = JSON.parse(raw);
if (!Array.isArray(result) || result.length !== texts.length) {
throw new Error(`Unexpected response: got ${result.length}, expected ${texts.length}`);
}
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, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ lang, translations })
});
const data = await res.json();
if (data.success) {
console.log(`Saved lang/${lang}.php — static next visit`);
} else {
console.warn('Save failed:', data.error);
}
} catch (e) {
console.warn('Could not save lang file:', e.message);
}
}
// 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 {};
const texts = elements.map(el => el.getAttribute('data-en') || el.textContent.trim());
const translated = await translateBatch(texts, targetLang);
const result = {};
elements.forEach((el, i) => {
const key = el.getAttribute('data-key') || el.getAttribute('data-en');
result[key] = translated[i];
});
return result;
}
// Main translation flow — triggered by button click
async function doTranslation() {
hidePrompt();
if (btn) btn.disabled = true;
const elements = [...document.querySelectorAll('[data-translate]')];
if (!elements.length) return;
showBadge('Translating page...');
elements.forEach(el => el.style.opacity = '0.4');
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);
elements.forEach((el, i) => {
el.textContent = translated[i];
const key = el.getAttribute('data-key') || el.getAttribute('data-en');
allTranslations[key] = translated[i];
});
} catch (e) {
console.warn('Page translation failed:', e.message);
elements.forEach(el => el.style.opacity = '1');
showBadge('Translation failed — showing original');
return;
}
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}...`);
try {
const res = await fetch(url);
const html = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const pageTranslations = await collectPageTranslations(doc, browserLang);
Object.assign(allTranslations, pageTranslations);
} catch (e) {
console.warn(`Could not translate ${url}:`, e.message);
}
}
}
// 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';
if (prompt) prompt.style.display = 'block';
if (btn) btn.addEventListener('click', () => {
window.location.href = window.location.pathname + '?lang=en';
});
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 (actionSpan) actionSpan.textContent = 'Translate to';
if (detectedSpan) detectedSpan.textContent = langName;
if (prompt) prompt.style.display = 'block';
if (btn) btn.addEventListener('click', doTranslation);
}
initTranslation();

47
web-app/web-app.yaml Normal file
View File

@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-website-deployment
namespace: website-production
labels:
app: php-website
spec:
replicas: 2
selector:
matchLabels:
app: php-website
template:
metadata:
labels:
app: php-website
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- php-website
topologyKey: "kubernetes.io/hostname"
containers:
- name: php-app
image: 192.168.100.68:30500/my-php-app:v1
imagePullPolicy: Always
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: php-website-service
namespace: website-production
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 30080
selector:
app: php-website