Compare commits
6 Commits
feat/polis
...
docs/homel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22660bed7a | ||
|
|
7f614d28b5 | ||
|
|
e9d7a994c7 | ||
|
|
f6118aa7a4 | ||
|
|
5dea6121a3 | ||
| 4e51dd4a83 |
@@ -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
|
|
||||||
</p>
|
|
||||||
<p className="font-mono text-sm text-[var(--color-text)] leading-relaxed max-w-xl opacity-80">
|
|
||||||
Experiments, browser extensions, and bootcamp projects. Kept here for context — not
|
|
||||||
representative of current work.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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
|
<a
|
||||||
key={project.slug}
|
href="/projects/"
|
||||||
href={project.githubUrl}
|
className="text-[var(--color-accent-green)] underline"
|
||||||
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">
|
/projects/
|
||||||
<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>
|
</a>
|
||||||
))}
|
</p>
|
||||||
</div>
|
|
||||||
</Widget>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 1–2ms. 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. ~600–900 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 2–3 Gbps routing and 600–900 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
77
src/app/projects/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user