feat: add architecture diagrams page with archify visualizations
CI / CI (pull_request) Failing after 1m46s
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
This commit is contained in:
@@ -0,0 +1,301 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { ArrowLeft, Clock, Zap, BookOpen } from 'lucide-react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
chapter: 'Chapter 1',
|
||||||
|
title: 'AWS Step Functions: The Foundation',
|
||||||
|
period: '2022–2024',
|
||||||
|
icon: '🏗️',
|
||||||
|
highlights: [
|
||||||
|
'Owned Distributed-Map end-to-end: design → production across 57+ regions',
|
||||||
|
'Sub-100ms P99 latency, 20x burst handling',
|
||||||
|
'Learned deployment alignment: frontend spec updates must sync with service deployments',
|
||||||
|
'Built Checkpoint recovery: customers resume mid-workflow without re-runs',
|
||||||
|
'Solved distributed edge cases: race conditions, concurrent updates, dedup',
|
||||||
|
'Oncall mastery: CloudWatch dashboards, runbooks, production debugging'
|
||||||
|
],
|
||||||
|
keyLearning: 'Backward compatibility is invisible until it breaks silently. A missed spec change cascades across regions.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chapter: 'Chapter 2',
|
||||||
|
title: 'RBC: Infrastructure as Code & Observability',
|
||||||
|
period: 'Nov 2024 – May 2025',
|
||||||
|
icon: '⚡',
|
||||||
|
highlights: [
|
||||||
|
'Problem: Unstable deployments with 500+ resource state files, API rate-limits, race conditions',
|
||||||
|
'Solution: JFrog Artifactory backend + workspace prefixes, plan artifacts, -parallelism=5 throttle',
|
||||||
|
'Result: Zero state corruption, zero pipeline blockage',
|
||||||
|
'Problem: Configuration drift invisible for weeks, massive unreadable diffs',
|
||||||
|
'Solution: Nightly terraform plan -refresh-only -detailed-exitcode → Slack webhook',
|
||||||
|
'Result: Drift visibility from 3 weeks → <24 hours'
|
||||||
|
],
|
||||||
|
keyLearning: 'Terraform wins for things that barely change. For K8s resources that churn (pods, configmaps), you need a different tool—GitOps.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chapter: 'Chapter 3',
|
||||||
|
title: 'Homelab: Building AWS from Scratch',
|
||||||
|
period: 'May 2025 – Present',
|
||||||
|
icon: '🚀',
|
||||||
|
highlights: [
|
||||||
|
'Question: How does LLM serving work at scale? Build it at home.',
|
||||||
|
'4 bare-metal machines: 1 GPU node, 1 Dell PowerEdge, 2 mini-desktops',
|
||||||
|
'Discovery: Powerline adapters killed etcd consensus. Ran ethernet to garage.',
|
||||||
|
'Wildcard DNS via Cloudflare, HTTPS for all subdomains (cert-manager + Let\'s Encrypt)',
|
||||||
|
'GitOps split: Terraform for Talos machine config, ArgoCD for K8s resources',
|
||||||
|
'99.2% uptime with Talos Linux, Cilium CNI, Longhorn, PostgreSQL HA, Prometheus+Grafana+Loki'
|
||||||
|
],
|
||||||
|
keyLearning: 'Design infrastructure so AI can modify it easily. GitOps + immutable OS = no surprises.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chapter: 'Chapter 4',
|
||||||
|
title: 'Poimen: AI Meets Workflows',
|
||||||
|
period: 'Building',
|
||||||
|
icon: '🤖',
|
||||||
|
highlights: [
|
||||||
|
'Insight: AI is powerful because of context, tool-calling, and memory.',
|
||||||
|
'poimen-memory: Graph-RAG with wiki-link indexing, pgvector + OpenSearch hybrid search',
|
||||||
|
'poimen-workflows: Natural language → executable Temporal workflows via reasoning model',
|
||||||
|
'Activity Knowledge Base informs LLM about timeouts, retries, dependencies',
|
||||||
|
'Generic state machine: JSON workflow spec + JSONPath parameter chaining',
|
||||||
|
'60% latency reduction for LLM serving (vLLM INT4 quantization, custom Go gateway)'
|
||||||
|
],
|
||||||
|
keyLearning: 'Skill factory: each Temporal activity is independently refined. General-purpose workflow orchestration emerges.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const technologies = {
|
||||||
|
'Infrastructure': ['Talos Linux', 'Kubernetes', 'Cilium CNI', 'Terraform', 'kustomize'],
|
||||||
|
'Data': ['PostgreSQL + HA', 'pgvector', 'Kafka/Redpanda', 'MinIO', 'Longhorn'],
|
||||||
|
'GitOps': ['ArgoCD', 'Forgejo', 'Docker-in-Docker'],
|
||||||
|
'Identity': ['Authentik OIDC', 'RBAC', 'SOPS encrypted secrets', 'cert-manager'],
|
||||||
|
'Observability': ['Prometheus', 'Grafana', 'Loki', 'Tempo', 'OpenTelemetry'],
|
||||||
|
'AI/ML': ['vLLM', 'Ollama', 'TEI', 'KServe', 'Temporal', 'Qwen3-32B'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyInsights = [
|
||||||
|
{
|
||||||
|
title: 'Deployment Alignment',
|
||||||
|
description: 'When frontend consumes the latest API, you need backward-compat checks. A missed spec change breaks customers silently across all regions.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Infrastructure as Code Split',
|
||||||
|
description: 'Terraform for things that rarely change (machine config). GitOps for things that churn (pods, configmaps). Different tools, different philosophies.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Network is Critical',
|
||||||
|
description: 'Powerline adapters killed etcd consensus at 200ms latency. Ethernet cable to garage solved it. Hardware matters.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'AI-Friendly Infra',
|
||||||
|
description: 'Design systems so AI can modify them. Declarative configs + clear abstractions = easier for models to reason about.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Observability First',
|
||||||
|
description: 'Drift detection <24hr. Prometheus + Grafana dashboards before you have problems. Not post-mortem tools.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function HomelabArticle() {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-white dark:bg-gray-950">
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-900 border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<div className="max-w-3xl mx-auto px-6 py-16">
|
||||||
|
<Link
|
||||||
|
href="/homelab"
|
||||||
|
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:underline mb-8"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
Back to Homelab
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
<h1 className="text-5xl font-bold text-gray-900 dark:text-white mb-4 leading-tight">
|
||||||
|
Building AWS at Home:<br />A 3-Year Journey
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-400 mb-6">
|
||||||
|
From Step Functions to LLM inference—how distributed systems knowledge compounds.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock size={16} />
|
||||||
|
<span>12 min read</span>
|
||||||
|
</div>
|
||||||
|
<span>•</span>
|
||||||
|
<span>May 2025</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<article className="max-w-3xl mx-auto px-6 py-20">
|
||||||
|
{/* Intro */}
|
||||||
|
<motion.section
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="mb-16 prose prose-lg dark:prose-invert max-w-none"
|
||||||
|
>
|
||||||
|
<p className="text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-4">
|
||||||
|
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 way—first at AWS, then at RBC, and now at home.
|
||||||
|
</p>
|
||||||
|
<p className="text-lg text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||||
|
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.section>
|
||||||
|
|
||||||
|
{/* Chapters */}
|
||||||
|
<div className="space-y-24">
|
||||||
|
{sections.map((section, idx) => (
|
||||||
|
<motion.section
|
||||||
|
key={section.chapter}
|
||||||
|
initial={{ opacity: 0, y: 40 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="border-l-4 border-blue-500 pl-8"
|
||||||
|
>
|
||||||
|
<div className="flex items-baseline gap-3 mb-4">
|
||||||
|
<span className="text-4xl">{section.icon}</span>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold text-blue-600 dark:text-blue-400 uppercase tracking-wide">
|
||||||
|
{section.chapter}
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{section.title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400 mb-6 font-medium">
|
||||||
|
{section.period}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Highlights */}
|
||||||
|
<ul className="space-y-3 mb-8">
|
||||||
|
{section.highlights.map((highlight, i) => (
|
||||||
|
<li key={i} className="flex gap-3 text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="text-blue-500 font-bold mt-0.5 flex-shrink-0">•</span>
|
||||||
|
<span>{highlight}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Key Learning */}
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/30 border-l-4 border-blue-500 p-4 rounded-r">
|
||||||
|
<div className="font-semibold text-blue-900 dark:text-blue-200 mb-1">Key Learning</div>
|
||||||
|
<p className="text-blue-800 dark:text-blue-100 text-sm">
|
||||||
|
{section.keyLearning}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Insights */}
|
||||||
|
<motion.section
|
||||||
|
initial={{ opacity: 0, y: 40 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="mt-24 pt-16 border-t border-gray-200 dark:border-gray-800"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-8">
|
||||||
|
<Zap size={24} className="text-yellow-500" />
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Core Insights
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{keyInsights.map((insight, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ delay: idx * 0.05 }}
|
||||||
|
className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 border border-gray-200 dark:border-gray-800"
|
||||||
|
>
|
||||||
|
<h4 className="font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
{insight.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{insight.description}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
|
||||||
|
{/* Tech Stack */}
|
||||||
|
<motion.section
|
||||||
|
initial={{ opacity: 0, y: 40 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="mt-24 pt-16 border-t border-gray-200 dark:border-gray-800"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-8">
|
||||||
|
<BookOpen size={24} className="text-purple-500" />
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Technologies Mastered
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
{Object.entries(technologies).map(([category, techs]) => (
|
||||||
|
<motion.div key={category}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
>
|
||||||
|
<h4 className="font-bold text-gray-900 dark:text-white mb-3">
|
||||||
|
{category}
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{techs.map((tech) => (
|
||||||
|
<span
|
||||||
|
key={tech}
|
||||||
|
className="text-xs px-3 py-1.5 rounded-full bg-gradient-to-r from-blue-100 to-purple-100 dark:from-blue-900 dark:to-purple-900 text-gray-800 dark:text-gray-200 font-medium"
|
||||||
|
>
|
||||||
|
{tech}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<motion.section
|
||||||
|
initial={{ opacity: 0, y: 40 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="mt-24 pt-16 border-t border-gray-200 dark:border-gray-800 text-center"
|
||||||
|
>
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Want the full details?
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||||
|
Ask Poimen any technical question about the architecture, deployment strategies, or lessons learned.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/#project-homelab"
|
||||||
|
className="inline-block bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg font-semibold transition"
|
||||||
|
>
|
||||||
|
Ask Poimen →
|
||||||
|
</Link>
|
||||||
|
</motion.section>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
+230
-130
@@ -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 home—full 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 way—first 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 Linux—immutable, 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 Linux—immutable, 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 everything—but 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 foundational—hardware 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 refined—enabling 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
@@ -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>
|
||||||
{' • '}
|
{' • '}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
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
@@ -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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user