6 Commits

Author SHA1 Message Date
lerko96
22660bed7a fix(theme): unblock light mode in dev
Hardcoded class="dark" on <html> meant React owned it via JSX. HMR
re-renders and reconciliation kept restoring the class after
classList.toggle removed it, so light toggle never stuck.

ThemeScript already handles initial paint; suppressHydrationWarning
covers the post-script class mismatch.
2026-04-27 00:49:57 -04:00
lerko96
7f614d28b5 feat(projects): consolidate /projects, hide skills, redirect /archive
- /projects: merged page with featured (top) + archive (bottom)
- titles mirror homelab pattern: projects/featured, projects/archive
- nav: archive → projects
- home: drop Skills section and featured grid
- /archive → /projects via meta-refresh + JS redirect stub
2026-04-27 00:49:52 -04:00
lerko96
e9d7a994c7 chore(content): refresh projects + timeline
- featured: swap helm → nib, drop claude-vault, reorder
- helm → archive
- homelab description: 7 VLANs + Wireguard VPN (matches access-tier framing)
- timeline: add Proxmox Backup Server 2025; pfSense entry corrected to Netgate 1100
2026-04-27 00:49:47 -04:00
lerko96
f6118aa7a4 refactor(homelab): rename VLAN col to Segment for VPN row
VPN is an L3 tunnel, not an 802.1Q VLAN. Reframes the table as
network segments / access tiers so the VPN row is consistent with
the others.
2026-04-27 00:49:42 -04:00
lerko96
5dea6121a3 docs(homelab): trim operational detail from network table and ADRs
Pure copy edit. Page now publishes the reasoning behind decisions, not
the operational specifics (IPs, subnets, ports, hardware fingerprints,
build pipeline mechanics). Reasoning preserved in every ADR.

- VLAN table: drop Subnet column; replace numeric VLAN IDs with tier names
- ISP gateway ADR: drop carrier and gateway model
- Caddy ADR: tighten DNS-01 framing to internal-services exposure; SSL → TLS
- WireGuard ADR: drop port, VPN subnet, throughput numbers, tier enumeration
- Pi-hole ADR: drop host IP and VLAN ID; sharpen trade-off
- N100 ADR: drop core/clock and precise throughput; rename to "Mini-PC"
- Postgres+Redis ADR: drop apps LXC IP
- Gitea CI/CD ADR: drop runner version, build image, host IPs, deploy mechanics
- Authentik ADR: unchanged
2026-04-26 23:19:16 -04:00
4e51dd4a83 feat/polish (#5)
All checks were successful
Build and Deploy / deploy (push) Successful in 51s
Reorder homepage sections (journey above projects), refine component styles, update copy and data across projects, timeline, and homelab.

Reviewed-on: #5
2026-04-20 00:34:50 +00:00
8 changed files with 164 additions and 149 deletions

View File

@@ -1,68 +1,24 @@
import type { Metadata } from "next"; "use client";
import Widget from "@/components/Widget";
import { archiveProjects } from "@/data/projects";
export const metadata: Metadata = { import { useEffect } from "react";
title: "Archive | Tyler Koenig",
description: "Earlier projects and experiments — browser extensions, canvas apps, and bootcamp work.", export default function ArchiveRedirect() {
}; useEffect(() => {
window.location.replace("/projects/");
}, []);
export default function ArchivePage() {
return ( return (
<> <>
<div className="mb-4lh"> <meta httpEquiv="refresh" content="0; url=/projects/" />
<p className="font-mono text-sm font-bold text-[var(--color-text)] mb-1lh"> <p className="font-mono text-sm text-[var(--color-text)]">
<span className="text-[var(--color-accent-green)] select-none mr-1ch" aria-hidden="true"></span> This page moved.{" "}
tyler/projects/archive <a
</p> href="/projects/"
<p className="font-mono text-sm text-[var(--color-text)] leading-relaxed max-w-xl opacity-80"> className="text-[var(--color-accent-green)] underline"
Experiments, browser extensions, and bootcamp projects. Kept here for context not >
representative of current work. /projects/
</p> </a>
</div> </p>
<Widget title="tyler/projects/archive" badge={archiveProjects.length} as="section">
<div className="flex flex-col gap-px bg-[var(--color-border)]">
{archiveProjects.map((project) => (
<a
key={project.slug}
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="bg-[var(--color-surface)] hover:bg-[var(--color-surface-raised)] flex items-start justify-between gap-2ch px-2ch py-1lh group"
>
<div className="flex flex-col gap-1ch flex-1 min-w-0">
<div className="flex items-center gap-1ch">
{project.year && (
<span className="font-mono text-sm text-[var(--color-text-dim)] shrink-0">
{project.year}
</span>
)}
<span className="font-mono text-sm text-[var(--color-text)] group-hover:text-[var(--color-accent-green)] truncate">
{project.title}
</span>
</div>
<p className="font-mono text-sm text-[var(--color-text)] leading-relaxed opacity-75">
{project.description}
</p>
<div className="flex flex-wrap gap-x-1ch gap-y-0.5">
{project.tags.map((tag) => (
<span key={tag} className="font-mono text-sm text-[var(--color-text-dim)]">
{tag}
</span>
))}
</div>
</div>
<span
className="font-mono text-sm text-[var(--color-text-label)] group-hover:text-[var(--color-text)] shrink-0 mt-0.5"
aria-hidden="true"
>
</span>
</a>
))}
</div>
</Widget>
</> </>
); );
} }

View File

@@ -22,98 +22,90 @@ const glanceStats = [
const vlans = [ const vlans = [
{ {
id: "1000", id: "MGMT",
name: "MGMT", name: "MGMT",
subnet: "10.0.0.0/24",
purpose: "Network equipment only", purpose: "Network equipment only",
}, },
{ {
id: "1010", id: "LAN",
name: "LAN", name: "LAN",
subnet: "10.1.0.0/24",
purpose: "Trusted personal devices", purpose: "Trusted personal devices",
}, },
{ {
id: "1020", id: "Lab",
name: "Homelab", name: "Homelab",
subnet: "10.2.0.0/24",
purpose: "All self-hosted services", purpose: "All self-hosted services",
}, },
{ {
id: "1030", id: "Guest",
name: "Guests", name: "Guests",
subnet: "10.3.0.0/24",
purpose: "Internet only, RFC1918 blocked", purpose: "Internet only, RFC1918 blocked",
}, },
{ {
id: "1040", id: "IoT",
name: "IoT", name: "IoT",
subnet: "10.4.0.0/24",
purpose: "Smart home, isolated", purpose: "Smart home, isolated",
}, },
{ {
id: "1050", id: "WFH",
name: "WFH", name: "WFH",
subnet: "10.5.0.0/24",
purpose: "Work devices, no personal access", purpose: "Work devices, no personal access",
}, },
{ {
id: "1099", id: "DMZ",
name: "DMZ", name: "DMZ",
subnet: "10.99.0.0/24",
purpose: "Public-facing, hard-blocked internally", purpose: "Public-facing, hard-blocked internally",
}, },
{ {
id: "VPN", id: "VPN",
name: "VPN", name: "VPN",
subnet: "10.200.0.0/24", purpose: "WireGuard clients, LAN-equivalent access",
purpose: "WireGuard clients = LAN access",
}, },
]; ];
const adrs = [ const adrs = [
{ {
title: "AT&T Gateway: IP Passthrough over EAP bypass", title: "ISP gateway: passthrough mode",
decision: decision:
"BGW320 stays in-line with IP Passthrough mode. pfSense gets the public IP directly. Gateway WiFi disabled.", "ISP gateway stays in-line in passthrough mode, pfSense gets the public IP directly. Gateway WiFi disabled.",
why: "AT&T locks 802.1X auth to their gateway hardware. EAP proxy bypass is brittle — breaks on firmware updates and only saves 12ms. True bridge mode isn't supported.", why: "Carrier locks 802.1X auth to their own gateway hardware, and bypassing it is brittle — breaks on firmware updates and only saves a millisecond or two. True bridge mode isn't supported. Passthrough is the cleanest option that keeps pfSense as the actual perimeter.",
}, },
{ {
title: "Caddy over NGINX Proxy Manager", title: "Caddy over NGINX Proxy Manager",
decision: decision:
"Caddy with DNS-01 challenge via Cloudflare API. All subdomains resolve to Caddy internally via Pi-hole. Caddy terminates SSL and proxies to backends.", "Caddy with DNS-01 challenge via Cloudflare API. All subdomains resolve to Caddy internally via Pi-hole. Caddy terminates TLS and proxies to backends.",
why: "Single Caddyfile, auto-cert without exposing port 80/443 to the internet. NPM has more UI overhead for the same outcome. Traefik is more complex for no benefit here.", why: "Single Caddyfile, automatic certs without ever needing to expose internal services to the internet for an HTTP-01 challenge. NPM has more UI overhead for the same outcome. Traefik is more complex for no benefit at this scale.",
}, },
{ {
title: "WireGuard over OpenVPN", title: "WireGuard over OpenVPN",
decision: decision:
"WireGuard on pfSense, UDP 51820, VPN subnet 10.200.0.0/24. Clients get LAN + MGMT access, blocked from Guest/IoT/WFH.", "WireGuard on pfSense as the only remote-access path. Clients get the access tier documented in the access model — same as LAN, plus the admin surfaces that aren't reachable any other way.",
why: "Faster, simpler config, better battery life on mobile. ~600900 Mbps on an N100. OpenVPN has no advantage here. Tailscale adds an external relay dependency.", why: "Faster, simpler config, better battery life on mobile. Throughput on the firewall hardware comfortably exceeds the WAN link. OpenVPN has no advantage here. Tailscale would add an external relay dependency for a problem WireGuard already solves.",
}, },
{ {
title: "Pi-hole in Homelab VLAN, not MGMT", title: "Pi-hole in Homelab VLAN, not MGMT",
decision: decision:
"Pi-hole at 10.2.0.11 (VLAN 1020). Firewall allows port 53 inbound from all VLANs. MGMT uses pfSense Unbound as its primary DNS.", "Pi-hole runs in the Homelab VLAN. Firewall allows port 53 inbound from VLANs that need local resolution. MGMT uses pfSense Unbound as its primary resolver instead.",
why: "Putting Pi-hole in MGMT would require opening MGMT to all VLANs — a larger attack surface. DNS traffic crossing into Homelab VLAN is the lesser risk.", why: "Putting Pi-hole in MGMT would mean opening MGMT to all the VLANs that need DNS — much bigger attack surface for the most sensitive tier. DNS traffic crossing into the Homelab VLAN is the lesser risk, and Homelab is already where service traffic terminates anyway.",
}, },
{ {
title: "N100 for pfSense", title: "Mini-PC for pfSense",
decision: decision:
"Intel N100 mini PC: 4-core 3.4 GHz, ~6W idle. Handles 23 Gbps routing and 600900 Mbps WireGuard.", "Intel N100 mini-PC as the firewall host. ~6W idle, handles multi-Gbps routing, saturates the WAN link with WireGuard headroom to spare.",
why: "Right-sized for 1 Gbps fiber with headroom. Raspberry Pi can't handle 1 Gbps + VPN. A full rack server wastes power for this role.", why: "Right-sized for 1 Gbps fiber. A Raspberry Pi can't handle 1 Gbps plus VPN. A full rack server wastes power for this role and adds noise to a room I sit in.",
}, },
{ {
title: "Shared Postgres + Redis in apps LXC", title: "Shared Postgres + Redis in apps LXC",
decision: decision:
"One Postgres instance, multiple databases. One Redis instance. A single init script provisions all schemas on first run.", "One Postgres instance hosting multiple databases. One Redis instance. A single init script provisions schemas on first run.",
why: "Avoids 15 separate DB containers. Reduces RAM overhead significantly. All productivity apps share the same LXC (10.2.0.60).", why: "Avoids ~15 separate DB containers. Big RAM savings. Productivity apps colocate in one LXC anyway, so a shared backing store there is the natural shape.",
}, },
{ {
title: title:
"Gitea CI/CD: Self-hosted runner with container build + SSH rsync deploy", "Gitea CI/CD: self-hosted runner, internal pipeline, static deploy",
decision: decision:
"act_runner v0.3.1 on Gitea LXC (10.99.0.22). Push to dev → node:22-alpine container builds Next.js → rsync out/ to Portfolio LXC → SSH docker rebuild.", "Self-hosted Gitea Actions runner builds the portfolio on push, then deploys pre-built static files to the public-facing host. Build runs in an isolated container so the runner host stays clean. Public host serves static files only — no build toolchain on it.",
why: "Keeps the full pipeline internal — no GitHub Actions, no external runners. Build runs in an isolated Alpine container so the Gitea LXC isn't polluted. Portfolio LXC (10.99.0.23) just serves pre-built static files via nginx.", why: "Keeps the whole pipeline internal. No external runners, no GitHub Actions. The build/serve split means the public-facing host has the smallest possible footprint — static file server, nothing more.",
}, },
{ {
title: "Authentik over Authelia", title: "Authentik over Authelia",
@@ -163,7 +155,7 @@ export default function HomelabPage() {
{/* VLAN table */} {/* VLAN table */}
<Widget <Widget
title="homelab/network" title="homelab/network"
meta="8 isolated vlans · default deny inter-vlan" meta="8 network segments · default deny"
as="section" as="section"
> >
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -171,14 +163,11 @@ export default function HomelabPage() {
<thead> <thead>
<tr className="border-b border-[var(--color-border)]"> <tr className="border-b border-[var(--color-border)]">
<th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase"> <th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase">
VLAN Segment
</th> </th>
<th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase"> <th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase">
Name Name
</th> </th>
<th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase">
Subnet
</th>
<th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh uppercase"> <th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh uppercase">
Purpose Purpose
</th> </th>
@@ -196,9 +185,6 @@ export default function HomelabPage() {
<td className="font-mono text-[var(--color-text)] py-half-lh pr-[3ch]"> <td className="font-mono text-[var(--color-text)] py-half-lh pr-[3ch]">
{v.name} {v.name}
</td> </td>
<td className="font-mono text-[var(--color-text-label)] py-half-lh pr-[3ch]">
{v.subnet}
</td>
<td className="font-mono text-sm text-[var(--color-text)] py-2.5 opacity-80"> <td className="font-mono text-sm text-[var(--color-text)] py-2.5 opacity-80">
{v.purpose} {v.purpose}
</td> </td>

View File

@@ -24,7 +24,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en" className="dark"> <html lang="en" suppressHydrationWarning>
<head> <head>
<ThemeScript /> <ThemeScript />
</head> </head>

View File

@@ -1,10 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import Hero from "@/components/Hero"; import Hero from "@/components/Hero";
import Skills from "@/components/Skills";
import Timeline from "@/components/Timeline"; import Timeline from "@/components/Timeline";
import ProjectCard from "@/components/ProjectCard";
import Widget from "@/components/Widget";
import { featuredProjects } from "@/data/projects";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Tyler Koenig", title: "Tyler Koenig",
@@ -17,14 +13,6 @@ export default function Home() {
<> <>
<Hero /> <Hero />
<Timeline /> <Timeline />
<Widget title="tyler/projects" badge={featuredProjects.length}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1ch">
{featuredProjects.map((project) => (
<ProjectCard key={project.slug} project={project} />
))}
</div>
</Widget>
<Skills />
</> </>
); );
} }

77
src/app/projects/page.tsx Normal file
View File

@@ -0,0 +1,77 @@
import type { Metadata } from "next";
import Widget from "@/components/Widget";
import ProjectCard from "@/components/ProjectCard";
import { featuredProjects, archiveProjects } from "@/data/projects";
export const metadata: Metadata = {
title: "Projects | Tyler Koenig",
description:
"Featured projects and earlier work — homelab, open-pact, helm, and bootcamp/experiment archive.",
};
export default function ProjectsPage() {
return (
<>
<div className="mb-4lh">
<p className="font-mono text-sm font-bold text-[var(--color-text)] mb-1lh">
<span className="text-[var(--color-accent-green)] select-none mr-1ch" aria-hidden="true"></span>
projects
</p>
<p className="font-mono text-sm text-[var(--color-text)] leading-relaxed max-w-xl opacity-80">
Featured work first. Earlier experiments, browser extensions, and bootcamp projects below kept for context.
</p>
</div>
<Widget title="projects/featured" badge={featuredProjects.length} as="section">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1ch">
{featuredProjects.map((project) => (
<ProjectCard key={project.slug} project={project} />
))}
</div>
</Widget>
<Widget title="projects/archive" badge={archiveProjects.length} as="section">
<div className="flex flex-col gap-px bg-[var(--color-border)]">
{archiveProjects.map((project) => (
<a
key={project.slug}
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="bg-[var(--color-surface)] hover:bg-[var(--color-surface-raised)] flex items-start justify-between gap-2ch px-2ch py-1lh group"
>
<div className="flex flex-col gap-1ch flex-1 min-w-0">
<div className="flex items-center gap-1ch">
{project.year && (
<span className="font-mono text-sm text-[var(--color-text-dim)] shrink-0">
{project.year}
</span>
)}
<span className="font-mono text-sm text-[var(--color-text)] group-hover:text-[var(--color-accent-green)] truncate">
{project.title}
</span>
</div>
<p className="font-mono text-sm text-[var(--color-text)] leading-relaxed opacity-75">
{project.description}
</p>
<div className="flex flex-wrap gap-x-1ch gap-y-0.5">
{project.tags.map((tag) => (
<span key={tag} className="font-mono text-sm text-[var(--color-text-dim)]">
{tag}
</span>
))}
</div>
</div>
<span
className="font-mono text-sm text-[var(--color-text-label)] group-hover:text-[var(--color-text)] shrink-0 mt-0.5"
aria-hidden="true"
>
</span>
</a>
))}
</div>
</Widget>
</>
);
}

View File

@@ -7,7 +7,7 @@ import { useTheme } from "@/context/ThemeContext";
const links = [ const links = [
{ href: "/", label: "tyler" }, { href: "/", label: "tyler" },
{ href: "/homelab/", label: "homelab" }, { href: "/homelab/", label: "homelab" },
{ href: "/archive/", label: "archive" }, { href: "/projects/", label: "projects" },
]; ];
export default function Nav() { export default function Nav() {

View File

@@ -17,22 +17,12 @@ export const projects: Project[] = [
slug: "homelab", slug: "homelab",
title: "homelab", title: "homelab",
description: description:
"8-VLAN segmented network, Proxmox VMs/LXCs, SSO via Authentik, full monitoring stack (VictoriaMetrics + Grafana + Beszel + ntfy).", "7-VLAN segmented network, Wireguard VPN, Proxmox VMs/LXCs, SSO via Authentik, full monitoring stack (VictoriaMetrics + Grafana + Beszel + ntfy).",
tags: ["Markdown", "Mermaid", "Proxmox", "Monitor", "Backup"], tags: ["Markdown", "Mermaid", "Proxmox", "Monitor", "Backup"],
githubUrl: "https://gitea.lerkolabs.com/lerko/homelab", githubUrl: "https://gitea.lerkolabs.com/lerko/homelab",
tier: "featured", tier: "featured",
year: 2026, year: 2026,
}, },
{
slug: "open-pact",
title: "open-pact",
description:
"Open protocol for AI agent identity, delegation, and portable memory. Ed25519 keypair identity, signed delegation warrants, portable signed memory facts. No central registry.",
tags: ["TypeScript", "Ed25519", "DID", "npm", "CC0"],
githubUrl: "https://github.com/lerko96/open-pact",
tier: "featured",
year: 2026,
},
{ {
slug: "portfolio", slug: "portfolio",
title: "portfolio", title: "portfolio",
@@ -43,6 +33,27 @@ export const projects: Project[] = [
tier: "featured", tier: "featured",
year: 2021, year: 2021,
}, },
{
slug: "nib",
title: "nib",
description:
"Capture-first personal journal built with Go + React + SQLite. Currently developing in private when I have spare time.",
tags: ["Go", "React", "SQLite", "Journal", "Stream-of-Thought"],
githubUrl: "https://github.com/lerko96/nib",
tier: "featured",
year: 2026,
},
{
slug: "open-pact",
title: "open-pact",
description:
"Open protocol for AI agent identity, delegation, and portable memory. Ed25519 keypair identity, signed delegation warrants, portable signed memory facts. No central registry.",
tags: ["TypeScript", "Ed25519", "DID", "npm", "CC0"],
githubUrl: "https://github.com/lerko96/open-pact",
tier: "featured",
year: 2026,
},
// --- Archive ---
{ {
slug: "helm", slug: "helm",
title: "helm", title: "helm",
@@ -50,20 +61,19 @@ export const projects: Project[] = [
"Full-stack personal productivity dashboard. Go backend with chi router and SQLite, React + TypeScript frontend. Notes, todos, calendar (CalDAV), clipboard, bookmarks, memos. Self-hosted, single-user, daily use.", "Full-stack personal productivity dashboard. Go backend with chi router and SQLite, React + TypeScript frontend. Notes, todos, calendar (CalDAV), clipboard, bookmarks, memos. Self-hosted, single-user, daily use.",
tags: ["Go", "React", "TypeScript", "SQLite", "CalDAV"], tags: ["Go", "React", "TypeScript", "SQLite", "CalDAV"],
githubUrl: "https://github.com/lerko96/helm", githubUrl: "https://github.com/lerko96/helm",
tier: "featured", tier: "archive",
year: 2026, year: 2026,
}, },
{ {
slug: "claude-vault", slug: "risk-ops",
title: "claude-vault", title: "risk-ops",
description: description:
"A scaffolding system for maintaining a living project knowledge base alongside a code repo, powered by Claude Code skills.", "Browser-based strategy dashboard for Risk: Global Domination (SMG Studio). Open one HTML file — no install needed.",
tags: ["Shell", "Developer-Tools", "Claude", "Knowledge-Management"], tags: ["HTML", "JavaScript"],
githubUrl: "https://github.com/lerko96/claude-vault", githubUrl: "#",
tier: "featured", tier: "archive",
year: 2026, year: 2026,
}, },
// --- Archive ---
{ {
slug: "golf-book-mobile", slug: "golf-book-mobile",
title: "golf-book-mobile", title: "golf-book-mobile",
@@ -76,16 +86,6 @@ export const projects: Project[] = [
statusBadge: "Pending App Store Approval", statusBadge: "Pending App Store Approval",
year: 2025, year: 2025,
}, },
{
slug: "risk-ops",
title: "risk-ops",
description:
"Browser-based strategy dashboard for Risk: Global Domination (SMG Studio). Open one HTML file — no install needed.",
tags: ["HTML", "JavaScript"],
githubUrl: "#",
tier: "archive",
year: 2026,
},
{ {
slug: "twitter-thread-ext", slug: "twitter-thread-ext",
title: "twitter-thread-ext", title: "twitter-thread-ext",

View File

@@ -47,12 +47,19 @@ export const timeline: TimelineEntry[] = [
tags: ["go", "react", "typescript"], tags: ["go", "react", "typescript"],
}, },
{ {
date: "2024-08", date: "2025",
title: "Proxmox Backup Server",
type: "homelab",
description: "Deployed PBS on used desktop hardware for disaster recovery.",
tags: ["backup", "recovery", "retention"],
},
{
date: "2025",
title: "Proxmox Cluster", title: "Proxmox Cluster",
type: "homelab", type: "homelab",
description: description:
"Proxmox VMs/LXCs, SSO via Authentik, full monitoring stack (VictoriaMetrics + Grafana + Beszel + ntfy).", "Proxmox installed on dedicated server and the fun begins. VMs/LXCs, SSO via Authentik, full monitoring stack (VictoriaMetrics + Grafana + Beszel + ntfy).",
tags: ["proxmox", "networking", "monitoring", "sso"], tags: ["proxmox", "containers", "VMs", "linux"],
}, },
{ {
date: "2024-06", date: "2024-06",
@@ -66,7 +73,8 @@ export const timeline: TimelineEntry[] = [
date: "2024-03", date: "2024-03",
title: "pfSense", title: "pfSense",
type: "homelab", type: "homelab",
description: "Netgate pfSense n100 picked up on ebay.", description:
"Netgate 1100 picked up on ebay to experience hands on networking configuration and troubleshooting.",
tags: ["network", "firewall", "vlan", "dhcp"], tags: ["network", "firewall", "vlan", "dhcp"],
}, },
{ {