+
- {entry.description} -
- - {entry.tags && entry.tags.length > 0 && ( -- {entry.tags.join(" · ")} +
+ {entry.description}
- )} +This page moved. /projects/
+This page moved. /#projects
diff --git a/src/pages/homelab.astro b/src/pages/homelab.astro index d28aef6c..8dcb1bc5 100644 --- a/src/pages/homelab.astro +++ b/src/pages/homelab.astro @@ -1,202 +1,12 @@ ---- -import Base from "@/layouts/Base.astro"; -import Nav from "@/components/Nav.astro"; -import Footer from "@/components/Footer.astro"; -import Widget from "@/components/Widget.astro"; -import { services, categoryOrder, categoryLabels } from "@/data/services"; - -const glanceStats = [ - { label: "Hypervisor", value: "Proxmox VE" }, - { label: "Firewall", value: "pfSense (Netgate 1100)" }, - { label: "Switching", value: "TP-Link Omada (managed)" }, - { label: "ISP", value: "AT&T Fiber 1 Gbps" }, - { label: "VPN", value: "WireGuard (pfSense)" }, - { label: "Reverse Proxy", value: "Caddy + Cloudflare DNS-01" }, - { label: "Auth", value: "Authentik SSO" }, - { label: "DNS", value: "Pi-hole → Unbound → Cloudflare" }, - { label: "Containers", value: "9 LXC + 2 VMs" }, -]; - -const vlans = [ - { id: "MGMT", name: "MGMT", purpose: "Network equipment only" }, - { id: "LAN", name: "LAN", purpose: "Trusted personal devices" }, - { id: "Lab", name: "Homelab", purpose: "All self-hosted services" }, - { id: "Guest", name: "Guests", purpose: "Internet only, RFC1918 blocked" }, - { id: "IoT", name: "IoT", purpose: "Smart home, isolated" }, - { id: "WFH", name: "WFH", purpose: "Work devices, no personal access" }, - { id: "DMZ", name: "DMZ", purpose: "Public-facing, hard-blocked internally" }, - { id: "VPN", name: "VPN", purpose: "WireGuard clients, LAN-equivalent access" }, -]; - -const adrs = [ - { - title: "ISP gateway: passthrough mode", - decision: - "ISP gateway stays in-line in passthrough mode, pfSense gets the public IP directly. Gateway WiFi disabled.", - 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", - decision: - "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, 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", - decision: - "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. 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", - decision: - "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 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: "Netgate 1100 for pfSense", - decision: - "Netgate 1100 (Marvell ARMADA 3720, dual-core ARM) as the firewall appliance. ~6W idle, line-rate NAT at 1 Gbps, WireGuard at ~100–150 Mbps.", - why: "Purpose-built for pfSense. Right-sized for 1 Gbps fiber — NAT saturates the link, WireGuard is fast enough for remote access. A full rack server wastes power for this role. Configs and version tracked in private repo.", - }, - { - title: "Shared Postgres + Redis in apps LXC", - decision: - "One Postgres instance hosting multiple databases. One Redis instance. A single init script provisions schemas on first run.", - 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: "Gitea CI/CD: self-hosted runner, internal pipeline, static deploy", - decision: - "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 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", - decision: "Authentik as the SSO provider across all self-hosted services.", - why: "Full OIDC provider + forward auth in one. Lets services like Outline, Gitea, and Vikunja use real SSO rather than just a login gate. Authelia is forward-auth only — no OIDC provider capability.", - }, -]; ---- - -- Personal infrastructure environment for learning, self-hosting, and - operational practice. Running 24/7 on production-grade hardware with - real network segmentation, SSO, monitoring, and IaC-style - documentation. -
-| - Segment - | -- Name - | -- Purpose - | -
|---|---|---|
| {v.id} | -{v.name} | -{v.purpose} | -
- Decision: {adr.decision} -
-- Why: {adr.why} -
-- VLAN maps, runbooks, service registry, config exports, and setup guides. -
- - gitea.lerkolabs.com/lerko/homelab - -This page moved. /#homelab
+ + diff --git a/src/pages/index.astro b/src/pages/index.astro index c0ca2ca0..1aeadcf3 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,11 +4,218 @@ import Nav from "@/components/Nav.astro"; import Footer from "@/components/Footer.astro"; import Hero from "@/components/Hero.astro"; import Timeline from "@/components/Timeline.astro"; +import Widget from "@/components/Widget.astro"; +import ProjectCard from "@/components/ProjectCard.astro"; +import { featuredProjects } from "@/data/projects"; +import { services, categoryOrder, categoryLabels } from "@/data/services"; + +const glanceStats = [ + { label: "Hypervisor", value: "Proxmox VE" }, + { label: "Firewall", value: "pfSense (Netgate 1100)" }, + { label: "Switching", value: "TP-Link Omada (managed)" }, + { label: "ISP", value: "AT&T Fiber 1 Gbps" }, + { label: "VPN", value: "WireGuard (pfSense)" }, + { label: "Reverse Proxy", value: "Caddy + Cloudflare DNS-01" }, + { label: "Auth", value: "Authentik SSO" }, + { label: "DNS", value: "Pi-hole → Unbound → Cloudflare" }, + { label: "Containers", value: "9 LXC + 2 VMs" }, +]; + +const vlans = [ + { id: "MGMT", name: "MGMT", purpose: "Network equipment only" }, + { id: "LAN", name: "LAN", purpose: "Trusted personal devices" }, + { id: "Lab", name: "Homelab", purpose: "All self-hosted services" }, + { id: "Guest", name: "Guests", purpose: "Internet only, RFC1918 blocked" }, + { id: "IoT", name: "IoT", purpose: "Smart home, isolated" }, + { id: "WFH", name: "WFH", purpose: "Work devices, no personal access" }, + { id: "DMZ", name: "DMZ", purpose: "Public-facing, hard-blocked internally" }, + { id: "VPN", name: "VPN", purpose: "WireGuard clients, LAN-equivalent access" }, +]; + +const adrs = [ + { + title: "ISP gateway: passthrough mode", + decision: + "ISP gateway stays in-line in passthrough mode, pfSense gets the public IP directly. Gateway WiFi disabled.", + 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", + decision: + "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, 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", + decision: + "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. 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", + decision: + "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 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: "Netgate 1100 for pfSense", + decision: + "Netgate 1100 (Marvell ARMADA 3720, dual-core ARM) as the firewall appliance. ~6W idle, line-rate NAT at 1 Gbps, WireGuard at ~100–150 Mbps.", + why: "Purpose-built for pfSense. Right-sized for 1 Gbps fiber — NAT saturates the link, WireGuard is fast enough for remote access. A full rack server wastes power for this role. Configs and version tracked in private repo.", + }, + { + title: "Shared Postgres + Redis in apps LXC", + decision: + "One Postgres instance hosting multiple databases. One Redis instance. A single init script provisions schemas on first run.", + 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: "Gitea CI/CD: self-hosted runner, internal pipeline, static deploy", + decision: + "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 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", + decision: "Authentik as the SSO provider across all self-hosted services.", + why: "Full OIDC provider + forward auth in one. Lets services like Outline, Gitea, and Vikunja use real SSO rather than just a login gate. Authelia is forward-auth only — no OIDC provider capability.", + }, +]; ---+ Personal infrastructure environment for learning, self-hosting, and + operational practice. Running 24/7 on production-grade hardware with + real network segmentation, SSO, monitoring, and IaC-style + documentation. +
+| + Segment + | ++ Name + | ++ Purpose + | +
|---|---|---|
| {v.id} | +{v.name} | +{v.purpose} | +
+ Decision: {adr.decision} +
++ Why: {adr.why} +
++ VLAN maps, runbooks, service registry, config exports, and setup guides. +
+ + gitea.lerkolabs.com/lerko/homelab + +- Featured work first. Earlier experiments, browser extensions, and bootcamp projects below — kept for context. -
-- {project.description} -
-- {project.tags.join(" · ")} -
-This page moved. /#projects
+ + diff --git a/src/styles/globals.css b/src/styles/globals.css index 8f4e6eec..8e7c8bf6 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -17,8 +17,6 @@ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-serif: Charter, "Bitstream Charter", "Sitka Text", Cambria, serif; --font-mono: "Source Code Pro", ui-monospace, monospace; - --font-body: var(--font-sans); - /* Breakpoints */ --breakpoint-xs: 576px; @@ -44,14 +42,9 @@ html { line-height: 1.5; background-color: var(--color-bg); color: var(--color-text); - font-family: var(--font-body); + font-family: var(--font-sans); } -/* Typeface picker overrides */ -html[data-typeface="sans"] { --font-body: var(--font-sans); } -html[data-typeface="serif"] { --font-body: var(--font-serif); line-height: 1.6; } -html[data-typeface="mono"] { --font-body: var(--font-mono); font-size: 15px; letter-spacing: -0.01em; } - @layer base { * { box-sizing: border-box; @@ -82,11 +75,23 @@ a:hover { text-decoration-color: currentColor; } +/* Focus states */ +a:focus-visible { + text-decoration-color: currentColor; + outline: 2px solid var(--color-border-bright); + outline-offset: 2px; +} +button:focus-visible { + outline: 2px solid var(--color-border-bright); + outline-offset: 2px; +} + /* Default transitions */ a, button { transition: color 120ms linear, border-color 120ms linear, - opacity 120ms linear, text-decoration-color 120ms linear; + opacity 120ms linear, text-decoration-color 120ms linear, + outline-color 120ms linear; } @media (prefers-reduced-motion: reduce) {