+
+
+
+ $activityKey): ?>
+ -
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/website/homelab-tree.php b/apps/website/homelab-tree.php
new file mode 100644
index 0000000..b4254ae
--- /dev/null
+++ b/apps/website/homelab-tree.php
@@ -0,0 +1,261 @@
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Star: public DNS, TLS, and the entry point users actually type.
+ - Garlands: Tailscale routing, NodePorts, and the GitOps sync loop connecting the layers.
+ - Branches: Kubernetes namespaces and workloads that carry the visible services.
+ - Ornaments: Gitea, Actions, Argo CD, Buildx, Trivy, Gitleaks, Calico, registry, website, demos, and the Gitea app.
+ - Bells: probes and health checks that make noise before users do.
+ - Trunk: the Debian control-plane node that holds the platform upright.
+ - Roots: OpenEBS retained volumes, external SSD storage, Gitea dumps, and restore discipline.
+ - Gifts: the OCI edge host, Raspberry Pi worker, monitoring ideas, and the improvement backlog.
+
+
+
+
+
+
+
+
+
diff --git a/apps/website/lang/en.php b/apps/website/lang/en.php
index c96716c..d8c6b17 100644
--- a/apps/website/lang/en.php
+++ b/apps/website/lang/en.php
@@ -88,6 +88,45 @@ return [
'blog_stack_9' => 'The newer demos cover network jitter graphs, local JSON/JWT/log tools, an architecture simulator, an offline traveler converter, a redactor prototype, sentiment analysis, and model-drift simulation.',
'blog_stack_10' => 'The heavier ML demos are designed as client-side Wasm/ONNX/Transformers.js candidates, not server-side jobs. That keeps the homelab app boring to operate, which is secretly the whole point.',
'blog_stack_11' => 'The demo code now builds into its own demos-static image and Argo CD app, exposed at /demo-apps/. The PHP website only owns the catalog link, which is much less cursed.',
+ 'blog_arch_kicker' => 'Architecture map',
+ 'blog_arch_title' => 'The homelab, end to end',
+ 'blog_arch_intro' => 'The current delivery path starts with a push to Gitea, runs local validation, builds arm64 images, syncs the validated commit into the GitOps mirror, and lets Argo CD reconcile the Kubernetes workloads while the OCI edge routes public traffic back through the private path.',
+ 'blog_arch_caption' => 'The diagram is intentionally operational: it shows the control flow, image flow, storage boundary, and public traffic path without hiding the practical bits that make a small lab behave like a platform.',
+ 'blog_arch_fun_link' => 'Open the Christmas-tree version',
+ 'blog_activity_kicker' => 'Recent activity log',
+ 'blog_activity_title' => 'What changed since the first build',
+ 'blog_activity_intro' => 'The lab moved from a working Kubernetes experiment into a more complete self-hosted delivery system. The latest work focused on trust, repeatability, and making deploys match the exact commit that passed validation.',
+ 'blog_activity_1' => 'Brought Gitea online as the local Git service, including persistent storage and the public /git/ route through the edge stack.',
+ 'blog_activity_2' => 'Installed and validated a Debian-hosted Gitea Actions runner so pushes to main can build, scan, and deploy without depending on a laptop session.',
+ 'blog_activity_3' => 'Added a custom checkout flow for the /git/ subpath and kept a persistent Debian checkout for the deployment scripts.',
+ 'blog_activity_4' => 'Added Gitleaks secret scanning and Trivy scanning, with scoped exceptions only where the lab intentionally accepts a known Gitea workload shape.',
+ 'blog_activity_5' => 'Changed deployment so the validated commit is pushed into the local GitOps mirror before lab.sh runs, preventing Argo CD from reconciling an older tree.',
+ 'blog_activity_6' => 'Hardened the website, demos-static, and registry workloads with non-root containers, read-only root filesystems, resource limits, and explicit writable volumes.',
+ 'blog_activity_7' => 'Split the demos into a dedicated demos-static image and Argo CD application so the PHP website stays small and boring.',
+ 'blog_activity_8' => 'Fixed Gitea operational details around probes, service paths, backup dumps, and the user context used for safe backup execution.',
+ 'blog_activity_9' => 'Validated the full main-branch deployment path: fetch main, apply OpenTofu layers, build and push arm64 images, refresh Argo CD, and confirm the runner completes successfully.',
+ 'blog_todo_kicker' => 'Improvement backlog',
+ 'blog_todo_title' => 'Todo list for the next homelab pass',
+ 'blog_todo_intro' => 'These are improvement proposals, not chores for the sake of chores. Each item either reduces rebuild risk, tightens supply-chain hygiene, or makes the platform easier to operate when something fails.',
+ 'blog_todo_1' => 'Move Gitea to a rootless runtime image and remove the remaining privileged assumptions from the Git service.',
+ 'blog_todo_2' => 'Point Argo CD directly at Gitea once bootstrap is stable, then retire or simplify the local bare GitOps mirror.',
+ 'blog_todo_3' => 'Add a real OpenTofu remote state backend with backup, locking, and a documented recovery path.',
+ 'blog_todo_4' => 'Replace mutable latest image references with immutable tags or digest pins for website and demo workloads.',
+ 'blog_todo_5' => 'Generate SBOMs and sign images so the local registry can prove what it is serving.',
+ 'blog_todo_6' => 'Add Renovate or Dependabot-style dependency updates for base images, Helm charts, and GitHub/Gitea Actions.',
+ 'blog_todo_7' => 'Enforce baseline Kubernetes policy with Kyverno or Gatekeeper: non-root, read-only roots, resource requests, and allowed registries.',
+ 'blog_todo_8' => 'Install observability that fits the hardware: Prometheus, Grafana, Loki, node-exporter, and a few high-signal alerts.',
+ 'blog_todo_9' => 'Schedule backup restore drills for Gitea and OpenEBS volumes, then write the exact restore runbook.',
+ 'blog_todo_10' => 'Tighten TLS, SSH, and token rotation around the OCI edge, Gitea, registry, and runner credentials.',
+ 'blog_todo_11' => 'Design the next storage step before adding more apps: NAS, replicated storage, or a clearly documented single-node tradeoff.',
+ 'blog_todo_12' => 'Move sensitive app configuration into Sealed Secrets, External Secrets, or another explicit secret-management path.',
+ 'tree_kicker' => 'Fun architecture mode',
+ 'tree_title' => 'The Homelab Christmas Tree',
+ 'tree_subtitle' => 'Same platform, less serious outfit: every part of the homelab becomes a tree part, from the public DNS star down to the storage roots and backup gifts.',
+ 'tree_back_to_blog' => 'Back to the professional diagram',
+ 'tree_key_kicker' => 'Tree legend',
+ 'tree_key_title' => 'What each festive part means',
+ 'tree_key_intro' => 'The joke still maps to the real architecture: each visual part has one operational job in the homelab.',
// Demos
'demos_kicker' => 'Small tools, real browser work',
diff --git a/apps/website/styles.css b/apps/website/styles.css
index 1d9c07a..641538a 100644
--- a/apps/website/styles.css
+++ b/apps/website/styles.css
@@ -386,7 +386,474 @@ body {
line-height: 1.55;
}
+.architecture-section,
+.activity-log,
+.homelab-todo,
+.tree-key {
+ margin-top: 34px;
+ padding: 24px;
+ background: #fff;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+}
+
+.section-heading {
+ max-width: 820px;
+ margin-bottom: 18px;
+}
+
+.section-heading h1,
+.section-heading h2 {
+ margin: 0 0 10px;
+ color: #102a43;
+}
+
+.section-heading p {
+ margin: 0 0 12px;
+ color: #52606d;
+ line-height: 1.6;
+}
+
+.section-kicker {
+ color: #004085;
+ font-size: 0.82rem;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.diagram-shell {
+ overflow-x: auto;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+ background: #f8fbff;
+}
+
+.homelab-map {
+ display: block;
+ width: 100%;
+ min-width: 920px;
+ height: auto;
+}
+
+.diagram-zone {
+ fill: #f7fafc;
+ stroke: #cbd5e1;
+ stroke-width: 1.2;
+}
+
+.diagram-zone-source {
+ fill: #f7fbff;
+}
+
+.diagram-zone-platform {
+ fill: #f7fff9;
+}
+
+.diagram-zone-runtime {
+ fill: #fffaf2;
+}
+
+.diagram-zone-title {
+ fill: #102a43;
+ font-size: 17px;
+ font-weight: 800;
+}
+
+.diagram-node rect {
+ fill: #fff;
+ stroke-width: 1.5;
+}
+
+.diagram-node text {
+ fill: #102a43;
+ font-size: 16px;
+ font-weight: 800;
+}
+
+.diagram-node .diagram-small,
+.diagram-small {
+ fill: #52606d;
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.node-accent-blue rect {
+ stroke: #2b6cb0;
+}
+
+.node-accent-teal rect {
+ stroke: #2c7a7b;
+}
+
+.node-accent-red rect {
+ stroke: #c53030;
+}
+
+.node-accent-green rect {
+ stroke: #2f855a;
+}
+
+.node-accent-purple rect {
+ stroke: #6b46c1;
+}
+
+.node-accent-orange rect {
+ stroke: #b7791f;
+}
+
+.diagram-link {
+ fill: none;
+ stroke: #2b6cb0;
+ stroke-width: 2.2;
+ marker-end: url(#map-arrow);
+}
+
+.diagram-link-label {
+ fill: #334e68;
+ font-size: 13px;
+ font-weight: 800;
+}
+
+.diagram-actions {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ margin-top: 16px;
+}
+
+.diagram-caption {
+ margin: 0;
+ color: #52606d;
+ line-height: 1.55;
+}
+
+.diagram-fun-link,
+.tree-back-link {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 42px;
+ padding: 0 16px;
+ border-radius: 6px;
+ background: #004085;
+ color: #fff;
+ text-decoration: none;
+ font-weight: 800;
+ white-space: nowrap;
+}
+
+.diagram-fun-link:hover,
+.tree-back-link:hover {
+ background: #002752;
+}
+
+.activity-list {
+ display: grid;
+ gap: 10px;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+.activity-list li {
+ display: grid;
+ grid-template-columns: 52px 1fr;
+ gap: 14px;
+ align-items: start;
+ padding: 14px;
+ border: 1px solid #e6edf5;
+ border-radius: 8px;
+ background: #fbfdff;
+ line-height: 1.55;
+}
+
+.activity-number {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 42px;
+ height: 32px;
+ border-radius: 999px;
+ background: #e8f1ff;
+ color: #004085;
+ font-weight: 800;
+}
+
+.todo-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 12px;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+.todo-list li {
+ display: flex;
+ gap: 12px;
+ align-items: flex-start;
+ min-height: 76px;
+ padding: 14px;
+ border: 1px solid #e6edf5;
+ border-radius: 8px;
+ background: #fbfdff;
+ line-height: 1.5;
+}
+
+.todo-checkbox {
+ width: 18px;
+ height: 18px;
+ flex: 0 0 auto;
+ margin-top: 2px;
+ border: 2px solid #9fb3c8;
+ border-radius: 4px;
+ background: #fff;
+}
+
+.tree-page {
+ max-width: 1120px;
+ margin: 0 auto;
+}
+
+.tree-hero {
+ margin-bottom: 34px;
+}
+
+.tree-hero .section-heading h1 {
+ font-size: 2.4rem;
+}
+
+.tree-stage {
+ overflow-x: auto;
+ border-radius: 8px;
+ border: 1px solid #1f3a4d;
+ background: #132635;
+}
+
+.christmas-homelab {
+ display: block;
+ width: 100%;
+ min-width: 900px;
+ height: auto;
+}
+
+.tree-sky {
+ fill: #132635;
+}
+
+.tree-light {
+ opacity: 0.95;
+}
+
+.tree-light-blue {
+ fill: #9ed7ff;
+}
+
+.tree-light-gold {
+ fill: #ffd166;
+}
+
+.tree-light-red {
+ fill: #ff7a7a;
+}
+
+.tree-light-green {
+ fill: #9cffb1;
+}
+
+.tree-star polygon {
+ fill: #ffd166;
+ stroke: #f2a900;
+ stroke-width: 4;
+}
+
+.tree-star text,
+.tree-ornament text,
+.tree-bell text,
+.tree-gift text,
+.tree-trunk-text,
+.tree-root-label,
+.tree-garland-label {
+ text-anchor: middle;
+ font-size: 16px;
+ font-weight: 800;
+}
+
+.tree-star text {
+ fill: #412500;
+}
+
+.tree-small {
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.tree-layer {
+ stroke: #164f35;
+ stroke-width: 3;
+}
+
+.tree-layer-top {
+ fill: #1f7a4f;
+}
+
+.tree-layer-shadow {
+ fill: #17613f;
+ opacity: 0.9;
+}
+
+.garland {
+ fill: none;
+ stroke-width: 8;
+ stroke-linecap: round;
+ stroke-dasharray: 12 12;
+}
+
+.garland-blue {
+ stroke: #9ed7ff;
+}
+
+.garland-gold {
+ stroke: #ffd166;
+}
+
+.garland-red {
+ stroke: #ff7a7a;
+}
+
+.tree-garland-label {
+ fill: #f8fbff;
+ font-size: 14px;
+}
+
+.tree-ornament circle {
+ stroke: rgba(255, 255, 255, 0.85);
+ stroke-width: 3;
+}
+
+.tree-ornament text {
+ fill: #102a43;
+}
+
+.ornament-blue circle {
+ fill: #b9e3ff;
+}
+
+.ornament-gold circle {
+ fill: #ffe29a;
+}
+
+.ornament-red circle {
+ fill: #ffb3b3;
+}
+
+.ornament-purple circle {
+ fill: #d6c8ff;
+}
+
+.ornament-teal circle {
+ fill: #aee9e4;
+}
+
+.ornament-green circle {
+ fill: #bdedc6;
+}
+
+.tree-bell path {
+ fill: #ffd166;
+ stroke: #f2a900;
+ stroke-width: 3;
+}
+
+.tree-bell circle {
+ fill: #f2a900;
+}
+
+.tree-bell text {
+ fill: #f8fbff;
+ font-size: 13px;
+}
+
+.tree-trunk {
+ fill: #7b4a2a;
+ stroke: #4a2b16;
+ stroke-width: 3;
+}
+
+.tree-trunk-text {
+ fill: #fff5df;
+}
+
+.tree-root {
+ fill: none;
+ stroke: #7b4a2a;
+ stroke-width: 8;
+ stroke-linecap: round;
+}
+
+.tree-root-label {
+ fill: #f8fbff;
+ font-size: 14px;
+}
+
+.tree-gift rect {
+ stroke: rgba(255, 255, 255, 0.85);
+ stroke-width: 3;
+}
+
+.tree-gift path {
+ stroke: rgba(255, 255, 255, 0.85);
+ stroke-width: 5;
+}
+
+.tree-gift text {
+ fill: #102a43;
+}
+
+.gift-blue rect {
+ fill: #b9e3ff;
+}
+
+.gift-red rect {
+ fill: #ffb3b3;
+}
+
+.gift-green rect {
+ fill: #bdedc6;
+}
+
+.gift-gold rect {
+ fill: #ffe29a;
+}
+
+.tree-key-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 12px;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+.tree-key-list li {
+ min-height: 86px;
+ padding: 14px;
+ border: 1px solid #e6edf5;
+ border-radius: 8px;
+ background: #fbfdff;
+ line-height: 1.5;
+}
+
+.tree-key-list strong {
+ color: #004085;
+}
+
@media (max-width: 768px) {
+ body {
+ margin: 24px;
+ }
+
.top-nav {
align-items: flex-start;
flex-direction: column;
@@ -400,6 +867,32 @@ body {
.message.answer {
justify-self: stretch;
}
+
+ .architecture-section,
+ .activity-log,
+ .homelab-todo,
+ .tree-key {
+ padding: 18px;
+ }
+
+ .diagram-actions {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .diagram-fun-link,
+ .tree-back-link {
+ white-space: normal;
+ text-align: center;
+ }
+
+ .activity-list li {
+ grid-template-columns: 1fr;
+ }
+
+ .tree-hero .section-heading h1 {
+ font-size: 2rem;
+ }
}
/* CV theme toggle */
@@ -919,4 +1412,3 @@ body.home-page.cv-fancy {
font-size: 2.35rem;
}
}
-