Files
Lucas WintherandClaude Opus 5.5 843b6d32bc chore: bootstrap agent-ready docs, rules and design-phase artefacts
Add AGENTS.md as the single source of truth (with CLAUDE.md and GEMINI.md
as pointers), permissions for Claude Code, Codex and Gemini, the docs tree
(architecture, decisions, TODO, permissions, specs, plans), PRODUCT.md and
DESIGN.md, plus the design-phase record: the inspiration log, all mockup
rounds with briefs and screenshots, and the owner's work history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 06:47:38 +01:00

736 lines
61 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">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lucas Winther / work: drawing set</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Sans+Condensed:wght@500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#e8eae6; --grid:#d5d9d3; --grid2:#c7ccc5; --sheet:#f3f4f1; --ink:#1c2024; --soft:#3a4044; --mute:#6b726f; --line:#cfd3cd;
--acc:#d9532b; --art:#2a1a1f; --art-ink:#f1e6d6;
--sans:"IBM Plex Sans",system-ui,sans-serif; --cond:"IBM Plex Sans Condensed","IBM Plex Sans",sans-serif; --mono:"IBM Plex Mono",ui-monospace,monospace;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:64px}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{transition:none!important}}
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:14.5px;line-height:1.5;
background-image:linear-gradient(var(--grid2) 1px,transparent 1px),linear-gradient(90deg,var(--grid2) 1px,transparent 1px),linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);
background-size:120px 120px,120px 120px,24px 24px,24px 24px;background-position:-1px -1px}
a{color:inherit;text-decoration-color:var(--acc);text-underline-offset:3px}
a:hover{color:var(--acc)}
button{font:inherit;color:inherit}
.ph{text-decoration:underline dotted var(--mute);text-underline-offset:3px;cursor:help}
.mono{font-family:var(--mono)}
.k{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--mute)}
.mock-note{position:fixed;bottom:12px;right:56px;z-index:99;max-width:310px;background:#fff;color:#222;border:1px solid #aaa;padding:8px 30px 8px 10px;font:12px/1.4 system-ui,sans-serif}
.mock-note b{display:block}
.mock-note button{position:absolute;top:4px;right:6px;border:0;background:none;font-size:16px;cursor:pointer;color:#555}
.edge{position:fixed;top:0;right:0;bottom:0;width:40px;background:var(--art);color:var(--art-ink);z-index:30;display:flex;align-items:center;justify-content:center;text-decoration:none;writing-mode:vertical-rl;font:italic 15px/1 Georgia,serif;letter-spacing:.03em;transition:width .2s}
.edge:hover{width:52px;color:#e8b04a}
/* ---------- top bar ---------- */
.bar{position:sticky;top:0;z-index:20;background:var(--ink);color:#c9cec8;font-family:var(--mono);font-size:12px;margin-right:40px}
.bar-in{max-width:1380px;margin:0 auto;padding:0 24px;display:flex;align-items:stretch;gap:0;height:46px}
.bar a{text-decoration:none;color:#e8eae6}
.bar .back{display:flex;align-items:center;gap:8px;padding-right:18px;border-right:1px solid #3b4044}
.bar .back:hover{color:var(--acc)}
.bar kbd{font-family:var(--mono);font-size:10px;border:1px solid #5a6064;border-radius:3px;padding:0 4px;color:#9aa09b}
@media (max-width:1500px){.bar .set{display:none!important}}
.bar .set{display:flex;align-items:center;padding:0 18px;color:#8d938e;border-right:1px solid #3b4044;white-space:nowrap}
.tabs{display:flex;align-items:stretch;flex:1;min-width:0;overflow-x:auto;scrollbar-width:none}
.tabs a{display:flex;align-items:center;gap:7px;padding:0 13px;border-right:1px solid #2e3337;white-space:nowrap;color:#b9bdb7;position:relative}
.tabs a b{color:#7d837f;font-weight:400}
.tabs a:hover{background:#262b2f;color:#fff}
.tabs a.on{background:var(--sheet);color:var(--ink)}
.tabs a.on b{color:var(--acc)}
.tabs a.off{opacity:.35;text-decoration:line-through}
.pref{display:flex;align-items:center;gap:6px;padding-left:16px;white-space:nowrap}
.pref span{color:#7d837f}
.pref button{background:none;border:1px solid #3b4044;color:#b9bdb7;font-size:11px;padding:2px 8px;border-radius:99px;cursor:pointer}
.pref button[aria-pressed="true"]{background:#e8eae6;color:var(--ink);border-color:#e8eae6}
.pref button:hover{border-color:#e8eae6}
.wrap{max-width:1380px;margin:0 auto;padding:22px 64px 60px 24px;display:grid;grid-template-columns:minmax(0,1fr) 316px;gap:0 28px}
/* ---------- CV title block ---------- */
.cv{grid-column:1 / -1;display:grid;grid-template-columns:1.25fr 1.5fr 1.2fr 1.1fr 150px;border:2px solid var(--ink);background:var(--sheet);font-size:13px;line-height:1.4}
.cv > div{padding:9px 12px 10px;border-left:1px solid var(--ink);min-width:0}
.cv > div:first-child{border-left:0}
.cv .k{display:block;margin-bottom:3px}
.cv h1{font-family:var(--cond);font-weight:600;font-size:30px;letter-spacing:-.01em;line-height:1;margin:0;text-transform:uppercase}
.cv .sub{color:var(--mute)}
.cv p{margin:0}
.cv .cvpdf{padding:9px 12px 10px;display:flex;flex-direction:column;justify-content:space-between;text-decoration:none;background:var(--ink);color:var(--sheet);font-family:var(--mono);font-size:12px}
.cv .cvpdf:hover{background:var(--acc);color:#fff}
.cv .cvpdf .k{color:#9aa09b}
.cv .cvpdf big{font-size:20px;font-weight:500}
.cv .row2{grid-column:1 / -1;border-left:0;border-top:1px solid var(--ink);display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;font-family:var(--mono);font-size:11px;color:var(--soft);padding:5px 12px}
.sechead{grid-column:1;display:flex;flex-wrap:wrap;align-items:baseline;gap:14px;margin:30px 0 10px}
.sechead h2{margin:0;font-family:var(--cond);font-weight:600;font-size:20px;letter-spacing:.02em;text-transform:uppercase}
.sechead p{margin:0;color:var(--mute);font-size:13px}
.sechead .k{margin-left:auto}
/* ---------- sheets ---------- */
.sheets{grid-column:1;min-width:0}
.sheet{position:relative;background:var(--sheet);border:1px solid var(--ink);padding:16px;margin:0 0 30px;scroll-margin-top:62px;cursor:default;transition:box-shadow .2s}
.sheet.hide{display:none}
.sheet::before{content:"";position:absolute;inset:10px;border:2px solid var(--ink);pointer-events:none}
.sheet .zones-x,.sheet .zones-y{position:absolute;font-family:var(--mono);font-size:8.5px;color:var(--mute);display:flex;pointer-events:none}
.sheet .zones-x{left:10px;right:10px;top:0;height:10px;align-items:center}
.sheet .zones-x span{flex:1;text-align:center;border-left:1px solid var(--ink);line-height:10px}
.sheet .zones-x span:first-child{border-left:0}
.sheet .zones-y{top:10px;bottom:10px;left:0;width:10px;flex-direction:column}
.sheet .zones-y span{flex:1;display:flex;align-items:center;justify-content:center;border-top:1px solid var(--ink)}
.sheet .zones-y span:first-child{border-top:0}
.sheet.sel{box-shadow:0 0 0 3px var(--acc)}
.selmark{display:none;position:absolute;top:-22px;left:0;font-family:var(--mono);font-size:10.5px;color:var(--acc)}
.sheet-in{position:relative;display:grid;grid-template-columns:minmax(0,1.6fr) minmax(0,1fr);grid-template-rows:auto 1fr auto;gap:0;min-height:520px}
.fig{grid-row:1 / 3;border-right:1px solid var(--ink);padding:14px 16px 8px 14px;position:relative;min-width:0}
.fig svg{display:block;width:100%;height:auto;overflow:visible}
.fig .figcap{font-family:var(--mono);font-size:10px;color:var(--mute);display:flex;justify-content:space-between;gap:10px;border-top:1px solid var(--line);padding-top:5px;margin-top:6px}
.notes{padding:14px 14px 10px 16px;font-size:13.5px;min-width:0}
.notes h3{font-family:var(--cond);font-weight:600;font-size:12px;letter-spacing:.08em;text-transform:uppercase;margin:0 0 8px;border-bottom:1px solid var(--ink);padding-bottom:4px}
.notes ol{margin:0;padding:0;list-style:none;counter-reset:n}
.notes ol li{counter-increment:n;position:relative;padding-left:24px;margin-bottom:8px}
.notes ol li::before{content:counter(n);position:absolute;left:0;top:1px;width:16px;height:16px;border:1px solid var(--acc);color:var(--acc);border-radius:50%;font:500 9.5px/14px var(--mono);text-align:center}
.notes ol li b{font-weight:600}
.notes .gen{margin:12px 0 0;font-size:13px;color:var(--soft)}
.notes .gen p{margin:0 0 6px}
.notes .gen strong{font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;font-weight:500;color:var(--mute);display:block;text-transform:uppercase}
.tb{grid-column:1 / -1;border-top:2px solid var(--ink);display:grid;grid-template-columns:2.1fr .9fr .8fr 1.7fr 1.1fr 128px;font-size:12.5px}
.tb > div{padding:5px 9px 6px;border-left:1px solid var(--ink);min-width:0}
.tb > div:first-child{border-left:0}
.tb .k{display:block;font-size:8.5px}
.tb .title{font-family:var(--cond);font-weight:600;font-size:19px;line-height:1.1;text-transform:uppercase;letter-spacing:.01em}
.tb .v{font-family:var(--mono);font-size:11.5px}
.tb .stampcell{display:flex;align-items:center;justify-content:center;position:relative;overflow:visible}
.stamp{font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;border:2px solid currentColor;padding:4px 8px 3px;transform:rotate(-7deg);display:inline-block;white-space:nowrap;line-height:1.15;text-align:center;background:rgba(243,244,241,.6)}
.stamp small{display:block;font-size:8px;letter-spacing:.1em;font-weight:500}
.stamp.hand{color:var(--ink);border-style:double;border-width:4px}
.stamp.ai{color:var(--acc)}
.stamp.unk{color:var(--mute);border-style:dashed}
.rev{grid-column:2;align-self:end;margin:0 14px 10px 16px;border:1px solid var(--ink);font-family:var(--mono);font-size:10.5px;border-collapse:collapse}
.rev th,.rev td{border:1px solid var(--line);padding:2px 6px;text-align:left;vertical-align:top}
.rev th{font-weight:500;font-size:8.5px;letter-spacing:.08em;color:var(--mute);border-bottom:1px solid var(--ink)}
.links{display:flex;gap:4px 14px;flex-wrap:wrap;font-family:var(--mono);font-size:11.5px;margin-top:8px}
.links a::after{content:" ↗";color:var(--mute)}
.placeholder-tag{font-family:var(--mono);font-size:9px;color:var(--mute);border:1px dashed var(--mute);padding:0 4px;letter-spacing:.06em;text-transform:uppercase;vertical-align:1px}
/* svg drawing styles */
.ln{fill:none;stroke:var(--ink);stroke-width:1.3}
.ln-th{fill:none;stroke:var(--ink);stroke-width:.8}
.hid{fill:none;stroke:var(--ink);stroke-width:1;stroke-dasharray:5 3}
.ctr{fill:none;stroke:var(--mute);stroke-width:.7;stroke-dasharray:12 3 2 3}
.dim{fill:none;stroke:var(--mute);stroke-width:.8}
.dimt{font-family:var(--mono);font-size:9.5px;fill:var(--soft)}
.lbl{font-family:var(--mono);font-size:10px;fill:var(--ink)}
.lbl-s{font-family:var(--mono);font-size:8.5px;fill:var(--mute)}
.lead{fill:none;stroke:var(--acc);stroke-width:1}
.cb circle{fill:var(--sheet);stroke:var(--acc);stroke-width:1.2}
.cb text{font-family:var(--mono);font-size:10px;font-weight:500;fill:var(--acc);text-anchor:middle;dominant-baseline:central}
.hatch{fill:url(#hatch)}
.phbox{fill:none;stroke:var(--line);stroke-width:1}
/* ---------- register ---------- */
.register{grid-column:1;border:2px solid var(--ink);background:var(--sheet);margin-bottom:10px}
.register table{width:100%;border-collapse:collapse;font-size:13.5px}
.register th{font-family:var(--mono);font-weight:500;font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--mute);text-align:left;padding:7px 10px;border-bottom:2px solid var(--ink);background:var(--sheet);position:sticky;top:46px;z-index:2}
.register td{padding:7px 10px;border-bottom:1px solid var(--line);vertical-align:baseline}
.register tr.r{cursor:pointer;transition:background .12s}
.register tr.r:hover{background:#fff}
.register tr.r.open{background:#fff}
.register tr.r.open td{border-bottom-color:transparent}
.register tr.grp td{font-family:var(--mono);font-size:10px;letter-spacing:.09em;text-transform:uppercase;color:var(--soft);background:#e3e6e1;padding:4px 10px;border-bottom:1px solid var(--ink)}
.register tr.hide{display:none}
.register .no{font-family:var(--mono);font-size:11.5px;color:var(--mute);white-space:nowrap;width:74px}
.register .t{font-weight:500}
.register .t small{font-weight:400;color:var(--mute);margin-left:6px}
.register .st{font-family:var(--mono);font-size:11px;color:var(--soft)}
.register .yr{font-family:var(--mono);font-size:11.5px;width:60px}
.register .made{font-family:var(--mono);font-size:10.5px;white-space:nowrap;width:118px}
.register .op{width:72px;text-align:right;font-family:var(--mono);font-size:11px;color:var(--acc);white-space:nowrap}
.register tr.lit td{background:rgba(217,83,43,.07)}
.aid{display:inline-block;width:9px;height:9px;border-radius:50%;border:1.5px solid var(--ink);margin-right:6px;vertical-align:-1px}
.aid.ai{background:var(--acc);border-color:var(--acc)}
.aid.unk{border-style:dashed;border-color:var(--mute)}
tr.drawer > td{padding:0 10px 14px;background:#fff;border-bottom:2px solid var(--ink)}
tr.drawer .sheet{margin:6px 0 0;cursor:default}
tr.drawer .sheet-in{min-height:0}
.mini-fig{grid-row:1 / 3}
.mini-fig svg{max-width:400px;margin:0 auto}
/* ---------- inspector ---------- */
.insp{grid-column:2;grid-row:2 / span 20;align-self:start;position:sticky;top:64px;border:2px solid var(--ink);background:var(--sheet);font-size:13.5px;max-height:calc(100vh - 80px);overflow:auto;scrollbar-width:thin}
.insp header{background:var(--ink);color:#e8eae6;padding:8px 12px 9px;font-family:var(--mono);font-size:11px;display:flex;justify-content:space-between}
.insp header span:last-child{color:var(--acc)}
.insp .sec{padding:10px 12px 12px;border-bottom:1px solid var(--ink)}
.insp .sec:last-child{border-bottom:0}
.insp h3{margin:2px 0 2px;font-family:var(--cond);font-weight:600;font-size:21px;text-transform:uppercase;line-height:1.05}
.insp p{margin:4px 0 0}
.insp .k{display:block;margin-bottom:5px}
.skills{display:flex;flex-wrap:wrap;gap:5px}
.skills button{font-family:var(--mono);font-size:11px;border:1px solid var(--ink);background:none;padding:1px 7px;cursor:pointer}
.skills button:hover,.skills button[aria-pressed="true"]{background:var(--ink);color:var(--sheet)}
.skillnote{font-family:var(--mono);font-size:10.5px;color:var(--mute);margin-top:6px;min-height:15px}
.skillnote a{color:var(--ink)}
.wlist{list-style:none;margin:0;padding:0}
.wlist li{padding:5px 0;border-bottom:1px dotted var(--line)}
.wlist li:last-child{border-bottom:0}
.wlist a{text-decoration:none;font-weight:500;display:block;line-height:1.3}
.wlist a:hover{text-decoration:underline;text-decoration-color:var(--acc)}
.wlist span{font-family:var(--mono);font-size:10px;color:var(--mute)}
.whobar{display:flex;height:10px;border:1px solid var(--ink);margin:6px 0 3px}
.whobar i{display:block}
.whobar .h{background:repeating-linear-gradient(90deg,var(--ink) 0 1px,transparent 1px 3px)}
.whobar .a{background:var(--acc)}
.whobar .u{background:repeating-linear-gradient(135deg,var(--line) 0 3px,transparent 3px 6px)}
.insp .hint{font-family:var(--mono);font-size:10px;color:var(--mute)}
footer.foot{grid-column:1;margin-top:30px;border-top:2px solid var(--ink);padding-top:10px;font-size:12.5px;color:var(--mute)}
footer.foot p{margin:0 0 5px}
@media (max-width:1180px){
.wrap{grid-template-columns:1fr}
.insp{grid-column:1;grid-row:auto;position:relative;top:0;max-height:none;margin:0 0 20px}
.cv{grid-template-columns:1fr 1fr 1fr}
.cv > div:nth-child(4){border-left:0}
.cv > div:nth-child(n+4){border-top:1px solid var(--ink)}
}
@media (max-width:860px){
.bar-in{flex-wrap:wrap;height:auto;padding:0 10px}
.bar .back{padding:8px 12px 8px 0}
.pref{padding:6px 0;width:100%;overflow-x:auto}
.tabs{order:3;width:100%;flex:none;border-top:1px solid #3b4044}
.tabs a{padding:8px 10px}
.register th{top:0;position:static}
.sheet-in{grid-template-columns:1fr;min-height:0}
.fig{grid-row:auto;border-right:0;border-bottom:1px solid var(--ink)}
.rev{grid-column:1}
.tb{grid-template-columns:1fr 1fr}
.tb > div{border-top:1px solid var(--ink)}
.tb > div:nth-child(odd){border-left:0}
.tb > div:first-child{grid-column:1 / -1;border-top:0}
.tb > div:nth-child(2){border-left:0}
.tb > div:nth-child(3){border-left:1px solid var(--ink)}
.tb > div:nth-child(4){border-left:0}
.tb > div:nth-child(5){border-left:1px solid var(--ink)}
.tb > div:nth-child(6){border-left:0;grid-column:1 / -1;padding:12px}
.cv{grid-template-columns:1fr 1fr}
.cv > div{border-left:0!important;border-top:1px solid var(--ink)}
.cv > div:first-child{grid-column:1 / -1;border-top:0}
.register .st,.register th.st,.register .yr,.register th.yr{display:none}
}
@media (max-width:480px){
.wrap{padding:16px 48px 40px 12px}
.cv{grid-template-columns:1fr}
.register .made{width:auto}
.register .op{display:none}
.bar{margin-right:30px}
.edge{width:30px;font-size:13px}
}
</style>
</head>
<body>
<div class="mock-note"><button aria-label="Dismiss note" onclick="this.parentNode.remove()">×</button><b>tech-r2-b · Spec sheets</b>Projects as engineering drawings on the gate's grid paper. Four featured projects get full sheets with a title block, an AI stamp and numbered callouts; the rest sit in a drawing register (T1's overview, with character) that opens each sheet in place. A T4-style inspector shows the selected sheet's skills and writing.</div>
<a class="edge" href="nav-d.html" title="Cross to the personal side">off the clock →</a>
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
<pattern id="hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><path d="M0 0V6" stroke="#9aa09b" stroke-width="1"/></pattern>
<marker id="ar" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0 2 L10 5 L0 8z" fill="#6b726f"/></marker>
<marker id="ar-i" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 1 L10 5 L0 9" fill="none" stroke="#1c2024" stroke-width="1.5"/></marker>
<marker id="dotr" viewBox="0 0 6 6" refX="3" refY="3" markerWidth="5" markerHeight="5"><circle cx="3" cy="3" r="2.4" fill="#d9532b"/></marker>
</defs></svg>
<!-- ============ TOP BAR ============ -->
<nav class="bar" aria-label="Drawing set">
<div class="bar-in">
<a class="back" href="nav-d.html">← gate <kbd>Esc</kbd></a>
<span class="set">LW–WORK · drawing set</span>
<div class="tabs" id="tabs">
<a href="#cv"><b>00</b> CV</a>
<a href="#s-compute" data-sheet="compute"><b>01</b> compute</a>
<a href="#s-hypercache" data-sheet="hypercache"><b>02</b> HyperCache</a>
<a href="#s-rowing" data-sheet="rowing"><b>03</b> rowing</a>
<a href="#s-config" data-sheet="config"><b>04</b> Config</a>
<a href="#register"><b>R</b> register (17)</a>
</div>
<div class="pref" role="group" aria-label="Filter projects by how they were made">
<span>projects made</span>
<button aria-pressed="true" data-pref="all">any way</button>
<button aria-pressed="false" data-pref="nofull">not fully by AI</button>
<button aria-pressed="false" data-pref="hand">by hand only</button>
</div>
</div>
</nav>
<div class="wrap">
<!-- ============ CV as the set's title block ============ -->
<section class="cv" id="cv" aria-label="CV">
<div><span class="k">drawn by</span><h1>Lucas Winther</h1><p class="sub" style="margin-top:4px">Computer scientist. Builds and runs research software.</p></div>
<div><span class="k">current · Jan 2025–now</span><p><b>IT-medarbejder</b>, IT Research Tech, Aarhus University</p><p class="sub">Slurm compute platform, document services, secure and monitored infrastructure.</p></div>
<div><span class="k">previous</span><p>ScienceAtHome, intern <span class="sub">· 2019</span></p><p>TERMA Group, student helper, local configuration management <span class="sub">· 2016–18</span></p></div>
<div><span class="k">education · contact</span><p>BSc Computer Science, <span class="ph" title="placeholder: university">Aarhus University</span></p><p><a href="mailto:lucas@lucaswinther.info">lucas@lucaswinther.info</a><br><a href="https://github.com/StereotypicalCat">GitHub</a> · <a href="https://www.linkedin.com/in/lucas-winther">LinkedIn</a></p></div>
<a class="cvpdf" href="#" title="placeholder: CV PDF"><span class="k">full CV</span><big>PDF ↓</big><span>1 page</span></a>
<div class="row2"><span>DevOps · Docker · Linux admin · observability · cloud (OCI) · Slurm · React/Node · Ansible · Unity/C# · leadership</span><span>sheets 01–04 featured · 17 in register · <span class="ph" title="placeholder">rev. Nov 2026</span></span></div>
</section>
<div class="sechead"><h2>Featured sheets</h2><p>Click a sheet to inspect it. Numbered circles point at the notes on the right of each sheet.</p></div>
<!-- ============ INSPECTOR ============ -->
<aside class="insp" id="insp" aria-live="polite">
<header><span>INSPECTOR</span><span id="i-no">LW-01</span></header>
<div class="sec">
<span class="k" id="i-group">at work</span>
<h3 id="i-title">AU compute platform</h3>
<p id="i-one">…</p>
<div class="links" id="i-links"></div>
</div>
<div class="sec">
<span class="k">who wrote it</span>
<div class="whobar" id="i-bar"></div>
<span class="hint" id="i-who"></span>
</div>
<div class="sec">
<span class="k">skills on this sheet · click to find other sheets</span>
<div class="skills" id="i-skills"></div>
<div class="skillnote" id="i-skillnote"></div>
</div>
<div class="sec">
<span class="k">writing about it</span>
<ul class="wlist" id="i-writing"></ul>
</div>
<div class="sec">
<span class="k">latest writing · <a href="https://blog.lucaswinther.info">blog ↗</a></span>
<ul class="wlist" id="i-latest"></ul>
</div>
</aside>
<div class="sheets" id="sheets">
<!-- ===== SHEET 01: COMPUTE ===== -->
<article class="sheet" id="s-compute" data-id="compute" data-ai="unk" tabindex="0">
<span class="selmark">▸ selected</span>
<div class="zones-x"><span>A</span><span>B</span><span>C</span><span>D</span><span>E</span><span>F</span></div>
<div class="zones-y"><span>1</span><span>2</span><span>3</span><span>4</span></div>
<div class="sheet-in">
<div class="fig">
<svg viewBox="0 0 520 380" role="img" aria-label="Placeholder figure: the job dashboard in a browser frame, with callouts to the job form, queue, ERDA picker and node count; dimension lines show the elastic web and compute tiers">
<!-- dimension: web tier -->
<path class="dim" d="M40 22 V34 M330 22 V34"/><path class="dim" d="M40 28 H330" marker-start="url(#ar)" marker-end="url(#ar)"/>
<rect x="140" y="20" width="90" height="14" fill="#f3f4f1"/><text class="dimt" x="185" y="31" text-anchor="middle">web: 1…n</text>
<!-- browser frame -->
<rect class="ln" x="40" y="44" width="290" height="216"/>
<path class="ln-th" d="M40 62 H330"/><circle class="ln-th" cx="50" cy="53" r="3"/><circle class="ln-th" cx="60" cy="53" r="3"/><rect class="ln-th" x="76" y="48" width="160" height="10"/>
<text class="lbl-s" x="82" y="56">compute.au.dk (placeholder)</text>
<!-- job form -->
<rect class="ln-th" x="52" y="74" width="118" height="98"/>
<text class="lbl" x="58" y="88">new job</text>
<rect class="phbox" x="58" y="96" width="104" height="10"/><rect class="phbox" x="58" y="112" width="104" height="10"/><rect class="phbox" x="58" y="128" width="64" height="10"/>
<rect x="58" y="150" width="50" height="14" fill="#1c2024"/><text x="64" y="160" class="lbl-s" fill="#f3f4f1" style="fill:#f3f4f1">submit</text>
<!-- queue -->
<rect class="ln-th" x="180" y="74" width="140" height="122"/>
<text class="lbl" x="186" y="88">queue</text>
<g class="ln-th"><path d="M186 98 H314 M186 112 H314 M186 126 H314 M186 140 H314 M186 154 H314 M186 168 H314"/></g>
<rect x="290" y="102" width="20" height="6" class="hatch"/><rect x="290" y="116" width="20" height="6" fill="#d9532b" opacity=".6"/>
<!-- ERDA picker -->
<rect class="ln-th" x="52" y="182" width="118" height="66"/>
<text class="lbl" x="58" y="196">ERDA files</text>
<path class="ln-th" d="M60 206 h8 v6 h-8z M60 220 h8 v6 h-8z M60 234 h8 v6 h-8z"/><path class="phbox" d="M74 209 H150 M74 223 H140 M74 237 H156"/>
<!-- node gauge -->
<rect class="ln-th" x="180" y="206" width="140" height="42"/>
<text class="lbl" x="186" y="220">nodes</text>
<g><rect x="186" y="228" width="12" height="12" fill="#1c2024"/><rect x="202" y="228" width="12" height="12" fill="#1c2024"/><rect x="218" y="228" width="12" height="12" fill="#1c2024"/><rect class="hid" x="234" y="228" width="12" height="12"/><rect class="hid" x="250" y="228" width="12" height="12"/></g>
<!-- compute tier, side elevation -->
<path class="ctr" d="M20 300 H500"/>
<g><rect class="ln" x="60" y="284" width="44" height="32"/><rect class="ln" x="112" y="284" width="44" height="32"/><rect class="ln" x="164" y="284" width="44" height="32"/><rect class="hid" x="216" y="284" width="44" height="32"/><rect class="hid" x="268" y="284" width="44" height="32"/></g>
<text class="lbl-s" x="66" y="304">node</text><text class="lbl-s" x="118" y="304">node</text><text class="lbl-s" x="170" y="304">node</text>
<path class="dim" d="M60 326 V340 M312 326 V340"/><path class="dim" d="M60 334 H312" marker-start="url(#ar)" marker-end="url(#ar)"/>
<rect x="130" y="327" width="110" height="14" fill="#f3f4f1"/><text class="dimt" x="186" y="338" text-anchor="middle">compute: 0…n (OCI)</text>
<rect class="ln" x="370" y="276" width="110" height="48"/><text class="lbl" x="380" y="296">Slurm ctl.</text><text class="lbl-s" x="380" y="310">+ job service</text>
<path class="ln-th" d="M370 300 H314" marker-end="url(#ar-i)"/>
<rect class="ln" x="370" y="72" width="110" height="40"/><text class="lbl" x="380" y="90">Entra ID</text><text class="lbl-s" x="380" y="103">OAuth login</text>
<path class="hid" d="M330 92 H368"/>
<rect class="ln" x="370" y="180" width="110" height="40"/><text class="lbl" x="380" y="198">ERDA</text><text class="lbl-s" x="380" y="211">AU storage</text>
<path class="hid" d="M425 222 V274"/><path class="hid" d="M330 215 H368"/>
<path class="ln-th" d="M300 260 C310 270 350 260 370 290" marker-end="url(#ar-i)"/>
<!-- callouts -->
<g><path class="lead" d="M110 134 L20 134 L20 112" marker-start="url(#dotr)"/><g class="cb"><circle cx="20" cy="104" r="8"/><text x="20" y="104">1</text></g></g>
<g><path class="lead" d="M250 160 L348 160 L348 140" marker-start="url(#dotr)"/><g class="cb"><circle cx="348" cy="132" r="8"/><text x="348" y="132">2</text></g></g>
<g><path class="lead" d="M110 236 L20 236 L20 262" marker-start="url(#dotr)"/><g class="cb"><circle cx="20" cy="270" r="8"/><text x="20" y="270">3</text></g></g>
<g><path class="lead" d="M262 234 L348 246 L348 256" marker-start="url(#dotr)"/><g class="cb"><circle cx="348" cy="264" r="8"/><text x="348" y="264">4</text></g></g>
<g><path class="lead" d="M440 112 L460 140" marker-start="url(#dotr)"/><g class="cb"><circle cx="464" cy="148" r="8"/><text x="464" y="148">5</text></g></g>
</svg>
<div class="figcap"><span>FIG. 1 · placeholder: job dashboard screenshot, over the platform's two elastic tiers</span><span>hidden lines = scaled-down</span></div>
</div>
<div class="notes">
<h3>Notes</h3>
<ol>
<li><b>Job form.</b> Researchers submit Slurm jobs without a terminal: script, resources, inputs.</li>
<li><b>Queue.</b> Status and logs per job, fed by a Node service that talks to Slurm for the user.</li>
<li><b>ERDA picker.</b> Inputs come from, and results go back to, AU's own ERDA storage.</li>
<li><b>Nodes.</b> Compute nodes on Oracle Cloud scale up with the queue and down to zero when it's empty.</li>
<li><b>Login.</b> OAuth with Entra ID: university accounts only.</li>
</ol>
<div class="gen">
<p><strong>what &amp; why</strong>A scalable, easy-to-use compute job system for researchers: Slurm's power behind a React/Node web app, with both web stack and compute scaling on Oracle Cloud.</p>
<p><strong>interesting</strong><span class="ph" title="placeholder">Scaling the web tier and the compute pool separately, since a busy week of uploads isn't a busy week of jobs.</span></p>
</div>
</div>
<table class="rev"><tr><th>REV</th><th>DATE</th><th>CHANGE</th></tr><tr><td>A</td><td>2025</td><td>first release, <span class="ph" title="placeholder">one group</span></td></tr><tr><td>B</td><td><span class="ph" title="placeholder">2026</span></td><td><span class="ph" title="placeholder">autoscaling compute</span></td></tr></table>
<div class="tb">
<div><span class="k">project</span><span class="title">AU compute platform</span></div>
<div><span class="k">dwg no.</span><span class="v">LW-01</span></div>
<div><span class="k">rev / year</span><span class="v">B · 2025–</span></div>
<div><span class="k">stack</span><span class="v">Slurm · React · Node · OCI · Entra ID</span></div>
<div><span class="k">role</span><span class="v">developer &amp; ops, AU IT Research Tech</span></div>
<div class="stampcell"><span class="stamp unk" title="placeholder: AI level not known yet">AI level<small>not marked</small></span></div>
</div>
</div>
</article>
<!-- ===== SHEET 02: HYPERCACHE ===== -->
<article class="sheet" id="s-hypercache" data-id="hypercache" data-ai="hand" tabindex="0">
<span class="selmark">▸ selected</span>
<div class="zones-x"><span>A</span><span>B</span><span>C</span><span>D</span><span>E</span><span>F</span></div>
<div class="zones-y"><span>1</span><span>2</span><span>3</span><span>4</span></div>
<div class="sheet-in">
<div class="fig">
<svg viewBox="0 0 520 360" role="img" aria-label="Figure: peer network with reputation scores; a free-riding peer is cut off; an inset shows an audit round with a time interval dimension">
<g class="ln-th">
<path d="M70 70 L200 50 L330 80 L400 180 L260 220 L110 190 Z M70 70 L110 190 M200 50 L260 220 M200 50 L110 190 M330 80 L260 220"/>
</g>
<path class="hid" d="M200 50 L180 130 M110 190 L180 130 M260 220 L180 130"/>
<g fill="#f3f4f1" stroke="#1c2024" stroke-width="1.3">
<circle cx="70" cy="70" r="20"/><circle cx="200" cy="50" r="20"/><circle cx="330" cy="80" r="20"/><circle cx="400" cy="180" r="20"/><circle cx="260" cy="220" r="20"/><circle cx="110" cy="190" r="20"/>
</g>
<circle cx="180" cy="130" r="18" class="hatch" stroke="#1c2024" stroke-width="1.3"/>
<g class="lbl" text-anchor="middle"><text x="70" y="74">.92</text><text x="200" y="54">.88</text><text x="330" y="84">.71</text><text x="400" y="184">.95</text><text x="260" y="224">.64</text><text x="110" y="194">.90</text></g>
<!-- inset: audit round -->
<rect class="ln" x="40" y="258" width="440" height="84"/>
<text class="lbl-s" x="48" y="272">DETAIL A · one audit round</text>
<path class="ctr" d="M60 312 H460"/>
<g class="ln"><path d="M90 300 V324 M210 300 V324 M330 300 V324 M450 300 V324"/></g>
<text class="lbl-s" x="78" y="296">challenge</text><text class="lbl-s" x="196" y="296">proof</text><text class="lbl-s" x="316" y="296">score</text><text class="lbl-s" x="436" y="296">next</text>
<path class="dim" d="M90 332 H450" marker-start="url(#ar)" marker-end="url(#ar)"/><rect x="240" y="326" width="48" height="12" fill="#f3f4f1"/><text class="dimt" x="264" y="336" text-anchor="middle">Δt</text>
<!-- callouts -->
<g><path class="lead" d="M180 130 L120 130 L120 116" marker-start="url(#dotr)"/><g class="cb"><circle cx="120" cy="108" r="8"/><text x="120" y="108">1</text></g></g>
<g><path class="lead" d="M330 80 L430 60" marker-start="url(#dotr)"/><g class="cb"><circle cx="438" cy="58" r="8"/><text x="438" y="58">2</text></g></g>
<g><path class="lead" d="M200 300 L200 246 L170 246" marker-start="url(#dotr)"/><g class="cb"><circle cx="162" cy="246" r="8"/><text x="162" y="246">3</text></g></g>
<g><path class="lead" d="M260 220 L330 240 L420 240" marker-start="url(#dotr)"/><g class="cb"><circle cx="428" cy="240" r="8"/><text x="428" y="240">4</text></g></g>
</svg>
<div class="figcap"><span>FIG. 2 · reputation network (scores = weighted audit history) and detail A</span><span>hatched = free-rider</span></div>
</div>
<div class="notes">
<h3>Notes</h3>
<ol>
<li><b>Free-rider.</b> Claims to store chunks, fails the spot checks. Hidden lines: the links that get dropped.</li>
<li><b>Score.</b> Each peer's reputation comes from its past audits, weighted toward recent ones.</li>
<li><b>Audit round.</b> Challenge, proof, score, repeat every Δt.</li>
<li><b>Churn.</b> <span class="ph" title="placeholder">Honest peers going offline mustn't look like cheating; this needed a grace window.</span></li>
</ol>
<div class="gen">
<p><strong>what &amp; why</strong>Bachelor thesis: reputation systems for peer-to-peer archival, so peers that don't keep what they promise get found and routed around. Simulated in Node.js.</p>
<div class="links"><a href="#">thesis PDF</a><a href="https://github.com/StereotypicalCat">code</a></div>
</div>
</div>
<table class="rev"><tr><th>REV</th><th>DATE</th><th>CHANGE</th></tr><tr><td>A</td><td><span class="ph" title="placeholder">2022</span></td><td>thesis submitted</td></tr></table>
<div class="tb">
<div><span class="k">project</span><span class="title">HyperCache, bachelor thesis</span></div>
<div><span class="k">dwg no.</span><span class="v">LW-02</span></div>
<div><span class="k">rev / year</span><span class="v">A · <span class="ph" title="placeholder">2022</span></span></div>
<div><span class="k">stack</span><span class="v">Node.js · simulation · P2P</span></div>
<div><span class="k">role</span><span class="v">author, BSc CS</span></div>
<div class="stampcell"><span class="stamp hand">by hand<small>no AI</small></span></div>
</div>
</div>
</article>
<!-- ===== SHEET 03: ROWING ===== -->
<article class="sheet" id="s-rowing" data-id="rowing" data-ai="hand" tabindex="0">
<span class="selmark">▸ selected</span>
<div class="zones-x"><span>A</span><span>B</span><span>C</span><span>D</span><span>E</span><span>F</span></div>
<div class="zones-y"><span>1</span><span>2</span><span>3</span><span>4</span></div>
<div class="sheet-in">
<div class="fig">
<svg viewBox="0 0 520 340" role="img" aria-label="Figure: plan view of the rowing rig with two oars, angle sensors at the oarlocks, an Arduino and a headset, with a dimension line for oar length and a swept angle">
<!-- seat & rail -->
<path class="ctr" d="M260 20 V300"/>
<rect class="ln" x="230" y="120" width="60" height="140"/>
<rect class="hatch" x="240" y="200" width="40" height="30" stroke="#1c2024" stroke-width="1"/>
<text class="lbl-s" x="242" y="250">seat</text>
<!-- rigger -->
<path class="ln" d="M130 140 H390"/>
<circle class="ln" cx="140" cy="140" r="7"/><circle class="ln" cx="380" cy="140" r="7"/>
<!-- oars -->
<path class="ln" d="M140 140 L30 80 M380 140 L490 80"/>
<path class="ln" d="M30 80 l-12 -10 l-8 14 l14 6z M490 80 l12 -10 l8 14 l-14 6z"/>
<path class="ln" d="M140 140 L236 168 M380 140 L284 168"/>
<!-- swept angle -->
<path class="dim" d="M80 113 A70 70 0 0 0 76 170"/><path class="hid" d="M140 140 L40 196"/>
<text class="dimt" x="44" y="146">≈70°</text>
<!-- oar length -->
<path class="dim" d="M397 170 L520 106" /><path class="dim" d="M386 154 L404 186 M496 96 L514 128"/>
<path class="dim" d="M395 170 L505 112" marker-start="url(#ar)" marker-end="url(#ar)"/>
<text class="dimt" x="452" y="158" transform="rotate(-28 452 158)"><tspan class="ph">≈ 2.9 m</tspan></text>
<!-- arduino -->
<rect class="ln" x="226" y="44" width="68" height="40"/><text class="lbl" x="234" y="62">Arduino</text><text class="lbl-s" x="234" y="76">angles @ 60Hz</text>
<path class="hid" d="M140 133 C150 80 200 64 226 64 M380 133 C370 80 320 64 294 64"/>
<!-- headset -->
<path class="ln" d="M232 290 h56 q8 0 8 8 v8 q0 8 -8 8 h-16 l-6 -6 h-12 l-6 6 h-16 q-8 0 -8 -8 v-8 q0 -8 8 -8z"/>
<text class="lbl-s" x="300" y="306">headset · Unity</text>
<path class="hid" d="M226 70 C150 90 150 270 230 298"/>
<!-- callouts -->
<g><path class="lead" d="M140 140 L100 220 L100 240" marker-start="url(#dotr)"/><g class="cb"><circle cx="100" cy="248" r="8"/><text x="100" y="248">1</text></g></g>
<g><path class="lead" d="M294 52 L360 30" marker-start="url(#dotr)"/><g class="cb"><circle cx="368" cy="28" r="8"/><text x="368" y="28">2</text></g></g>
<g><path class="lead" d="M234 304 L190 304" marker-start="url(#dotr)"/><g class="cb"><circle cx="182" cy="304" r="8"/><text x="182" y="304">3</text></g></g>
<g><path class="lead" d="M30 80 L40 34" marker-start="url(#dotr)"/><g class="cb"><circle cx="42" cy="26" r="8"/><text x="42" y="26">4</text></g></g>
</svg>
<div class="figcap"><span>FIG. 3 · rig, plan view · <span class="ph" title="placeholder">dimensions approximate</span></span><span>hidden lines = cables</span></div>
</div>
<div class="notes">
<h3>Notes</h3>
<ol>
<li><b>Oarlock sensor.</b> An angle sensor where each oar pivots; angle, not force, is what the boat needs.</li>
<li><b>Arduino.</b> Reads both sensors and streams angles to Unity over USB serial.</li>
<li><b>Unity.</b> C# turns two angle streams into a boat; mixed reality, so you still see your hands.</li>
<li><b>Real oars.</b> Two of them, with real weight. <span class="ph" title="placeholder">Taped, not welded.</span></li>
</ol>
<div class="gen">
<p><strong>what &amp; why</strong>A mixed-reality rowing game played with two real oars. Does real resistance make a virtual space feel more real?</p>
<div class="links"><a href="#">video</a><a href="https://github.com/StereotypicalCat">GitHub</a></div>
</div>
</div>
<table class="rev"><tr><th>REV</th><th>DATE</th><th>CHANGE</th></tr><tr><td>A</td><td><span class="ph" title="placeholder">2020</span></td><td>course project, group</td></tr></table>
<div class="tb">
<div><span class="k">project</span><span class="title">Mixed-reality rowing game</span></div>
<div><span class="k">dwg no.</span><span class="v">LW-03</span></div>
<div><span class="k">rev / year</span><span class="v">A · <span class="ph" title="placeholder">2020</span></span></div>
<div><span class="k">stack</span><span class="v">Unity · C# · Arduino</span></div>
<div><span class="k">role</span><span class="v">hardware + game, <span class="ph" title="placeholder">group of 3</span></span></div>
<div class="stampcell"><span class="stamp hand">by hand<small>no AI</small></span></div>
</div>
</div>
</article>
<!-- ===== SHEET 04: CONFIG ===== -->
<article class="sheet" id="s-config" data-id="config" data-ai="ai" tabindex="0">
<span class="selmark">▸ selected</span>
<div class="zones-x"><span>A</span><span>B</span><span>C</span><span>D</span><span>E</span><span>F</span></div>
<div class="zones-y"><span>1</span><span>2</span><span>3</span><span>4</span></div>
<div class="sheet-in">
<div class="fig">
<svg viewBox="0 0 520 320" role="img" aria-label="Figure: repository tree on the left, ansible in the middle, three hosts with compose stacks on the right">
<rect class="ln" x="20" y="30" width="150" height="250"/>
<text class="lbl" x="30" y="50">config/</text>
<g class="lbl-s"><text x="40" y="74">inventory.yml</text><text x="40" y="96">group_vars/</text><text x="40" y="118">roles/</text><text x="56" y="138">docker/</text><text x="56" y="156">backup/</text><text x="56" y="174">monitoring/</text><text x="40" y="198">compose/</text><text x="56" y="218">media.yml</text><text x="56" y="236">services.yml</text><text x="40" y="262">vault.yml (enc.)</text></g>
<path class="ln-th" d="M32 58 V258 M32 70 H38 M32 92 H38 M32 114 H38 M48 120 V170 M48 134 H54 M48 152 H54 M48 170 H54 M32 194 H38 M48 200 V232 M48 214 H54 M48 232 H54 M32 258 H38"/>
<rect class="ln" x="210" y="130" width="96" height="44"/><text class="lbl" x="222" y="150">ansible</text><text class="lbl-s" x="222" y="164">playbook site.yml</text>
<path class="ln-th" d="M170 152 H208" marker-end="url(#ar-i)"/>
<g><rect class="ln" x="360" y="40" width="140" height="60"/><rect class="ln" x="360" y="130" width="140" height="60"/><rect class="ln" x="360" y="220" width="140" height="60"/></g>
<g class="lbl"><text x="370" y="58">nas</text><text x="370" y="148">mini-1</text><text x="370" y="238">mini-2</text></g>
<g><rect class="hatch" x="370" y="66" width="36" height="24"/><rect class="hatch" x="412" y="66" width="36" height="24"/><rect class="hatch" x="370" y="156" width="36" height="24"/><rect class="hatch" x="412" y="156" width="36" height="24"/><rect class="hatch" x="454" y="156" width="36" height="24"/><rect class="hatch" x="370" y="246" width="36" height="24"/></g>
<path class="ln-th" d="M306 144 L358 70 M306 152 H358 M306 160 L358 250" marker-end="url(#ar-i)"/>
<path class="dim" d="M360 300 H500" marker-start="url(#ar)" marker-end="url(#ar)"/><text class="dimt" x="392" y="314">3 hosts, 1 command</text>
<g><path class="lead" d="M120 262 L190 262 L190 290" marker-start="url(#dotr)"/><g class="cb"><circle cx="190" cy="298" r="8"/><text x="190" y="298">1</text></g></g>
<g><path class="lead" d="M258 130 L258 96" marker-start="url(#dotr)"/><g class="cb"><circle cx="258" cy="88" r="8"/><text x="258" y="88">2</text></g></g>
<g><path class="lead" d="M100 120 L200 100 L200 70" marker-start="url(#dotr)"/><g class="cb"><circle cx="200" cy="62" r="8"/><text x="200" y="62">3</text></g></g>
<g><path class="lead" d="M430 168 L430 200 L340 200" marker-start="url(#dotr)"/><g class="cb"><circle cx="332" cy="200" r="8"/><text x="332" y="200">4</text></g></g>
</svg>
<div class="figcap"><span>FIG. 4 · repo and hosts · <span class="ph" title="placeholder">host and file names placeholder</span></span><span>hatched = compose stack</span></div>
</div>
<div class="notes">
<h3>Notes</h3>
<ol>
<li><b>Secrets.</b> In an encrypted vault file, never in plain history.</li>
<li><b>One command.</b> Any host rebuilt from bare metal with a single playbook run.</li>
<li><b>Roles.</b> Generated by AI, then iterated for a month; the layout was written by hand.</li>
<li><b>Compose, not Kubernetes.</b> The lab is three machines.</li>
</ol>
<div class="gen">
<p><strong>what &amp; why</strong>Ansible + docker-compose deployment management for the homelab, with git as the documentation.</p>
<p><strong>broke</strong><span class="ph" title="placeholder">Every run restarted every container: a template rendered a new timestamp. Found by hand with --diff.</span></p>
<div class="links"><a href="https://github.com/StereotypicalCat">GitHub</a></div>
</div>
</div>
<table class="rev"><tr><th>REV</th><th>DATE</th><th>CHANGE</th></tr><tr><td>A</td><td><span class="ph" title="placeholder">2025</span></td><td>first hosts</td></tr><tr><td>B</td><td><span class="ph" title="placeholder">2025</span></td><td>idempotent templates</td></tr></table>
<div class="tb">
<div><span class="k">project</span><span class="title">Config</span></div>
<div><span class="k">dwg no.</span><span class="v">LW-04</span></div>
<div><span class="k">rev / year</span><span class="v">B · <span class="ph" title="placeholder">2025</span></span></div>
<div><span class="k">stack</span><span class="v">Ansible · docker-compose · IaC</span></div>
<div><span class="k">role</span><span class="v">solo, homelab</span></div>
<div class="stampcell"><span class="stamp ai">AI-made<small>spec by hand</small></span></div>
</div>
</div>
</article>
</div>
<!-- ============ REGISTER ============ -->
<div class="sechead" id="register"><h2>Drawing register</h2><p>Every project. Click a row to open its sheet in place.</p><span class="k" id="regcount"></span></div>
<div class="register">
<table>
<thead><tr><th>dwg</th><th>title</th><th class="st">stack</th><th class="yr">year</th><th>made</th><th></th></tr></thead>
<tbody id="reg"></tbody>
</table>
</div>
<footer class="foot">
<p>Set in IBM Plex Sans, Sans Condensed and Mono, on the gate's grid paper. Stamps: double ring = by hand, red = AI-made, dashed = not marked yet.</p>
<p><b>Placeholders</b> (dotted underline): university, project years, revision tables, roles and group sizes, all blog titles and dates, the CV PDF, the AU projects' AI level, figure dimensions and host names, "interesting / broke" notes, the ScienceAtHome project name. Figures are drawn placeholders for real screenshots.</p>
<p><a href="nav-d.html">← back to the gate</a> · <a href="nav-d.html">off the clock →</a> · <a href="https://old.lucaswinther.info">the 2021 site (by hand)</a></p>
</footer>
</div>
<script>
(function(){
var W = { // placeholder blog titles
slurm:["A Slurm cluster that scales to zero","Sep 2026"],
entra:["Entra ID, OAuth and the redirect URI that ate a week","Jul 2026"],
agent:["What I let the agent write, and what I didn't","Oct 2026"],
ansible:["Ansible idempotency, the hard way","May 2026"],
k3s:["Three mini PCs, one k3s cluster","Jun 2026"],
alerts:["Four alerts, not forty","Aug 2026"],
thesis:["Reading a thesis I wrote by hand, three years later","Mar 2026"]
};
var LATEST = ["agent","slurm","alerts","entra"];
// id, no, title, group, year, stack, ai, one-liner, skills, writing, links, share(hand%), featured, callouts
var P = [
["compute","LW-01","AU compute platform","at work · AU","2025–","Slurm · React · Node · OCI","unk","Slurm job system with a React/Node web app, elastic on Oracle Cloud, Entra ID login, ERDA storage.",["Slurm","React","Node","Cloud (OCI)","OAuth","Linux"],["slurm","entra"],[],null,1],
["docs","LW-05","Document & information services","at work · AU","2025–","Overleaf · OpenProject","unk","Self-hosted Overleaf and OpenProject for research groups, wired into university login and each other.",["Docker","Linux","OAuth"],[],[],null,0,["login","Overleaf","OpenProject"]],
["ops","LW-06","Kept secure, kept running","at work · AU","2025–","OpenBao · Grafana · Docker · Ansible","unk","Testing new software for researchers while keeping it isolated, updated and monitored.",["Docker","Ansible","Observability","Linux","Vault"],["alerts"],[],null,0,["containers","OpenBao","Grafana"]],
["sah","LW-07","Early prototype, ScienceAtHome","at work · internship","2019","prototype","hand","Ideation and planning with the team, an early prototype of the base mechanics, pair programming to start the real project.",["Prototyping"],[],[],100,0,["base mechanics","demo for stakeholders"]],
["gacha","LW-08","Event Tracker for Gacha Games","on my own","2025","JS · scraping · web app","ai","One calendar for limited-time events across several gacha games. Spec and tests by hand, code by an agent.",["JavaScript","Scraping","Testing"],["agent"],[["live demo","#"],["GitHub","https://github.com/StereotypicalCat"]],10,0,["scrapers","normaliser","calendar"]],
["config","LW-04","Config","on my own","2025","Ansible · compose · IaC","ai","Homelab deployment management: every host rebuildable with one command.",["Ansible","Docker","Linux","IaC"],["ansible","k3s","agent"],[],15,1],
["chess","LW-09","Chessswap","on my own","2020","React","hand","Chess where the board swaps every few turns.",["React","JavaScript"],[],[["GitHub","https://github.com/StereotypicalCat"]],100,0,["board","swap timer"]],
["expenses","LW-10","Monthly Expenses Tracker","on my own","2021","JS · web app","hand","Monthly spending in DKK, amounts kept in øre.",["JavaScript"],[],[["GitHub","https://github.com/StereotypicalCat"]],100,0,["month view","DKK totals"]],
["lol","LW-11","LoL Winrate Discord Bot","on my own","2019","Node.js · Discord","hand","Ask in chat, get a champion's win rate back.",["Node","JavaScript"],[],[["GitHub","https://github.com/StereotypicalCat"]],100,0,["command","reply"]],
["portfolio","LW-12","Portfolio, 2021","on my own","2021","React · Material UI","hand","The previous site, frozen as it was.",["React","JavaScript"],[],[["old.lucaswinther.info","https://old.lucaswinther.info"]],100,0,["side panel","project grid"]],
["borlaug","LW-13","Tribute to Norman Borlaug","on my own","2016","HTML · CSS","hand","One of my first websites, from freeCodeCamp.",["HTML/CSS"],[],[],100,0,["portrait","timeline"]],
["hypercache","LW-02","HyperCache, bachelor thesis","school · university","2022","Node.js · simulation","hand","Reputation systems for peer-to-peer archival, simulated in Node.js.",["Node","Distributed systems","Research"],["thesis"],[],100,1],
["rowing","LW-03","Mixed-reality rowing game","school · university","2020","Unity · C# · Arduino","hand","A rowing game played with two real oars.",["Unity","C#","Arduino","Hardware"],[],[],100,1],
["dashboard","LW-14","Student Dashboard","school · university","2021","React · user studies","hand","A student dashboard designed from two rounds of user studies.",["React","User studies"],[],[["report PDF","#"],["demo","#"]],100,0,["overview","deadlines"]],
["vr","LW-15","VR-Classroom","school · university","2019","Node.js · prototype","hand","A shared virtual classroom prototype, synced by a Node.js server.",["Node","VR"],[],[["report","#"],["video","#"]],100,0,["sync server","clients"]],
["censor","LW-16","Censor Simulator","school · high school","2018","Unity · VR","hand","High-school exam project: a VR game about being the censor.",["Unity","C#","VR"],[],[],100,0,["desk","documents"]],
["breakout","LW-17","BreakoutPython","school · high school","2017","Python","hand","High-school exam project: Breakout, from scratch.",["Python"],[],[["GitHub","https://github.com/StereotypicalCat"]],100,0,["paddle","bricks"]]
];
var byId = {}; P.forEach(function(p){ byId[p[0]] = p; });
var AIL = {hand:"by hand", ai:"AI-made", unk:"not marked"};
var YEAR_PH = function(y){ return /^20(2[5-9])/.test(y) && y.indexOf("–")>0 ? y : '<span class="ph" title="placeholder year">'+y+'</span>'; };
// ---------- register ----------
var reg = document.getElementById("reg"), html = "", last = "";
P.slice().sort(function(a,b){ var o=["at work","on my own","school"]; return o.indexOf(a[3].split(" · ")[0]) - o.indexOf(b[3].split(" · ")[0]); }).forEach(function(p){
var g = p[3].split(" · ")[0];
if (g !== last){ html += '<tr class="grp" data-g="'+g+'"><td colspan="6">'+g+'</td></tr>'; last = g; }
html += '<tr class="r" data-id="'+p[0]+'" data-ai="'+p[6]+'" tabindex="0" aria-expanded="false"><td class="no">'+p[1]+'</td><td class="t">'+p[2]+''+(p[3].split(" · ")[1]?'<small>'+p[3].split(" · ")[1]+'</small>':'')+'</td><td class="st">'+p[5]+'</td><td class="yr">'+YEAR_PH(p[4])+'</td><td class="made"><i class="aid '+(p[6]==="hand"?"":p[6])+'"></i>'+AIL[p[6]]+'</td><td class="op">'+(p[12]?'sheet ↑':'open ▾')+'</td></tr>';
});
reg.innerHTML = html;
function miniSheet(p){
var c = p[13] || ["part A","part B"];
var pts = [[70,60],[200,52],[140,118]];
var callouts = c.map(function(t,i){ var q=pts[i]; return '<path class="lead" d="M'+q[0]+' '+q[1]+' L'+(q[0]+(i%2?60:-40))+' '+(q[1]-26)+'" marker-start="url(#dotr)"/><g class="cb"><circle cx="'+(q[0]+(i%2?66:-46))+'" cy="'+(q[1]-30)+'" r="8"/><text x="'+(q[0]+(i%2?66:-46))+'" y="'+(q[1]-30)+'">'+(i+1)+'</text></g>'; }).join("");
var links = (p[10]||[]).map(function(l){ return '<a href="'+l[1]+'">'+l[0]+'</a>'; }).join("");
var stamp = p[6]==="ai" ? '<span class="stamp ai">AI-made</span>' : p[6]==="unk" ? '<span class="stamp unk" title="placeholder">AI level<small>not marked</small></span>' : '<span class="stamp hand">by hand<small>no AI</small></span>';
return '<article class="sheet" data-id="'+p[0]+'"><div class="zones-x"><span>A</span><span>B</span><span>C</span><span>D</span></div><div class="zones-y"><span>1</span><span>2</span></div><div class="sheet-in">'+
'<div class="fig mini-fig"><svg viewBox="0 0 300 160" role="img" aria-label="placeholder figure"><rect class="ln" x="40" y="40" width="220" height="110"/><path class="phbox" d="M40 40 L260 150 M260 40 L40 150"/><text class="lbl-s" x="150" y="100" text-anchor="middle" style="paint-order:stroke;stroke:#f3f4f1;stroke-width:6px">placeholder: screenshot</text><path class="dim" d="M40 156 H260" marker-start="url(#ar)" marker-end="url(#ar)"/>'+callouts+'</svg><div class="figcap"><span>FIG. '+p[1].slice(3)+' · placeholder figure</span></div></div>'+
'<div class="notes"><h3>Notes</h3><ol>'+c.map(function(t){ return '<li><b>'+t+'.</b> <span class="ph" title="placeholder">detail to come</span></li>'; }).join("")+'</ol><div class="gen"><p><strong>what &amp; why</strong>'+p[7]+'</p>'+(links?'<div class="links">'+links+'</div>':'')+'</div></div>'+
'<div class="tb"><div><span class="k">project</span><span class="title" style="font-size:16px">'+p[2]+'</span></div><div><span class="k">dwg no.</span><span class="v">'+p[1]+'</span></div><div><span class="k">year</span><span class="v">'+YEAR_PH(p[4])+'</span></div><div><span class="k">stack</span><span class="v">'+p[5]+'</span></div><div><span class="k">group</span><span class="v">'+p[3]+'</span></div><div class="stampcell">'+stamp+'</div></div>'+
'</div></article>';
}
function toggleRow(tr, forceOpen){
var p = byId[tr.dataset.id];
if (p[12]) { var s = document.getElementById("s-"+p[0]); s.scrollIntoView(); select(p[0]); return; }
var next = tr.nextElementSibling, isOpen = next && next.classList.contains("drawer");
if (isOpen && !forceOpen){ next.remove(); tr.classList.remove("open"); tr.setAttribute("aria-expanded","false"); tr.querySelector(".op").textContent="open ▾"; return; }
if (!isOpen){
var d = document.createElement("tr"); d.className = "drawer"; d.innerHTML = '<td colspan="6">'+miniSheet(p)+'</td>';
tr.after(d); tr.classList.add("open"); tr.setAttribute("aria-expanded","true"); tr.querySelector(".op").textContent="close ▴";
}
select(p[0]);
}
reg.addEventListener("click", function(e){ var tr = e.target.closest("tr.r"); if (tr && !e.target.closest("a")) toggleRow(tr); });
reg.addEventListener("keydown", function(e){ var tr = e.target.closest("tr.r"); if (tr && (e.key==="Enter"||e.key===" ")){ e.preventDefault(); toggleRow(tr); } });
// ---------- inspector ----------
var $ = function(id){ return document.getElementById(id); };
var current = null;
function skillUsers(skill){ return P.filter(function(p){ return p[8].indexOf(skill) > -1; }); }
function select(id){
if (current === id) return; current = id;
var p = byId[id];
document.querySelectorAll(".sheet[id]").forEach(function(s){ s.classList.toggle("sel", s.dataset.id === id); });
document.querySelectorAll("#tabs a[data-sheet]").forEach(function(a){ a.classList.toggle("on", a.dataset.sheet === id); });
$("i-no").textContent = p[1]; $("i-group").textContent = p[3]; $("i-title").textContent = p[2]; $("i-one").textContent = p[7];
$("i-links").innerHTML = (p[10]||[]).map(function(l){ return '<a href="'+l[1]+'">'+l[0]+'</a>'; }).join("");
var h = p[11];
$("i-bar").innerHTML = h==null ? '<i class="u" style="width:100%"></i>' : '<i class="h" style="width:'+h+'%"></i><i class="a" style="width:'+(100-h)+'%"></i>';
$("i-who").innerHTML = h==null ? '<span class="ph" title="placeholder">not marked yet: work project, breakdown to come</span>' : h===100 ? 'all by hand' : '≈ '+h+'% by hand, '+(100-h)+'% AI <span class="ph" title="placeholder">(estimate)</span>';
$("i-skills").innerHTML = p[8].map(function(s){ return '<button data-skill="'+s+'" aria-pressed="false">'+s+' <span style="opacity:.55">'+skillUsers(s).length+'</span></button>'; }).join("");
$("i-skillnote").textContent = "";
clearLit();
$("i-writing").innerHTML = p[9].length ? p[9].map(function(k){ return '<li><a href="https://blog.lucaswinther.info"><span class="ph" title="placeholder title" style="font:inherit;color:inherit">'+W[k][0]+'</span></a><span>'+W[k][1]+' · placeholder</span></li>'; }).join("") : '<li><span>nothing written about this one yet</span></li>';
}
function clearLit(){ document.querySelectorAll("#reg tr.lit").forEach(function(t){ t.classList.remove("lit"); }); }
$("i-skills").addEventListener("click", function(e){
var b = e.target.closest("button"); if (!b) return;
var on = b.getAttribute("aria-pressed") !== "true";
document.querySelectorAll("#i-skills button").forEach(function(x){ x.setAttribute("aria-pressed","false"); });
clearLit();
if (!on){ $("i-skillnote").textContent = ""; return; }
b.setAttribute("aria-pressed","true");
var users = skillUsers(b.dataset.skill).filter(function(p){ return p[0] !== current; });
users.forEach(function(p){ var tr = reg.querySelector('tr.r[data-id="'+p[0]+'"]'); if (tr) tr.classList.add("lit"); });
$("i-skillnote").innerHTML = users.length ? "also in: " + users.map(function(p){ return '<a href="#register" data-go="'+p[0]+'">'+p[2]+'</a>'; }).join(", ") + " (marked in the register)" : "only on this sheet";
});
$("i-skillnote").addEventListener("click", function(e){
var a = e.target.closest("[data-go]"); if (!a) return; e.preventDefault();
var tr = reg.querySelector('tr.r[data-id="'+a.dataset.go+'"]'); if (!tr) return;
toggleRow(tr, true); (tr.classList.contains("open") ? tr : document.getElementById("s-"+a.dataset.go) || tr).scrollIntoView({block:"start"});
});
$("i-latest").innerHTML = LATEST.map(function(k){ return '<li><a href="https://blog.lucaswinther.info"><span class="ph" title="placeholder title" style="font:inherit;color:inherit">'+W[k][0]+'</span></a><span>'+W[k][1]+'</span></li>'; }).join("");
document.getElementById("sheets").addEventListener("click", function(e){ var s = e.target.closest(".sheet"); if (s) select(s.dataset.id); });
document.getElementById("sheets").addEventListener("focusin", function(e){ var s = e.target.closest(".sheet"); if (s) select(s.dataset.id); });
if ("IntersectionObserver" in window){
var io = new IntersectionObserver(function(es){ es.forEach(function(en){ if (en.isIntersecting) select(en.target.dataset.id); }); }, {rootMargin:"-40% 0px -50% 0px"});
document.querySelectorAll(".sheet[id]").forEach(function(s){ io.observe(s); });
}
select("compute");
// ---------- AI filter ----------
function applyPref(pref){
var hidden = 0;
function hideFor(ai){ return (pref==="nofull" && ai==="ai") || (pref==="hand" && ai!=="hand"); }
document.querySelectorAll(".sheet[id]").forEach(function(s){ var h = hideFor(s.dataset.ai); s.classList.toggle("hide", h); var t = document.querySelector('#tabs a[data-sheet="'+s.dataset.id+'"]'); if (t) t.classList.toggle("off", h); });
reg.querySelectorAll("tr.r").forEach(function(tr){
var h = hideFor(tr.dataset.ai); tr.classList.toggle("hide", h); if (h) hidden++;
var n = tr.nextElementSibling; if (h && n && n.classList.contains("drawer")) { n.remove(); tr.classList.remove("open"); tr.querySelector(".op").textContent="open ▾"; }
});
reg.querySelectorAll("tr.grp").forEach(function(g){
var n = g.nextElementSibling, any = false;
while (n && !n.classList.contains("grp")) { if (n.classList.contains("r") && !n.classList.contains("hide")) any = true; n = n.nextElementSibling; }
g.classList.toggle("hide", !any);
});
$("regcount").textContent = (P.length - hidden) + " of " + P.length + " shown" + (pref==="hand" ? " · unmarked work hidden" : "");
document.querySelectorAll("[data-pref]").forEach(function(b){ b.setAttribute("aria-pressed", b.dataset.pref===pref ? "true" : "false"); });
}
document.querySelectorAll("[data-pref]").forEach(function(b){ b.addEventListener("click", function(){ applyPref(b.dataset.pref); }); });
applyPref("all");
var m = location.search.match(/open=(\w+)/); if (m){ var tr0 = reg.querySelector('tr.r[data-id="'+m[1]+'"]'); if (tr0){ toggleRow(tr0); tr0.scrollIntoView(); } }
document.addEventListener("keydown", function(e){ if (e.key === "Escape" && !(e.target.closest && e.target.closest("input,textarea"))) location.href = "nav-d.html"; });
})();
</script>
</body>
</html>