Author SHA1 Message Date
Story Crater Bot eb465278ca remove: delete article page (planned for later)
CI / CI (pull_request) Failing after 1m48s
2026-09-09 14:24:10 +09:00
Story Crater Bot 17ef6d8a8a feat: add architecture diagrams page with archify visualizations
CI / CI (pull_request) Failing after 1m46s
- Create /diagrams page with interactive homelab architecture visualizations
- Add deepDive link to homelab project pointing to diagrams
- Include 4 archify diagrams:
  * Talos cluster topology (control planes, GPU worker, storage)
  * GitOps workflow (ArgoCD + Terraform)
  * LLM inference stack (vLLM, Ollama, TEI routing)
  * API request flow sequence diagram
- Support full-screen viewer and embedded grid layout
- Dark/light theme support with responsive design
2026-09-09 14:22:42 +09:00
Story Crater Bot 6212c3b717 fix: update GitHub URLs to use Riotpiaole and forgejo.riotpiao.com
CI / CI (pull_request) Successful in 5m36s
- Change public GitHub from github.com/rockliang to github.com/Riotpiaole
- Change private GitHub to forgejo.riotpiao.com for all references
- Update both desktop and mobile contact sections
2026-09-08 19:41:57 -07:00
Story Crater Bot 8ed3923c18 fix: use ksops generator pattern for SOPS-encrypted secrets
- Remove configmap.enc.yaml from resources (blocked kustomize parsing)
- Create secret-generator.yaml with ksops exec plugin config
- ksops decrypts secrets before kustomize reads them
- Allows Image Updater to safely edit images section
- Fixes: ArgoCD repo-server now properly decrypts secrets via ksops plugin

Pattern mirrors homelab/k8s/argocd/secrets/ for consistency
2026-09-07 17:54:34 -07:00
Story Crater Bot 16d61c7dea merge: CI/CD improvements and Image Updater fixes from fix/unified-ci
CI / CI (push) Successful in 5m46s
- Upgrade pnpm to v12.3.4 for reproducible builds
- Skip build scripts during pnpm install
- Enable ArgoCD Image Updater annotations
- Add images section to kustomization for Image Updater
- Remove sops field to unblock vanilla kustomize parsing

Closes PR #4
2026-09-07 17:51:08 -07:00
Story Crater Bot 303da88239 fix: remove sops field from kustomization to unblock Image Updater
- Remove sops section that breaks vanilla kustomize parsing
- SOPS decryption handled by ArgoCD repo-server ksops plugin
- Allows Image Updater to run 'kustomize edit set image' without errors
- Image Updater can now update portfolio:latest tag in images section
2026-09-07 17:49:55 -07:00
Story Crater Bot 9f3db93f93 fix: add images section to kustomization for Image Updater
- Add images section so kustomize can properly update portfolio image tag
- Keep sops field for ArgoCD's ksops plugin to decrypt secrets
- Allows Image Updater to detect and sync latest image builds
2026-09-07 17:47:10 -07:00
Story Crater Bot 436cbfb2c6 fix: add images section to kustomization for ArgoCD Image Updater
- Add images section to properly track portfolio image for kustomize
- Remove problematic sops field that breaks kustomize edit commands
- Allows Image Updater to properly update image tags via kustomize
- SOPS decryption should be handled by ArgoCD plugin, not kustomization
2026-09-07 17:42:59 -07:00
Story Crater Bot cff8ba4b85 feat: enable ArgoCD Image Updater for portfolio deployment
- Add argocd-image-updater annotations to auto-detect :latest image changes
- Updater will poll registry and trigger ArgoCD sync when new build pushed
- Deployment pulls latest image on pod restart

Enables automatic updates without manual ArgoCD sync
2026-09-07 17:39:49 -07:00
rock 6c415c6382 ci: unified workflow - single job, DOCKER_HOST, build+push on all events (#4)
CI / CI (push) Successful in 5m23s
Unify CI/CD workflow and fix pnpm v12 compatibility issues. This PR addresses the ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION failure that was blocking builds on fresh package releases.
2026-09-08 00:35:53 +00:00
Story Crater Bot e7dfef2ebc fix: skip build scripts during pnpm install in CI
CI / CI (pull_request) Successful in 5m41s
- Add --ignore-scripts flag to pnpm install
- Prevents ERR_PNPM_IGNORED_BUILDS from unrs-resolver
- Build scripts not needed during dependency install phase
- Compilation handled by 'pnpm run build' step
2026-09-07 17:27:04 -07:00
Story Crater Bot d0cb810bbe fix: upgrade to pnpm v12.3.4, disable release-age quarantine
CI / CI (pull_request) Failing after 1m14s
- Pin pnpm to v12.3.4 via packageManager field for reproducible builds
- Set minimum-release-age=0 to allow fresh package installs
- Update CI workflow to use corepack install (respects pinned version)
- Remove frozen-lockfile to regenerate for v12 format

Fixes ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION on [email protected].0
2026-09-07 17:24:41 -07:00
Story Crater Bot ffb31e7f33 ci: unified workflow - use corepack for pnpm, DOCKER_HOST, build+push on all events
CI / CI (pull_request) Failing after 1m6s
2026-09-07 16:47:00 -07:00
Story Crater Bot 6cc48b6fa5 ci: unified workflow - single job, DOCKER_HOST, build+push on all events
CI / CI (pull_request) Failing after 2m52s
2026-09-07 14:18:46 -07:00
Story Crater Bot cd3a4465b4 ci: add pre-verification step to catch failures early
CI / Test (push) Successful in 1m45s
CI / Build & Push Image (push) Failing after 58s
2026-09-07 08:58:51 -07:00
Story Crater Bot 46b1f82771 ci: add DOCKER_HOST env vars to fix socket connectivity
CI / Test (push) Successful in 1m29s
CI / Build & Push Image (push) Failing after 1m1s
2026-09-07 08:57:49 -07:00
rockandStory Crater Bot 19bf437d79 fix: use env vars for docker registry credentials (#3)
CI / Test (push) Successful in 1m56s
CI / Build & Push Image (push) Failing after 1m13s
Fix registry login by passing FORGEJO_REGISTRY_USER and FORGEJO_REGISTRY_TOKEN via environment variables instead of direct secret interpolation.

This is the reference implementation pattern used across all repos.

This prevents credentials from being exposed in logs or shell history while keeping the standard docker login approach.

After merge + org-level secrets configured:
- All repos inherit FORGEJO_REGISTRY_USER and FORGEJO_REGISTRY_TOKEN
- CI validates credentials exist before docker login
- Image pushed to registry on main push

---------

Co-authored-by: Story Crater Bot <[email protected]>
Reviewed-on: #3
2026-09-07 07:08:36 +00:00
Story Crater Bot 6506a639c9 fix: move LLM config to encrypted ConfigMap (CI-friendly)
CI / Test (push) Failing after 29s
CI / Build & Push Image (push) Skipped
Problem: LLM_API_URL was hardcoded to external endpoint
- Uses https://api.riotpiao.com/v1/chat/completions (TLS hairpin)
- Not externalizable for CI/different environments

Solution: Move to encrypted ConfigMap with in-cluster endpoint
- LLM_API_URL: http://api-gateway.api.svc.cluster.local:8080/v1/chat/completions
- No TLS, no nginx hairpin, direct cluster communication
- Encrypted with SOPS for security
- CI can update values.yaml and auto-deploy

Changes:
- Create configmap.enc.yaml (SOPS-encrypted)
- Update deployment.yaml to use configMapKeyRef
- Add SOPS config to kustomization.yaml
- Deployment now references portfolio-llm-config ConfigMap
2026-09-06 23:24:12 -07:00
Story Crater Bot 7389b977b4 refactor: standardize workflow name and move env to workflow level
CI / Test (push) Failing after 27s
CI / Build & Push Image (push) Skipped
- Change name to 'CI' (consistent with template)
- Move REGISTRY, IMAGE to workflow-level env (shared by both jobs)
- Compact 'on:' branches syntax
- Maintain all service-specific features (Delete old latest, build-arg)

No functional change, pure standardization to match template pattern.
2026-09-06 23:21:54 -07:00
17 changed files with 61184 additions and 974 deletions
+32 -54
View File
@@ -1,52 +1,46 @@
name: Build & Push Portfolio Image name: CI
on: on:
push: push:
branches: branches: [main]
- main
pull_request: pull_request:
branches: branches: [main]
- main workflow_dispatch:
jobs: env:
test:
name: Test
runs-on: node
steps:
- name: Install Node.js for actions runtime
run: apt-get update && apt-get install -y nodejs
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --run 2>&1 || echo "Tests completed"
build-push:
name: Build & Push Image
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: node
env:
REGISTRY: forgejo.riotpiao.com REGISTRY: forgejo.riotpiao.com
IMAGE: forgejo.riotpiao.com/rock/portfolio IMAGE: forgejo.riotpiao.com/rock/portfolio
DOCKER_HOST: tcp://localhost:2375
jobs:
ci:
name: CI
runs-on: node
steps: steps:
- name: Install Node.js and Docker - name: Install Docker and corepack
run: | run: |
apt-get update apt-get update
apt-get install -y nodejs docker.io apt-get install -y docker.io
corepack enable
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install pnpm via corepack
run: corepack install
- name: Install dependencies
run: pnpm install --ignore-scripts
- name: Run tests
run: pnpm test -- --run 2>&1 || echo "Tests completed"
- name: Build
run: pnpm run build
- name: Get short SHA - name: Get short SHA
id: sha id: sha
run: | run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
SHORT_SHA=$(git rev-parse --short HEAD)
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
- name: Registry login - name: Registry login
run: | run: |
@@ -56,33 +50,17 @@ jobs:
REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }} REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }} REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
- name: Delete old latest image - name: Build Docker image
run: |
# Delete old :latest tag from Forgejo registry via API
curl -s -X DELETE \
-u "${REGISTRY_USER}:${REGISTRY_TOKEN}" \
"https://${REGISTRY}/v2/rock/portfolio/manifests/$(curl -s -H 'Accept: application/vnd.oci.image.index.v1+json' -u "${REGISTRY_USER}:${REGISTRY_TOKEN}" "https://${REGISTRY}/v2/rock/portfolio/manifests/latest" | head -1 | grep -o 'sha256:[a-f0-9]*' || true)" \
2>/dev/null || echo "No old latest to delete"
env:
REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
- name: Build image
run: | run: |
docker build --no-cache \ docker build --no-cache \
--build-arg COMMIT_SHA=${{ steps.sha.outputs.short_sha }} \
-t "${IMAGE}:${{ steps.sha.outputs.short_sha }}" \ -t "${IMAGE}:${{ steps.sha.outputs.short_sha }}" \
-t "${IMAGE}:latest" \ -t "${IMAGE}:latest" .
.
- name: Push image - name: Push Docker image
run: | run: |
docker push "${IMAGE}:${{ steps.sha.outputs.short_sha }}" docker push "${IMAGE}:${{ steps.sha.outputs.short_sha }}"
docker push "${IMAGE}:latest" docker push "${IMAGE}:latest"
echo "✓ Image pushed: ${IMAGE}:${{ steps.sha.outputs.short_sha }}" echo "✓ Pushed: ${IMAGE}:${{ steps.sha.outputs.short_sha }}"
- name: Prune unused images - name: Prune unused images
run: | run: docker image prune -a --force 2>&1 | tail -3 || true
docker image prune -a --force 2>&1 | tail -3 || true
+1
View File
@@ -1,2 +1,3 @@
ignore-scripts=false ignore-scripts=false
enable-pre-post-scripts=true enable-pre-post-scripts=true
minimum-release-age=0
+230 -130
View File
@@ -2,6 +2,7 @@
import Link from 'next/link' import Link from 'next/link'
import { ArrowLeft } from 'lucide-react' import { ArrowLeft } from 'lucide-react'
import { motion } from 'framer-motion'
const skillCategories = [ const skillCategories = [
{ {
@@ -46,18 +47,10 @@ const skillCategories = [
}, },
] ]
const coreCompetencies = [
'Multi-tenant GPU scheduling (sm70/Volta constraints)',
'Zero-downtime GitOps deployments',
'Service mesh patterns without Istio overhead',
'Hybrid cloud networking (Cloudflare + bare-metal)',
'Declarative IAM with OIDC claim mapping',
]
export default function HomelabPage() { export default function HomelabPage() {
return ( return (
<main className="min-h-screen bg-white dark:bg-gray-950"> <main className="min-h-screen bg-white dark:bg-gray-950">
<div className="max-w-4xl mx-auto px-6 py-20"> <div className="max-w-6xl mx-auto px-6 py-20">
<Link <Link
href="/#project-homelab" href="/#project-homelab"
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:underline mb-8" className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:underline mb-8"
@@ -66,96 +59,57 @@ export default function HomelabPage() {
Back to Projects Back to Projects
</Link> </Link>
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4"> {/* Hero */}
Homelab: Self-Hosted Cloud Platform <motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="mb-16"
>
<h1 className="text-5xl font-bold text-gray-900 dark:text-white mb-4">
Building AWS at Home
</h1> </h1>
<p className="text-xl text-gray-600 dark:text-gray-400 mb-12"> <p className="text-xl text-gray-600 dark:text-gray-400 mb-6">
AWS rebuilt from scratch at homefull stack from compute to observability. A 3-year journey through distributed systems, from Step Functions to LLM inference.
</p> </p>
<p className="text-lg text-gray-700 dark:text-gray-300 leading-relaxed">
There's a difference between knowing how systems work in theory and building them in production. I've spent the last 3 years learning this difference the hard wayfirst at AWS, then at RBC, and now at home. This is the story of how I learned distributed systems by owning every layer: from workflow orchestration to hardware networking, from GitOps to AI agents.
</p>
</motion.div>
{/* Stats */} {/* Chapter 1: Homelab */}
<section className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-800 rounded-lg p-6 mb-12"> <motion.section
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center"> initial={{ opacity: 0, y: 40 }}
<div> whileInView={{ opacity: 1, y: 0 }}
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">4</div> viewport={{ once: true }}
<div className="text-sm text-gray-600 dark:text-gray-400">Nodes</div> className="mb-16 border-l-4 border-purple-500 pl-8"
</div>
<div>
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">20+</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Services</div>
</div>
<div>
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">99.2%</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Uptime</div>
</div>
<div>
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">60%</div>
<div className="text-sm text-gray-600 dark:text-gray-400">LLM Latency Cut</div>
</div>
</div>
</section>
{/* Skills Grid */}
<section className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Technologies & Skills
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{skillCategories.map((category) => (
<div
key={category.title}
className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 border border-gray-200 dark:border-gray-800"
> >
<h3 className="font-semibold text-gray-900 dark:text-white mb-2"> <div className="mb-6">
{category.title} <div className="text-sm font-semibold text-purple-600 dark:text-purple-400 uppercase tracking-wide mb-2">
</h3> The Journey
<div className="flex flex-wrap gap-2">
{category.skills.map((skill) => (
<span
key={skill}
className="text-xs px-2 py-1 rounded bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
>
{skill}
</span>
))}
</div> </div>
</div> <h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
))} Homelab: Building AWS from Scratch
</div>
</section>
{/* Core Competencies */}
<section className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Core Competencies Demonstrated
</h2> </h2>
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800"> <p className="text-gray-600 dark:text-gray-400 font-medium">May 2025 Present</p>
<ul className="space-y-2">
{coreCompetencies.map((item) => (
<li key={item} className="flex items-start gap-2 text-gray-700 dark:text-gray-300">
<span className="text-blue-500 mt-1"></span>
{item}
</li>
))}
</ul>
</div> </div>
</section>
{/* Architecture Sections */} <div className="prose prose-lg dark:prose-invert max-w-none mb-6">
<section className="mb-12"> <p className="text-gray-700 dark:text-gray-300">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6"> The question: <strong>How does LLM serving work at scale?</strong> The only way to answer that was to build an entire cloud-like platform with SaaS fundamentals from scratch. 4 bare-metal machines, 1 GPU node, 1 Dell PowerEdge (now in the garage due to noise), and 2 mini-desktops. All running Talos Linux, a Kubernetes-native OS designed for immutability.
Architecture Deep Dive </p>
</h2> </div>
{/* Infrastructure */} {/* Infrastructure section + diagram */}
<div className="mb-6 bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800"> <div className="mb-8">
<h3 className="font-semibold text-gray-900 dark:text-white mb-3"> <h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Infrastructure Layer Infrastructure Layer
</h3> </h3>
<p className="text-gray-700 dark:text-gray-300 mb-3"> <div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800 mb-6">
4-node bare-metal cluster (3 control plane + 1 worker) running Talos Linuximmutable, API-driven OS designed for Kubernetes. <p className="text-gray-700 dark:text-gray-300 mb-4">
4-node bare-metal cluster (3 control plane + 1 worker) running Talos Linuximmutable, API-driven OS designed for Kubernetes. All wired ethernet to avoid etcd consensus issues.
</p> </p>
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400"> <ul className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<li> <strong>Compute:</strong> Talos Linux nodes, machine config via Terraform</li> <li> <strong>Compute:</strong> Talos Linux nodes, machine config via Terraform</li>
<li> <strong>Networking:</strong> Cilium CNI, nginx ingress, Cloudflare Tunnel for zero-trust external access</li> <li> <strong>Networking:</strong> Cilium CNI, nginx ingress, Cloudflare Tunnel for zero-trust external access</li>
<li> <strong>Storage:</strong> Longhorn for distributed block storage with disk tagging, MinIO for S3</li> <li> <strong>Storage:</strong> Longhorn for distributed block storage with disk tagging, MinIO for S3</li>
@@ -163,28 +117,64 @@ export default function HomelabPage() {
</ul> </ul>
</div> </div>
{/* Data Platform */} {/* Cluster Topology Diagram */}
<div className="mb-6 bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800"> <div className="mb-6 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden hover:shadow-lg transition-shadow">
<h3 className="font-semibold text-gray-900 dark:text-white mb-3"> <div className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-800 border-b border-gray-200 dark:border-gray-800">
Data Platform <h4 className="font-semibold text-gray-900 dark:text-white">Talos Cluster Topology</h4>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">Control planes, worker GPU, storage, and networking architecture</p>
</div>
<div className="w-full" style={{ height: '700px' }}>
<iframe
src="/diagrams/homelab-cluster.html"
className="w-full h-full border-none"
title="Talos Cluster Topology"
/>
</div>
</div>
</div>
{/* GitOps section + diagram */}
<div className="mb-8">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
GitOps Evolution
</h3> </h3>
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400"> <div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800 mb-6">
<li> <strong>PostgreSQL:</strong> CloudNativePG operator with HA, automated failover</li> <p className="text-gray-700 dark:text-gray-300 mb-4">
<li> <strong>Vector DB:</strong> pgvector extension for AI embeddings</li> Started with Terraform for everythingbut constant reconciliation of pods and configmaps created chaos. Brought in ArgoCD and established a clear split:
<li> <strong>Streaming:</strong> Kafka/Redpanda for event-driven architecture</li> </p>
<li> <strong>Workflows:</strong> Temporal for durable, long-running processes</li> <ul className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<li> <strong>Terraform:</strong> Talos machine config (barely changes, no drift)</li>
<li> <strong>ArgoCD:</strong> CRD-driven observer pattern for K8s resources (changes sync automatically)</li>
<li> <strong>Design principle:</strong> Infrastructure should be easy for AI to modify. Declarative config + clear abstractions = easier for models to reason about</li>
</ul> </ul>
</div> </div>
{/* AI/ML */} {/* GitOps Workflow Diagram */}
<div className="mb-6 bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800"> <div className="mb-6 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden hover:shadow-lg transition-shadow">
<h3 className="font-semibold text-gray-900 dark:text-white mb-3"> <div className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-800 border-b border-gray-200 dark:border-gray-800">
<h4 className="font-semibold text-gray-900 dark:text-white">GitOps Deployment Flow</h4>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">ArgoCD for Kubernetes and Terraform for Talos infrastructure</p>
</div>
<div className="w-full" style={{ height: '700px' }}>
<iframe
src="/diagrams/homelab-gitops.html"
className="w-full h-full border-none"
title="GitOps Workflow"
/>
</div>
</div>
</div>
{/* AI/ML Platform */}
<div className="mb-8">
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
AI/ML Platform AI/ML Platform
</h3> </h3>
<p className="text-gray-700 dark:text-gray-300 mb-3"> <div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800 mb-6">
<p className="text-gray-700 dark:text-gray-300 mb-4">
Self-hosted LLM inference with multi-tenant GPU scheduling and 60% latency reduction. Self-hosted LLM inference with multi-tenant GPU scheduling and 60% latency reduction.
</p> </p>
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400"> <ul className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<li> <strong>Inference:</strong> vLLM serving Qwen3-32B with INT4 quantization</li> <li> <strong>Inference:</strong> vLLM serving Qwen3-32B with INT4 quantization</li>
<li> <strong>Multi-model:</strong> Ollama for smaller models, hot-swapping</li> <li> <strong>Multi-model:</strong> Ollama for smaller models, hot-swapping</li>
<li> <strong>Embeddings:</strong> TEI for text embeddings and reranking</li> <li> <strong>Embeddings:</strong> TEI for text embeddings and reranking</li>
@@ -193,51 +183,161 @@ export default function HomelabPage() {
</ul> </ul>
</div> </div>
{/* Security */} {/* LLM Stack Diagram */}
<div className="mb-6 bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800"> <div className="mb-6 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden hover:shadow-lg transition-shadow">
<div className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-800 border-b border-gray-200 dark:border-gray-800">
<h4 className="font-semibold text-gray-900 dark:text-white">LLM Inference Architecture</h4>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">Gateway routing to vLLM, Ollama, and TEI on GPU worker node</p>
</div>
<div className="w-full" style={{ height: '700px' }}>
<iframe
src="/diagrams/homelab-llm-stack.html"
className="w-full h-full border-none"
title="LLM Inference Stack"
/>
</div>
</div>
{/* Request Lifecycle Diagram */}
<div className="mb-6 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden hover:shadow-lg transition-shadow">
<div className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-800 border-b border-gray-200 dark:border-gray-800">
<h4 className="font-semibold text-gray-900 dark:text-white">Request Lifecycle</h4>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">Client nginx gateway predictor streaming response</p>
</div>
<div className="w-full" style={{ height: '700px' }}>
<iframe
src="/diagrams/homelab-api-gateway-sequence.html"
className="w-full h-full border-none"
title="API Request Flow"
/>
</div>
</div>
</div>
{/* Data & Security */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800">
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">
Data Platform
</h3>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<li> <strong>PostgreSQL:</strong> CloudNativePG operator with HA, automated failover</li>
<li> <strong>Vector DB:</strong> pgvector extension for AI embeddings</li>
<li> <strong>Streaming:</strong> Kafka/Redpanda for event-driven architecture</li>
<li> <strong>Workflows:</strong> Temporal for durable, long-running processes</li>
</ul>
</div>
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800">
<h3 className="font-semibold text-gray-900 dark:text-white mb-3"> <h3 className="font-semibold text-gray-900 dark:text-white mb-3">
Identity & Security Identity & Security
</h3> </h3>
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400"> <ul className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<li> <strong>SSO:</strong> Authentik OIDC provider with custom claims and group mapping</li> <li> <strong>SSO:</strong> Authentik OIDC provider with custom claims</li>
<li> <strong>RBAC:</strong> Kubernetes RBAC synced with Authentik groups</li> <li> <strong>RBAC:</strong> Kubernetes RBAC synced with Authentik groups</li>
<li> <strong>Secrets:</strong> SOPS-encrypted secrets in git, decrypted at deploy time</li> <li> <strong>Secrets:</strong> SOPS-encrypted secrets in git, decrypted at deploy time</li>
<li> <strong>TLS:</strong> cert-manager with DNS-01 ACME challenges via Cloudflare</li> <li> <strong>TLS:</strong> cert-manager with DNS-01 ACME via Cloudflare</li>
</ul> </ul>
</div> </div>
</div>
{/* Observability */} <div className="bg-purple-50 dark:bg-purple-900/30 border-l-4 border-purple-500 p-4 rounded-r">
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800"> <div className="font-semibold text-purple-900 dark:text-purple-200 mb-1">Key Learnings</div>
<h3 className="font-semibold text-gray-900 dark:text-white mb-3"> <ul className="text-purple-800 dark:text-purple-100 text-sm space-y-1">
Observability Stack <li> Powerline adapters killed etcd consensus at 200ms latency. Ethernet to garage solved it.</li>
</h3> <li> Network is foundationalhardware matters more than code sometimes.</li>
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-400"> <li> Design infrastructure so AI can modify it easily. Declarative + immutable = no surprises.</li>
<li> <strong>Metrics:</strong> Prometheus with custom recording rules, Grafana dashboards</li>
<li> <strong>Logs:</strong> Loki for aggregation, structured logging from all services</li>
<li> <strong>Traces:</strong> Tempo + OpenTelemetry Collector for distributed tracing</li>
<li> <strong>Alerts:</strong> AlertManager Slack integration for incident response</li>
</ul> </ul>
</div> </div>
</section> </motion.section>
{/* Applications */} {/* Chapter 4 */}
<section className="mb-12"> <motion.section
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> initial={{ opacity: 0, y: 40 }}
Self-Hosted Applications whileInView={{ opacity: 1, y: 0 }}
</h2> viewport={{ once: true }}
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800"> className="mb-16 border-l-4 border-yellow-500 pl-8"
<div className="flex flex-wrap gap-2">
{['Paperless-ngx', 'Immich', 'Homarr', 'Portainer', 'Forgejo', 'Authentik'].map((app) => (
<span
key={app}
className="text-sm px-3 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
> >
{app} <div className="mb-6">
<div className="text-sm font-semibold text-yellow-600 dark:text-yellow-400 uppercase tracking-wide mb-2">
Next: The AI System
</div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Poimen: The AI Agent System
</h2>
<p className="text-gray-600 dark:text-gray-400 font-medium">Building</p>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none mb-6">
<p className="text-gray-700 dark:text-gray-300">
Current LLM architecture works seamlessly with lambda/serverless patterns. The key insight: <strong>if AI is powerful because of context, tool-calling, and memory, what if we provide the right context in the right environment?</strong>
</p>
</div>
<ul className="space-y-3 text-gray-700 dark:text-gray-300 mb-6">
<li className="flex gap-3">
<span className="text-yellow-500 font-bold flex-shrink-0"></span>
<span><strong>poimen-memory:</strong> Graph-RAG with wiki-link indexing, 7B model for instruct QA, pgvector + OpenSearch hybrid search</span>
</li>
<li className="flex gap-3">
<span className="text-yellow-500 font-bold flex-shrink-0"></span>
<span><strong>poimen-workflows:</strong> Natural language executable WorkflowSpec via reasoning model, Temporal for durable execution</span>
</li>
<li className="flex gap-3">
<span className="text-yellow-500 font-bold flex-shrink-0"></span>
<span><strong>Activity Knowledge Base:</strong> Informs LLM about timeouts, retry policies, dependencies before routing</span>
</li>
<li className="flex gap-3">
<span className="text-yellow-500 font-bold flex-shrink-0"></span>
<span><strong>Generic state machine:</strong> JSON workflow spec + JSONPath parameter chaining. Temporal enables redrive + free self-hosted</span>
</li>
</ul>
<div className="bg-yellow-50 dark:bg-yellow-900/30 border-l-4 border-yellow-500 p-4 rounded-r">
<div className="font-semibold text-yellow-900 dark:text-yellow-200 mb-1">The Vision</div>
<p className="text-yellow-800 dark:text-yellow-100 text-sm">
Build a skill factory where a model generates workflows and completes them. Each Temporal activity is independently refinedenabling a general-purpose workflow orchestrator that improves through iteration.
</p>
</div>
</motion.section>
{/* Skills Grid */}
<motion.section
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mb-12 mt-24 pt-16 border-t border-gray-200 dark:border-gray-800"
>
<h2 className="text-4xl font-bold text-gray-900 dark:text-white mb-12">
Technologies & Skills Mastered
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{skillCategories.map((category, idx) => (
<motion.div
key={category.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.05 }}
className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800 rounded-lg p-8 border border-gray-200 dark:border-gray-800 hover:shadow-lg transition-shadow h-full"
>
<h3 className="font-bold text-gray-900 dark:text-white mb-6 text-xl">
{category.title}
</h3>
<div className="flex flex-wrap gap-3">
{category.skills.map((skill) => (
<span
key={skill}
className="text-base px-4 py-2 rounded-full bg-white dark:bg-gray-700 text-blue-700 dark:text-blue-300 font-semibold border border-blue-200 dark:border-blue-600 hover:border-blue-400 dark:hover:border-blue-400 transition-colors"
>
{skill}
</span> </span>
))} ))}
</div> </div>
</motion.div>
))}
</div> </div>
</section> </motion.section>
</div> </div>
</main> </main>
) )
+2 -1
View File
@@ -29,6 +29,7 @@ export default function Home() {
: undefined, : undefined,
videoUrl: '#', videoUrl: '#',
articleUrl: index === 4 ? 'https://lnkd.in/p/gm2PZkWw' : '#', articleUrl: index === 4 ? 'https://lnkd.in/p/gm2PZkWw' : '#',
deepDive: index === 0 ? { url: '/homelab', label: 'Architecture & Deep Dive' } : undefined,
ciRepos: index === 0 ? [ ciRepos: index === 0 ? [
{ label: 'portfolio', repo: 'rock/riotpiao.com', forgejoBase: 'https://forgejo.riotpiao.com/rock/riotpiao.com' }, { label: 'portfolio', repo: 'rock/riotpiao.com', forgejoBase: 'https://forgejo.riotpiao.com/rock/riotpiao.com' },
{ label: 'homelab-frontend', repo: 'rock/homelab-frontend', forgejoBase: 'https://forgejo.riotpiao.com/rock/homelab-frontend' }, { label: 'homelab-frontend', repo: 'rock/homelab-frontend', forgejoBase: 'https://forgejo.riotpiao.com/rock/homelab-frontend' },
@@ -136,7 +137,7 @@ export default function Home() {
<div className="max-w-6xl mx-auto text-center text-sm text-gray-600 dark:text-gray-400"> <div className="max-w-6xl mx-auto text-center text-sm text-gray-600 dark:text-gray-400">
<p>{t.footer.copyright}</p> <p>{t.footer.copyright}</p>
<p className="mt-2"> <p className="mt-2">
<a href="https://github.com/rockliang" target="_blank" rel="noopener noreferrer" className="hover:text-blue-600 dark:hover:text-blue-400"> <a href="https://github.com/Riotpiaole" target="_blank" rel="noopener noreferrer" className="hover:text-blue-600 dark:hover:text-blue-400">
{t.footer.github} {t.footer.github}
</a> </a>
{' • '} {' • '}
+8 -8
View File
@@ -117,7 +117,7 @@ export default function Header() {
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<a <a
href="https://github.com/rockliang" href="https://github.com/Riotpiaole"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 flex-1" className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 flex-1"
@@ -125,7 +125,7 @@ export default function Header() {
{t.header.contact.github} {t.header.contact.github}
</a> </a>
<button <button
onClick={() => handleCopy('https://github.com/rockliang', 'GitHub')} onClick={() => handleCopy('https://github.com/Riotpiaole', 'GitHub')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900" className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
> >
{copied === 'GitHub' ? '✓' : t.header.contact.copy} {copied === 'GitHub' ? '✓' : t.header.contact.copy}
@@ -133,7 +133,7 @@ export default function Header() {
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<a <a
href="https://github.com/rockliang" href="https://forgejo.riotpiao.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 flex-1" className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 flex-1"
@@ -141,7 +141,7 @@ export default function Header() {
{t.header.contact.privateGithub} {t.header.contact.privateGithub}
</a> </a>
<button <button
onClick={() => handleCopy('https://github.com/rockliang', 'PrivateGithub')} onClick={() => handleCopy('https://forgejo.riotpiao.com', 'PrivateGithub')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900" className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
> >
{copied === 'PrivateGithub' ? '✓' : t.header.contact.copy} {copied === 'PrivateGithub' ? '✓' : t.header.contact.copy}
@@ -314,7 +314,7 @@ export default function Header() {
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<a <a
href="https://github.com/rockliang" href="https://github.com/Riotpiaole"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 flex-1" className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 flex-1"
@@ -322,7 +322,7 @@ export default function Header() {
{t.header.contact.github} {t.header.contact.github}
</a> </a>
<button <button
onClick={() => handleCopy('https://github.com/rockliang', 'GitHub')} onClick={() => handleCopy('https://github.com/Riotpiaole', 'GitHub')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900" className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
> >
{copied === 'GitHub' ? '✓' : t.header.contact.copy} {copied === 'GitHub' ? '✓' : t.header.contact.copy}
@@ -330,7 +330,7 @@ export default function Header() {
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<a <a
href="https://github.com/rockliang" href="https://forgejo.riotpiao.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 flex-1" className="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 flex-1"
@@ -338,7 +338,7 @@ export default function Header() {
{t.header.contact.privateGithub} {t.header.contact.privateGithub}
</a> </a>
<button <button
onClick={() => handleCopy('https://github.com/rockliang', 'PrivateGithub')} onClick={() => handleCopy('https://forgejo.riotpiao.com', 'PrivateGithub')}
className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900" className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
> >
{copied === 'PrivateGithub' ? '✓' : t.header.contact.copy} {copied === 'PrivateGithub' ? '✓' : t.header.contact.copy}
+27
View File
@@ -0,0 +1,27 @@
apiVersion: ENC[AES256_GCM,data:Qv8=,iv:v6kMpvgzkpGdOzuwurpWhVJG8dqjHG55mZebDs0PeWs=,tag:9W00OTNe1kQ9mA/l0eSINg==,type:str]
kind: ENC[AES256_GCM,data:TUqF1gRXmGmc,iv:ettHZPLG+Jw29TuSmeUNi4DsK944JNgvmu6hry3osSo=,tag:CbtvTOVxpWivW6UyO7MrLA==,type:str]
metadata:
name: ENC[AES256_GCM,data:0YEEc2oqKeGjWr7C0eAzz0dZft0=,iv:BnCIMRZAsDdW3IxIAhsj/WrZNCipG5qT1kfnuQykt20=,tag:EFjanh4e6T5bQX6dupXRgw==,type:str]
namespace: ENC[AES256_GCM,data:MheF93lBY58x,iv:Mp64W8Beu+q8NiQTtUhpAxwcfT4JPkLenjh7ljJaGak=,tag:ves1LGuzj1QGY1OpqF560g==,type:str]
labels:
app.kubernetes.io/name: ENC[AES256_GCM,data:i+aoOfomVrFm,iv:/g+F6ZaWDvzjjv4P35rgcBMSSAyrTyWymm//KVRrpYg=,tag:UIn8tKERKolTYq/bVdQ+iA==,type:str]
data:
#ENC[AES256_GCM,data:9whKTa+VQQtYnOrrTanXJpPZltIEiAh029xIu9YhAqcaXQ2I6XTmCU8LqyNrQKNw5CWJJg0F,iv:Sm5T9g2aJQz0qrNzA1B8QBGRkio6u5AAc+hWoUIs0bU=,tag:jxZ0llX6pKYBsvtRxCdgaA==,type:comment]
#ENC[AES256_GCM,data:XLVVNIgolPDi5pkm/S6IZJHZIj+84b/1VTwLCnrePwFmDrIfRdJAG7LbxWFmRJtaO2TEHnd2FEHIEg==,iv:YkfB3Q26+PHqd+51gAZOXoJKPpvIeT5wN0Ye10spdYM=,tag:spTTVuE52SvDuTo3LQXqOw==,type:comment]
LLM_API_URL: ENC[AES256_GCM,data:dJRd+J1ovECOIctWhwMYuhv3rReqmwUHkcN8fxtxLfohIqg8/LOsPw5FE2B9GrEzjnYwTcku6vyvb+CCCJMLmDE=,iv:23Jo8dgWeMLX+uYyFQXPa1cHqKVTPs6qHI4i/Go6lTY=,tag:KuRtrTdzKk3uf9ph5Qp5Nw==,type:str]
LLM_MODEL: ENC[AES256_GCM,data:lRDaz2Fey+2czl3K1Qju/82XNw==,iv:nJR+wNNGKDCkYdToEfERqYZdVS34TmVvMVRjLHYFgE8=,tag:XOiZmi9k+54yJm4ofUiXFw==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkWEpWNFQ4TXNlSG5NcVFB
Tll2OUFFUjV4RDAvOE5ZSWRZZE5YL2xGYkY4CjI4SFp5OXJlcHd0WG9vTVFwM2do
YVVleVhCam9qeGVhL05xMG5mMWorNWMKLS0tIHU4MVNuSjkwNkNKTi93OXFkQlJh
Y28wbW9uK21zYzNWUzE2MzJKN0t2cFUKlgVpvion4Qg7DFw/arUZupyy1d7I59P4
njaeu0pTj6StNiZfHhCguVWpYBECnSaF5A4aoLFbUFVpdL/JC8V2GQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
lastmodified: "2026-09-07T06:23:56Z"
mac: ENC[AES256_GCM,data:2ufxupBbFbV08JJoN3ySbehsCqmAfNt6anUeaWg4JlxvTN+51yd8NeW76HJ8yo0MSf7FjjnX83CHeuVmcJaIXcTTFjKW4Dp3Ww5LYq9PESQu6ywoeXSfsrYH+T1WKAUCJq/HJOYHTvnpS4zSnHfDA8madHWjJI3/Hl1haC0cKyE=,iv:/Xv//b2mBrr+Rye01YNjvdUp4zLi/+Ly+4RCqG2RO+s=,tag:g+7rr4dxXbkATSBVQ4l0Wg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.2
+12 -3
View File
@@ -6,6 +6,9 @@ metadata:
labels: labels:
app.kubernetes.io/name: portfolio app.kubernetes.io/name: portfolio
app.kubernetes.io/component: web app.kubernetes.io/component: web
annotations:
argocd-image-updater.argoproj.io/image-list: portfolio=forgejo.riotpiao.com/rock/portfolio
argocd-image-updater.argoproj.io/portfolio.update-strategy: latest
spec: spec:
replicas: 2 replicas: 2
selector: selector:
@@ -42,11 +45,17 @@ spec:
secretKeyRef: secretKeyRef:
name: portfolio-agent-oidc name: portfolio-agent-oidc
key: TOKEN_URL key: TOKEN_URL
# LLM API configuration # LLM API configuration (from encrypted ConfigMap)
- name: LLM_API_URL - name: LLM_API_URL
value: "https://api.riotpiao.com/v1/chat/completions" valueFrom:
configMapKeyRef:
name: portfolio-llm-config
key: LLM_API_URL
- name: LLM_MODEL - name: LLM_MODEL
value: "qwen2.5:3b-instruct" valueFrom:
configMapKeyRef:
name: portfolio-llm-config
key: LLM_MODEL
ports: ports:
- name: http - name: http
containerPort: 3000 containerPort: 3000
+14
View File
@@ -1,8 +1,22 @@
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
namespace: portfolio namespace: portfolio
resources: resources:
- namespace.yaml - namespace.yaml
- deployment.yaml - deployment.yaml
- service.yaml - service.yaml
- ingress.yaml - ingress.yaml
# SOPS-encrypted secrets via ksops generator (ArgoCD repo-server plugin)
# ksops intercepts encrypted files, decrypts them, returns valid resources
generatorOptions:
disableNameSuffixHash: true
generators:
- secret-generator.yaml
# ArgoCD Image Updater configuration - for tag updates
images:
- name: forgejo.riotpiao.com/rock/portfolio
newTag: latest
@@ -0,0 +1,10 @@
apiVersion: viaduct.ai/v1
kind: ksops
metadata:
name: portfolio-secrets-generator
annotations:
config.kubernetes.io/function: |
exec:
path: ksops
files:
- configmap.enc.yaml
+3 -2
View File
@@ -12,9 +12,10 @@
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"packageManager": "[email protected]",
"dependencies": { "dependencies": {
"framer-motion": "^11.0.0", "framer-motion": "^11.0.0",
"lucide-react": "^0.344.0", "lucide-react": "^1.41.0",
"next": "^15.5.20", "next": "^15.5.20",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
@@ -28,7 +29,7 @@
"@typescript-eslint/eslint-plugin": "^8.64.0", "@typescript-eslint/eslint-plugin": "^8.64.0",
"@typescript-eslint/parser": "^8.64.0", "@typescript-eslint/parser": "^8.64.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"eslint": "^8.57.1", "eslint": "^9.0.0",
"eslint-config-next": "^16.2.10", "eslint-config-next": "^16.2.10",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"typescript": "5.8.2" "typescript": "5.8.2"
+59
View File
@@ -0,0 +1,59 @@
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/results.xml' }],
['list'],
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Mobile testing
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
// Tablet
{
name: 'iPad',
use: { ...devices['iPad Pro'] },
},
],
webServer: {
command: 'pnpm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
})
+849 -747
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+63
View File
@@ -0,0 +1,63 @@
/**
* Shared test data and constants for E2E tests
*/
export const TEST_VIEWPORTS = {
desktop: { width: 1920, height: 1080 },
tablet: { width: 768, height: 1024 },
mobile: { width: 375, height: 667 },
}
export const TEST_SELECTORS = {
// Hero section
heroTitle: 'h1',
heroSubtitle: 'text=Kubernetes',
exploreMoreBtn: 'button:has-text("Explore More")',
terminalHint: 'text=Terminal',
// Bio modal
bioModal: '.bg-white.dark\\:bg-gray-900.rounded-lg',
bioTitle: 'text=Bio',
bioCloseBtn: 'button:has-text("x")',
bioIntro: 'text=Welcome',
// Projects section
projectsSection: 'text=Projects',
projectCard: '[class*="ProjectShowcase"]',
projectTitle: 'text=Homelab',
// Timeline
timelineSection: 'text=Experience',
// Terminal
terminalContainer: '[class*="InteractiveTerminal"]',
terminalInput: 'input[placeholder*="terminal"]',
// Dark mode
themeToggle: 'button[aria-label*="theme"]',
// Links
watchVideoLink: 'text=Watch Video',
readArticleLink: 'text=Read Article',
}
export const TEST_TIMEOUTS = {
slow: 10000,
normal: 5000,
quick: 2000,
}
export const TEST_PROJECTS = [
'Homelab',
'Poimen Memory',
'Poimen Workflow',
'RBC',
'AWS',
]
export const TEST_BREAKPOINTS = {
mobile: 375,
tablet: 768,
desktop: 1280,
wide: 1920,
}