diff --git a/app/globals.css b/app/globals.css index 386d812..182da3d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -35,6 +35,9 @@ body{ line-height:1.6; -webkit-font-smoothing:antialiased; overflow-x:hidden; + text-align:justify; + text-align-last:left; + hyphens:auto; } .wrap{max-width:var(--container);margin:0 auto;padding-left:var(--pad);padding-right:var(--pad)} @@ -164,12 +167,12 @@ nav.nav-open .nav-burger span:nth-child(3){transform:translateY(-5.5px) rotate(- .hero-thesis{ font-family:var(--f-head);font-weight:500; font-size:17px;color:var(--navy);line-height:1.5; - max-width:520px;margin-bottom:24px; + max-width:640px;margin-bottom:24px; padding-left:18px;border-left:2px solid var(--navy2); } .hero-lead{ font-size:15px;color:var(--mid);line-height:1.7; - max-width:520px;margin-bottom:32px;font-weight:400; + max-width:640px;margin-bottom:32px;font-weight:400; } .btns{display:flex;gap:12px;flex-wrap:wrap;} .btn{ @@ -299,7 +302,7 @@ nav.nav-open .nav-burger span:nth-child(3){transform:translateY(-5.5px) rotate(- .status-tag.live{color:var(--navy2)} .status-tag.oss{color:var(--mid)} .status-tag.dev{color:#8A5A1E} -.detail-row .desc{font-family:var(--f-body);font-size:13px;color:var(--mid);line-height:1.65;margin-bottom:12px;max-width:560px} +.detail-row .desc{font-family:var(--f-body);font-size:13px;color:var(--mid);line-height:1.65;margin-bottom:12px;max-width:700px} .detail-row .pr-type{font-family:var(--f-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)} .detail-row .pr-t{font-family:var(--f-head);font-weight:600;font-size:16px;letter-spacing:-.01em;margin-bottom:8px} @@ -346,6 +349,12 @@ nav.nav-open .nav-burger span:nth-child(3){transform:translateY(-5.5px) rotate(- .note-t{font-size:13px;font-weight:600;color:var(--navy);margin-bottom:4px;font-family:var(--f-head)} .note-d{font-size:12.5px;color:var(--mid);line-height:1.6;font-weight:400} +.flow-diagram-wrap{border:1px solid var(--line);padding:24px;margin-top:8px} +.flow-zone{font-family:var(--f-mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;fill:var(--mid)} +.flow-t{font-family:var(--f-head);font-weight:600;font-size:12.5px;fill:var(--navy)} +.flow-s{font-family:var(--f-mono);font-size:9px;fill:var(--mid)} +@media(prefers-reduced-motion:reduce){.flow-dot{display:none}} + .row-index{border-top:1px solid var(--navy)} .repo-row{ display:grid;grid-template-columns:1fr 220px;gap:24px; @@ -354,7 +363,7 @@ nav.nav-open .nav-burger span:nth-child(3){transform:translateY(-5.5px) rotate(- } .repo-row:hover{background:var(--stone3)} .repo-row h3{font-family:var(--f-mono);font-weight:500;font-size:15px;margin-bottom:8px;color:var(--navy)} -.repo-row p{font-size:13px;color:var(--mid);line-height:1.6;max-width:560px;font-family:var(--f-body);font-weight:400} +.repo-row p{font-size:13px;color:var(--mid);line-height:1.6;max-width:760px;font-family:var(--f-body);font-weight:400} .repo-meta{display:flex;justify-content:flex-end;align-items:baseline;gap:14px;flex-wrap:wrap;white-space:nowrap} .repo-meta .lang{font-family:var(--f-mono);font-size:11px;color:var(--mid);text-transform:uppercase;letter-spacing:.05em} .repo-meta .kind{font-family:var(--f-mono);font-size:11px;color:var(--mid);text-transform:lowercase;letter-spacing:.02em;font-style:italic} @@ -435,7 +444,7 @@ nav.nav-open .nav-burger span:nth-child(3){transform:translateY(-5.5px) rotate(- font-size:clamp(30px,3.6vw,50px);letter-spacing:-.02em; margin-bottom:14px;line-height:1.05;color:var(--stone); } -.contact-body p{font-size:14.5px;color:rgba(247,246,241,.68);margin-bottom:32px;line-height:1.65;max-width:460px;font-weight:400} +.contact-body p{font-size:14.5px;color:rgba(247,246,241,.68);margin-bottom:32px;line-height:1.65;max-width:620px;font-weight:400} .contact-ctas{display:flex;gap:10px;flex-wrap:wrap} .btn-li{background:var(--navy2);color:#F7F6F1;padding:11px 22px;font-size:12.5px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;font-family:var(--f-mono);text-decoration:none;transition:all .15s;word-break:break-all} .btn-li:hover{background:#96090F;transform:translateY(-1px)} diff --git a/app/page.tsx b/app/page.tsx index 2bdf50b..97e72c9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -277,77 +277,198 @@ export default function Home() {

Homelab Infrastructure

-

Every service containerised, every entry-point proxied, every failure logged. Domain management via Cloudflare with AAAA records and tunnels. Built to enterprise patterns at personal scale.

+

+ Started with a single Proxmox node — my first real server, and the one that taught me virtualisation and LXC. + When file storage and parity-protected array management outgrew that setup, I built Nargothrond: + a dedicated UNRAID box, named after the hidden fortress from The Silmarillion. + Every service containerised, every entry-point proxied, every failure logged. +

—
Hardware
- + + - - + + + -
ComponentUNRAID ServerProxmox Node
ComponentUNRAID Server (Nargothrond)Proxmox Node
CPUIntel Core Ultra 5 225Intel Core i7-8700
MotherboardASUS Prime Z890M-Plus—
RAM48GB DDR5 5600MHz32GB DDR4
Storage4× HDD parity array + NVMe cacheSSD (OS) + SATA pool
Network1Gbps LAN · VLANs1Gbps LAN · VLANs
CaseJonsbo N6—
Storage14TB + 2×12TB parity array + 2TB NVMe cache + 1TB NVMe bootSSD (OS) + SATA pool
Network10GbE (Realtek RTL8127) + 1Gbps LAN · VLANs1Gbps LAN · VLANs
OSUNRAID 7.3.1Proxmox VE 8
AccessPangolin + TraefikTailscale + SSH
FocusIdle power efficiency · perf/wattVirtualisation · LXC
-
—
Services & Stack
+
—
Services & Stack
01
-
UNRAID NAS
+
Traefik + Pangolin
-
Parity-protected array hosting 9-user network shares, Docker Appdata, automated backups. Acts as the central file hub for all services via SMB/NFS.
-
UNRAID 7.3.1Parity arrayNVMe cache9 usersrsyncSMB/NFS
+
Pangolin (WireGuard-based, identity-aware tunnel) publishes services externally with zero open inbound ports; Traefik routes by hostname and handles automatic TLS. Cloudflared runs a second outbound tunnel for select services.
+
PangolinTraefikWireGuardCloudflaredAuto-TLS
02
-
DNS & Cloudflare
+
AdGuard Home + Unbound
-
Domain managed through Cloudflare with AAAA records for IPv6, Cloudflare Tunnels for zero-open-port external access, and AdGuard Home for internal DNS resolution and ad-blocking.
-
CloudflareAAAA recordsCF TunnelsAdGuard HomeSplit DNSDNSSEC
+
Network-wide DNS filtering on its own macvlan (br0) IP so it’s visible to every LAN device. Unbound handles recursive resolution, keeping DNS queries private end-to-end.
+
AdGuard HomeUnboundmacvlanSplit DNS
03
-
Pangolin + Traefik
+
CrowdSec + Gluetun
-
Pangolin (Traefik-based) handles all inbound HTTPS with automatic TLS, subdomain routing, and middleware. Newt tunnel enables access without exposing raw ports. Authelia adds SSO/2FA.
-
PangolinTraefikAuto-TLSNewt tunnelAutheliaMiddleware
+
CrowdSec watches logs and bans malicious IPs in real time using community threat intel. Gluetun routes the download client through an encrypted VPN tunnel with a kill switch, so it never touches the LAN directly.
+
CrowdSecGluetunKill switchIntrusion detection
04
-
Fail2Ban + Security
+
Jellyfin + *Arr Stack
-
Fail2Ban monitors SSH and auth logs, auto-banning brute-force IPs. Crowdsec adds community threat intelligence with real-time blocklist sync. UFW provides host-level firewall rules on each node.
-
Fail2BanCrowdsecUFWiptablesSSH hardeningAuthelia
+
Jellyfin streams the library with iGPU transcoding. Seerr lets family request titles, which flow into Sonarr/Radarr; Jackett and Byparr proxy indexers; qBittorrent (VPN-locked via Gluetun) handles downloads.
+
JellyfinSeerrSonarrRadarrJackettByparrqBittorrent
05
-
Jellyfin + Media
+
Navidrome + Music
-
Jellyfin handles hardware-transcoded streaming with per-user OAuth authentication and Authelia SSO. Sonarr/Radarr auto-manage TV and film libraries; Bazarr handles subtitles; Prowlarr indexes trackers.
-
JellyfinHW transcodeOAuthSonarrRadarrBazarrProwlarr
+
Navidrome serves a lossless personal library Spotify-style. Lidarr tracks favorite artists and new releases; slskd taps the Soulseek network for rare or indie finds.
+
NavidromeLidarrslskdSubsonic API
06
-
Music Pipeline
+
Immich + Vaultwarden
-
Lidarr monitors for releases → Prowlarr indexes → Beets normalises tags → Navidrome streams via Subsonic API. Scrobbling to Listenbrainz. Mobile playback via Symfonium and Explo apps.
-
NavidromeLidarrProwlarrBeetsListenbrainzSymfoniumExplo
+
Immich (with dedicated PostgreSQL + Redis) is a self-hosted Google Photos replacement for phone backups. Vaultwarden syncs passwords across every device. Both live on an isolated network alongside the reverse proxy and IDS.
+
ImmichPostgreSQLRedisVaultwarden
+
+
+
+
07
+
Network Segmentation
+
+
Six Docker networks isolate containers by sensitivity — bridge, tunnel, explo_default, a dedicated pangolin network for the security-critical core, macvlan for AdGuard, and a Tailscale overlay for Jellyfin/Immich remote access.
+
Docker networkingTailscaleVLANsZero-trust
Zero open ports via Tailscale + Cloudflare Tunnels
-
All services reachable remotely through a combination of Tailscale mesh VPN (for trusted devices) and Cloudflare Tunnels (for public-facing services). No port-forwarding. MagicDNS handles internal name resolution. Pangolin’s Newt agent tunnels external traffic into the private LAN.
+
All services reachable remotely through a combination of Tailscale mesh VPN (for trusted devices) and Cloudflare Tunnels (for public-facing services). No port-forwarding. Pangolin’s Newt agent tunnels external traffic into the private LAN.
+
+ +
—
Traffic Flow
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Outside + Internal network + + + Phone + outside the house + + + + Cloudflare / Pangolin + tunnel · no open ports + + passes through tunnel + + + LAN device + phone · laptop · TV + + + + AdGuard Home + DNS filtering + + + + Traefik + routes by hostname + + + + Target app + Jellyfin · Immich · … + + + IoT device + smart bulb, etc. + + + + no route to core + + + Public path (tunnel) + + LAN-only path + + + Blocked (VLAN isolated) + + + Request + + Response + +
+ +
+
Request out, response back the same way
+
A phone outside the house requests a photo backup → it passes through the Cloudflare/Pangolin tunnel (no open inbound ports) → Traefik routes it by hostname to the target app (e.g. Immich), which loads the photos and responds back along the same path.
+
+ +
+
Spun up on demand, not left running
+
A Satisfactory game server, FileFlows for media re-encoding, and an sftp-server for bulk transfers stay fully configured but stopped until a specific project or game night needs them — keeping idle power draw down.
@@ -356,7 +477,7 @@ export default function Home() {

Personal Workstation

-

Daily driver and dev machine. Built around AMD’s 3D V-Cache architecture for a blend of high single-threaded performance and serious GPU compute doubles as a gaming rig, 3D modelling workstation, and 3D printing controller.

+

Daily driver and dev machine. Built around AMD’s 3D V-Cache architecture for a blend of high single-threaded performance and serious GPU compute doubles as a gaming rig, 3D modelling workstation, and 3D printing controller.

@@ -514,7 +635,7 @@ export default function Home() {

Open Source Contributions

-

External pull requests and issues resolved in other maintainers’ codebases work landed in repos I don’t own.

+

External pull requests and issues resolved in other maintainers’ codebases work landed in repos I don’t own.

@@ -552,7 +673,7 @@ export default function Home() {

My Projects on GitHub

-

The complete repository list including the projects featured above, plus the smaller tools and documentation that support them.

+

The complete repository list including the projects featured above, plus the smaller tools and documentation that support them.

ComponentSpec