Files

2145 lines
39 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Single source of truth for the homelab">
<link rel="canonical" href="http://192.168.1.8:13080/history/case-study/">
<link rel="prev" href="../scrubbing-list-2026-05-17/">
<link rel="icon" href="../../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.6">
<title>Case study - nuclide.systems docs</title>
<link rel="stylesheet" href="../../assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
<script>__md_scope=new URL("../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="black" data-md-color-accent="blue">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#case-study-the-nuclide-homelab-built-with-claude" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../.." title="nuclide.systems docs" class="md-header__button md-logo" aria-label="nuclide.systems docs" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
nuclide.systems docs
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Case study
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="(prefers-color-scheme: dark)" data-md-color-scheme="slate" data-md-color-primary="black" data-md-color-accent="blue" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
<input class="md-option" data-md-color-media="(prefers-color-scheme: light)" data-md-color-scheme="default" data-md-color-primary="black" data-md-color-accent="blue" aria-hidden="true" type="radio" name="__palette" id="__palette_1">
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item">
<a href="../../CHANGELOG/" class="md-tabs__link">
Changelog
</a>
</li>
<li class="md-tabs__item">
<a href="../../RESUME/" class="md-tabs__link">
Resume
</a>
</li>
<li class="md-tabs__item">
<a href="../../ct-inventory/" class="md-tabs__link">
CT inventory
</a>
</li>
<li class="md-tabs__item">
<a href="../../infra/proxmox-state/" class="md-tabs__link">
Infra
</a>
</li>
<li class="md-tabs__item">
<a href="../../services/homelab-architecture/" class="md-tabs__link">
Services
</a>
</li>
<li class="md-tabs__item">
<a href="../../security/data-leak-audit-comparison/" class="md-tabs__link">
Security & audits
</a>
</li>
<li class="md-tabs__item">
<a href="../../ideas/stack-ideas/" class="md-tabs__link">
Ideas
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../traefik-migration/" class="md-tabs__link">
History
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../.." title="nuclide.systems docs" class="md-nav__button md-logo" aria-label="nuclide.systems docs" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
nuclide.systems docs
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../CHANGELOG/" class="md-nav__link">
<span class="md-ellipsis">
Changelog
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../RESUME/" class="md-nav__link">
<span class="md-ellipsis">
Resume
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../ct-inventory/" class="md-nav__link">
<span class="md-ellipsis">
CT inventory
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_5" >
<label class="md-nav__link" for="__nav_5" id="__nav_5_label" tabindex="0">
<span class="md-ellipsis">
Infra
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Infra
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../infra/proxmox-state/" class="md-nav__link">
<span class="md-ellipsis">
Proxmox state
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/portmap/" class="md-nav__link">
<span class="md-ellipsis">
Port map
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/storage/" class="md-nav__link">
<span class="md-ellipsis">
Storage
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/volumes/" class="md-nav__link">
<span class="md-ellipsis">
Volumes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/docker-networks/" class="md-nav__link">
<span class="md-ellipsis">
Docker networks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/connection-hosts/" class="md-nav__link">
<span class="md-ellipsis">
Connection hosts
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../infra/proxmox-memory-audit/" class="md-nav__link">
<span class="md-ellipsis">
Memory audit
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
Services
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
Services
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../services/homelab-architecture/" class="md-nav__link">
<span class="md-ellipsis">
Homelab architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/dev-environment/" class="md-nav__link">
<span class="md-ellipsis">
Dev environment (Coder + Gitea)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/mcp-gateway/" class="md-nav__link">
<span class="md-ellipsis">
MCP gateway
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/mcp-servers/" class="md-nav__link">
<span class="md-ellipsis">
MCP servers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/pocket-id/" class="md-nav__link">
<span class="md-ellipsis">
Pocket-ID (OIDC)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/backrest/" class="md-nav__link">
<span class="md-ellipsis">
Backrest (backups)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/databases/" class="md-nav__link">
<span class="md-ellipsis">
Databases
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/secrets-manager/" class="md-nav__link">
<span class="md-ellipsis">
Secrets manager
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/comfyui/" class="md-nav__link">
<span class="md-ellipsis">
ComfyUI
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/zoraxy/" class="md-nav__link">
<span class="md-ellipsis">
Zoraxy
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/adguard-dns/" class="md-nav__link">
<span class="md-ellipsis">
AdGuard DNS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/cloud-gpu/" class="md-nav__link">
<span class="md-ellipsis">
Cloud GPU
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/arcane/" class="md-nav__link">
<span class="md-ellipsis">
Arcane
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/doc-ingestion/" class="md-nav__link">
<span class="md-ellipsis">
Doc ingestion pipeline
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../services/llm-benchmark/" class="md-nav__link">
<span class="md-ellipsis">
LLM benchmark
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_7" >
<label class="md-nav__link" for="__nav_7" id="__nav_7_label" tabindex="0">
<span class="md-ellipsis">
Security & audits
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_7_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_7">
<span class="md-nav__icon md-icon"></span>
Security & audits
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../security/data-leak-audit-comparison/" class="md-nav__link">
<span class="md-ellipsis">
Comparison
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/data-leak-audit-2026-05-20-tr004-cloud-sandbox/" class="md-nav__link">
<span class="md-ellipsis">
2026-05-20 · cloud-sandbox breach
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/data-leak-audit-2026-05-21-tr004-artifacts/" class="md-nav__link">
<span class="md-ellipsis">
2026-05-21 · artifacts (clean)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/audit-claude-code-meta/" class="md-nav__link">
<span class="md-ellipsis">
Self-audit (Claude Code)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../security/transcripts/audit-chat-2026-05-21/" class="md-nav__link">
<span class="md-ellipsis">
Transcript (audit session)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_8" >
<label class="md-nav__link" for="__nav_8" id="__nav_8_label" tabindex="0">
<span class="md-ellipsis">
Ideas
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_8_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_8">
<span class="md-nav__icon md-icon"></span>
Ideas
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../ideas/stack-ideas/" class="md-nav__link">
<span class="md-ellipsis">
Stack ideas
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_9" checked>
<label class="md-nav__link" for="__nav_9" id="__nav_9_label" tabindex="">
<span class="md-ellipsis">
History
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_9_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_9">
<span class="md-nav__icon md-icon"></span>
History
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../traefik-migration/" class="md-nav__link">
<span class="md-ellipsis">
Traefik (abandoned 2026-05-16)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../traefik-migration-docker-labels/" class="md-nav__link">
<span class="md-ellipsis">
Traefik labels (abandoned)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../mcp-gateway-requirements/" class="md-nav__link">
<span class="md-ellipsis">
MCP gateway requirements (superseded)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../scrubbing-list-2026-05-17/" class="md-nav__link">
<span class="md-ellipsis">
Scrubbing list (2026-05-17)
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Case study
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Case study
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#origin-story" class="md-nav__link">
<span class="md-ellipsis">
Origin story
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#what-was-built-high-level" class="md-nav__link">
<span class="md-ellipsis">
What was built (high level)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#activity-signal" class="md-nav__link">
<span class="md-ellipsis">
Activity signal
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#productivity-estimate-honest-framing" class="md-nav__link">
<span class="md-ellipsis">
Productivity estimate (honest framing)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#co2-estimate-honest-framing" class="md-nav__link">
<span class="md-ellipsis">
CO2 estimate (honest framing)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#handover-current-state" class="md-nav__link">
<span class="md-ellipsis">
Handover / current state
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#origin-story" class="md-nav__link">
<span class="md-ellipsis">
Origin story
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#what-was-built-high-level" class="md-nav__link">
<span class="md-ellipsis">
What was built (high level)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#activity-signal" class="md-nav__link">
<span class="md-ellipsis">
Activity signal
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#productivity-estimate-honest-framing" class="md-nav__link">
<span class="md-ellipsis">
Productivity estimate (honest framing)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#co2-estimate-honest-framing" class="md-nav__link">
<span class="md-ellipsis">
CO2 estimate (honest framing)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#handover-current-state" class="md-nav__link">
<span class="md-ellipsis">
Handover / current state
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<blockquote>
<p><strong>STATUS: NARRATIVE — historical writeup. Not authoritative for current state.</strong></p>
</blockquote>
<hr />
<h1 id="case-study-the-nuclide-homelab-built-with-claude">Case Study — The Nuclide Homelab, built with Claude<a class="headerlink" href="#case-study-the-nuclide-homelab-built-with-claude" title="Permanent link">&para;</a></h1>
<h2 id="origin-story">Origin story<a class="headerlink" href="#origin-story" title="Permanent link">&para;</a></h2>
<p>One Saturday, the owner's wife left him home alone. He got bored, subscribed to
Claude, and started tinkering with a home server. That afternoon of boredom
turned into the <code>/opt/stacks</code> ecosystem documented here — a ~66-container,
~23-stack self-hosted platform with SSO, an MCP/agent gateway, GPU offload,
and a fully audited network. This is that story, kept as a record of what a
curiosity-driven collaboration produced.</p>
<blockquote>
<p>This is a personal passion project, not a work deliverable. The tone and
scope reflect that: depth and exploration over minimum-viable.</p>
</blockquote>
<h2 id="what-was-built-high-level">What was built (high level)<a class="headerlink" href="#what-was-built-high-level" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>AI/agent core</strong> — LiteLLM model gateway (~28 curated models), an
MCP gateway that DinD-spawns and OAuth-gates MCP servers, an Agent Operator
(cron/event agents), semantic tool retrieval, MCP→LobeChat registration.</li>
<li><strong>Identity</strong> — PocketID as the universal OIDC IdP; every service behind SSO
(LiteLLM, LobeChat, n8n, Nextcloud, Daytona via a Keycloak→PocketID adapter).</li>
<li><strong>Data layer</strong> — shared-postgres standard; a full <strong>tier-1 "no SQLite on
NFS"</strong> migration (ntfy, Karakeep, Arcane, traccar → local disk; Memos +
Vaultwarden → Postgres via pgloader); WAL-G PITR backups re-established.</li>
<li><strong>Network</strong> — full read-only UniFi audit + the unmanaged D-Link DGS-1210
core (SNTP fixed, topology/FDB mapped, DHCP/LLDP interop verified, SPOF
identified); pinned-NTP-source standard adopted.</li>
<li><strong>Reliability</strong> — gateway <strong>deep health-check</strong> (real MCP tool-call probe,
usage-aware backoff) that caught silently-broken servers.</li>
<li><strong>Cloud burst</strong> — on-demand Scaleway L40S GPU offload via WireGuard.</li>
</ul>
<p>See <code>homelab-architecture.md</code> for the living technical reference and
<code>PORTMAP.md</code> for the authoritative port/route map.</p>
<h2 id="activity-signal">Activity signal<a class="headerlink" href="#activity-signal" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>~79 commits in the trailing 14 days</strong> (<code>git log --since="14 days ago"</code>),
spanning gateway OAuth/health, OIDC bolt-ons, DB migrations, network audit,
GPU integration, and docs.</li>
<li>Multi-session, incident-driven: several entries trace to real failures
caught and fixed (WAL-G archiver hung silently ~13 h; n8n <code>latest</code>-drift
outage; SQLite-on-NFS corruption risk; an 8.5-month-stale switch clock).</li>
</ul>
<h2 id="productivity-estimate-honest-framing">Productivity estimate (honest framing)<a class="headerlink" href="#productivity-estimate-honest-framing" title="Permanent link">&para;</a></h2>
<p>These are <strong>rough order-of-magnitude estimates</strong>, not measurements. Assumptions
are stated so they can be challenged.</p>
<ul>
<li>Scope delivered ≈ a small platform: identity, AI gateway, agent runtime,
~23 service stacks, a DB-migration program, a full network audit, backups.</li>
<li>A solo engineer doing this unaided, part-time, learning the unfamiliar
pieces (OIDC internals, pgloader, MCP, UniFi/D-Link internals): a
conservative bound is <strong>several hundred focused hours</strong> (≈ 816 part-time
weeks). Assumes the owner is competent but not a specialist in every domain
touched (identity, Postgres ops, embedded-switch web UIs, MCP).</li>
<li>With the assistant: compressed into a small number of intensive sessions
over ~2 weeks. The leverage is largest where the work is <em>research-heavy
but low-novelty</em> — reverse-engineering a D-Link form POST, deriving an
OIDC redirect, mapping an FDB table — i.e. tasks that are tedious solo but
not conceptually hard. The leverage is smallest on genuine judgment calls
(what to prioritise, what risk is acceptable), which stayed with the owner.</li>
<li><strong>Caveat:</strong> estimate excludes the owner's own steering/review time, which
was substantial and is the reason the output is coherent rather than just
voluminous.</li>
</ul>
<h2 id="co2-estimate-honest-framing">CO2 estimate (honest framing)<a class="headerlink" href="#co2-estimate-honest-framing" title="Permanent link">&para;</a></h2>
<p>Also order-of-magnitude, assumptions explicit.</p>
<ul>
<li><strong>LLM inference:</strong> a heavy multi-session collaboration of this kind is on
the order of a few million tokens. Public estimates put frontier-model
inference at roughly 15 Wh per ~1k output tokens equivalent (wide error
bars). Taking ~3 M tokens × ~2 Wh/1k ≈ <strong>~6 kWh</strong> → at a ~0.35 kgCO2e/kWh
grid ≈ <strong>~2 kgCO2e</strong>. Plausible range <strong>15 kgCO2e</strong>. Datacenter PUE and
exact model size dominate the uncertainty.</li>
<li><strong>Homelab runtime</strong> (the larger ongoing footprint): the NUC 14 Pro draws
~1545 W under mixed load. At ~30 W average → ~0.72 kWh/day → ~260 kWh/yr
<strong>~90 kgCO2e/yr</strong> at the same grid factor. The German grid is cleaner
than that average in many hours, so treat as an upper-ish bound. The
on-demand Scaleway L40S burst is deliberately <em>on-demand</em> precisely to
avoid a 24/7 GPU's footprint.</li>
<li><strong>Takeaway:</strong> the assistant-collaboration carbon is a rounding error next
to a year of always-on homelab power. Efficiency wins (CPU-only default,
on-demand GPU, idle-aware health probing) matter more than the chat cost.</li>
</ul>
<h2 id="handover-current-state">Handover / current state<a class="headerlink" href="#handover-current-state" title="Permanent link">&para;</a></h2>
<p><strong>Healthy &amp; verified</strong>
- Tier-1 SQLite-off-NFS: complete.
- OIDC: n8n (302→PocketID, client <code>33135ad4</code>) and LobeChat (<code>AUTH_TRUSTED_
ORIGINS</code> fix, sign-in→PocketID) — both verified; LobeChat wants one real
browser login as final proof.
- D-Link SNTP: fixed (pinned PTB+Cloudflare IPs, clock corrected &amp; synced).
- Gateway deep health-check: live, usage-aware, surfaced in <code>/api/servers</code>.</p>
<p><strong>Open / pending</strong> (see <code>homelab-architecture.md</code> roadmap for detail)
- <strong>Broken MCP servers</strong> surfaced by the new health-check: <code>memos</code>
(degraded — <code>mcp-memos</code> can't resolve <code>memos</code> host; Docker-network
isolation), <code>context7</code>/<code>crawl4ai</code>/<code>markitdown</code> (down), <code>nextcloud</code>
(probe false-positive — needs <code>health_check:false</code> or per-user creds).
- <strong>D-Link mgmt hardening</strong> (bundle, confirm-first): HTTPS, SNMP review,
Trusted-Host allowlist <code>192.168.1.0/24</code>. Shared <code>tapirnase</code> password
reuse (WiFi/LiteLLM/switch) — rotation deferred, noted.
- <strong>Network</strong>: IoT-VLAN segmentation; D-Link is the unmanaged core/SPOF;
mgmt-TLS certs for Proxmox + D-Link.
- <strong>Platform</strong>: env→secret vault; LobeChat external-feature disable;
observability LXC; agent-platform evolution (memory/teams/MCP-exposed).
- <code>nexa</code> analysis blocked — private repo; deploy key pending authorization.</p>
<p><strong>Operating rules to preserve</strong>
- Confirm + risk-assess before any Proxmox / Ubiquiti / network-gear write.
- Never put DB/SQLite on the UNAS NFS share.
- Only a full pgloader of <em>all</em> tables is a complete DB migration.
- Prefer self-hosted; pin critical container images (no <code>latest</code> drift).</p>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "../..", "features": ["navigation.tabs", "navigation.sections", "navigation.expand", "navigation.top", "search.highlight", "search.suggest", "content.code.copy"], "search": "../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../assets/javascripts/bundle.79ae519e.min.js"></script>
</body>
</html>