feat(projects): add playground architecture diagram and notes

Update the Playground entry on the projects page: diagram,
prerender, privacy and operations bullets, one challenge,
one learning, and the Traefik, nginx and Tailscale technologies.
This commit is contained in:
lobo committed 2026-10-21 08:04:25 +02:00
1 parent e13db8c1de
commit 910822028b
5 files changed
+119 -9

No files matched your search

+1
View File
@@ -42,6 +42,7 @@ export class AssetsConstants {
];
static readonly CLAUDE_BOX_DIAGRAM = '/assets/projects/claude-in-a-box/1.svg';
static readonly PLAYGROUND_DIAGRAM = '/assets/projects/playground/architecture.svg';
static readonly TBP_DIAGRAM = '/assets/projects/to-be-played/architecture.svg';
static readonly TRIBBLE_DIAGRAM = '/assets/projects/tribble-the-homeserver/architecture.svg';
+5 -1
View File
@@ -125,6 +125,7 @@ export class ProjectsComponent implements OnInit, OnDestroy {
shortDescription: 'PROJECTS.PLAYGROUND.SHORT_DESCRIPTION',
introduction: 'PROJECTS.PLAYGROUND.INTRODUCTION',
images: AssetsConstants.PLAYGROUND_IMAGES.map(url => ({ url, source: '' })),
diagram: AssetsConstants.PLAYGROUND_DIAGRAM,
icon: 'web',
assets: '',
links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://andreas-dahm.eu' }],
@@ -134,17 +135,20 @@ export class ProjectsComponent implements OnInit, OnDestroy {
'PROJECTS.PLAYGROUND.BULLET_3',
'PROJECTS.PLAYGROUND.BULLET_4',
'PROJECTS.PLAYGROUND.BULLET_5',
'PROJECTS.PLAYGROUND.BULLET_6',
],
challenges: [
'PROJECTS.PLAYGROUND.CHALLENGE_1',
'PROJECTS.PLAYGROUND.CHALLENGE_2',
'PROJECTS.PLAYGROUND.CHALLENGE_3',
],
learnings: [
'PROJECTS.PLAYGROUND.LEARNING_1',
'PROJECTS.PLAYGROUND.LEARNING_2',
'PROJECTS.PLAYGROUND.LEARNING_3',
],
isFeatured: false,
technologies: ['Angular', 'TypeScript', 'Tailwind CSS', 'WebGPU', 'GLSL', 'Babylon.js', 'Gitea Actions', 'Docker']
technologies: ['Angular', 'TypeScript', 'Tailwind CSS', 'WebGPU', 'GLSL', 'Babylon.js', 'Gitea Actions', 'Docker', 'Traefik', 'nginx', 'Tailscale']
},
{
identifier: "elmucho",
+7 -4
View File
@@ -310,16 +310,19 @@
"PLAYGROUND": {
"TITLE": "Playground",
"SHORT_DESCRIPTION": "Diese Seite: ein Portfolio mit Algorithmus-Visualisierungen, die im Browser laufen, einige davon auf der GPU.",
"INTRODUCTION": "Auf dieser Seite probiere ich Dinge aus. Ich wollte sehen, wie weit ein normaler Browser mit den Algorithmen kommt, die ich mag. Deshalb läuft jede Visualisierung im Client: Pfadfindung, Labyrinth-Erzeugung, Sortieren, Conways Game of Life, Fraktale in GLSL-Shadern und zwei Physiksimulationen, die als Compute Shader auf WebGPU laufen. Der Rest ist eine normale Angular-App mit deutschen und englischen Texten, einem Dark Mode und einer CI-Pipeline, die sie baut, testet und als Docker-Image in meine eigene Registry schiebt.",
"INTRODUCTION": "Auf dieser Seite probiere ich Dinge aus. Ich wollte sehen, wie weit ein normaler Browser mit den Algorithmen kommt, die ich mag. Deshalb läuft jede Visualisierung im Client: Pfadfindung, Labyrinth-Erzeugung, Sortieren, Conways Game of Life, Fraktale in GLSL-Shadern und zwei Physiksimulationen, die als Compute Shader auf WebGPU laufen. Der Rest ist eine normale Angular-App mit deutschen und englischen Texten, einem Dark Mode und einer Pipeline, die jeden Push auf main bis auf den Live-Server bringt.",
"BULLET_1": "Pfadfindung mit Dijkstra und A*, Labyrinth-Erzeugung mit Prim und Kruskal, fünf Sortieralgorithmen mit Ton, Conways Game of Life und eine Vier-Farben-Karte.",
"BULLET_2": "2D-Fraktale (Mandelbrot, Julia, Burning Ship, Newton) in einem GLSL-Shader und 3D-Fraktale (Mandelbulb, Mandelbox, Julia-Bulb) per Raymarching, mit Normalen aus dem Distanzfeld.",
"BULLET_3": "Ein Doppelpendel und eine Stoffsimulation laufen als Compute Shader auf WebGPU. Ohne WebGPU weichen sie auf die CPU oder WebGL aus und melden das in einer Snackbar.",
"BULLET_4": "Angular mit Material und Tailwind, Signals für den Zustand, Deutsch und Englisch und ein Service Worker (PWA).",
"BULLET_5": "CI auf Gitea Actions: Lint, i18n-Prüfung, Tests, Produktions-Build und ein Lighthouse-Bericht. Auf main baut sie ein Docker-Image und schiebt es in meine eigene Registry.",
"BULLET_4": "Angular mit Material und Tailwind, Signals für den Zustand, Deutsch und Englisch. Jede Route wird beim Build in eine eigene HTML-Datei mit eigenem Titel und eigenen Meta-Tags vorgerendert, damit ein geteilter Link eine richtige Vorschau bekommt. Im Browser übernehmen dann Angular und ein Service Worker (PWA) die statische Seite.",
"BULLET_5": "Privacy by Design: Die Schriften kommen von der Seite selbst, es gibt keine Cookies, kein Tracking und kein Access-Log auf dem Server. Nur Sprache und Theme liegen im Local Storage des Browsers.",
"BULLET_6": "Betrieb: Die CI auf Gitea Actions auf meinem Homeserver Tribble führt Lint, i18n-Prüfung, Tests, den Produktions-Build, eine Prerender-Prüfung und Lighthouse aus. Ein Push auf main baut ein Docker-Image; auf dem VPS holt Watchtower es ab, nginx liefert die statischen Dateien aus und Traefik übernimmt HTTPS. Gitea selbst ist über Traefik und Tailscale erreichbar.",
"CHALLENGE_1": "Der Stoff-Solver auf der GPU. XPBD-Constraints, die sich einen Partikel teilen, lassen sich nicht parallel lösen, ohne eine Race Condition auszulösen. Ich gruppiere die Constraints per Graphfärbung in acht Phasen. So berühren innerhalb einer Phase nie zwei Constraints denselben Partikel, und jede Phase läuft voll parallel.",
"CHALLENGE_2": "Nicht jeder Browser hat WebGPU. Beide Simulationen haben einen CPU- oder WebGL-Pfad und eine Snackbar, die sagt, dass der Fallback aktiv ist. So funktioniert die Seite überall, und niemand fragt sich, warum es langsamer ist.",
"CHALLENGE_3": "Eine bestehende SPA vorrendern. Beim Build wird jede Seite in Node gerendert, und dort gibt es kein WebGPU, kein Canvas für Babylon.js und keinen localStorage. Browser-Code musste Komponente für Komponente abgesichert werden, bis der Build durchlief. Die Client-Shell index.csr.html bleibt, weil Service Worker und 404-Seite sie weiter brauchen.",
"LEARNING_1": "Compute Shader verändern, wie man über einen Algorithmus denkt. Das Datenlayout und die Reihenfolge der Schreibzugriffe zählen mehr als die Mathematik.",
"LEARNING_2": "Eine kleine CI mit einem Abgleich der Übersetzungsschlüssel und einem Lighthouse-Lauf fängt die langweiligen Fehler ab (eine fehlende Übersetzung, eine langsame Seite), bevor ich sie auf der Live-Seite sehe."
"LEARNING_2": "Eine kleine CI mit einem Abgleich der Übersetzungsschlüssel und einem Lighthouse-Lauf fängt die langweiligen Fehler ab (eine fehlende Übersetzung, eine langsame Seite), bevor ich sie auf der Live-Seite sehe.",
"LEARNING_3": "Eine Datenschutzseite ist eine Vorgabe für den Server. Sobald dort steht, dass es kein Access-Log und keine Verbindung zu Google gibt, darf nginx kein Log schreiben, und die Schriften müssen mit der Seite ausgeliefert werden. Es zuerst aufzuschreiben hat mir diese Entscheidungen abgenommen."
},
"TRIBBLE": {
"TITLE": "Self-Hosted Infrastructure",
+7 -4
View File
@@ -310,16 +310,19 @@
"PLAYGROUND": {
"TITLE": "Playground",
"SHORT_DESCRIPTION": "This site: a portfolio with algorithm visualizations that run in the browser, some of them on the GPU.",
"INTRODUCTION": "This site is where I try things out. I wanted to see how far a plain browser gets with the algorithms I like, so every visualization runs on the client: pathfinding, maze generation, sorting, Conway's Game of Life, fractals in GLSL shaders, and two physics simulations that run as compute shaders on WebGPU. The rest is a normal Angular app with German and English texts, a dark mode, and a CI pipeline that builds and tests it and pushes it as a Docker image to my own registry.",
"INTRODUCTION": "This site is where I try things out. I wanted to see how far a plain browser gets with the algorithms I like, so every visualization runs on the client: pathfinding, maze generation, sorting, Conway's Game of Life, fractals in GLSL shaders, and two physics simulations that run as compute shaders on WebGPU. The rest is a normal Angular app with German and English texts, a dark mode, and a pipeline that takes every push to main all the way to the live server.",
"BULLET_1": "Pathfinding with Dijkstra and A*, maze generation with Prim and Kruskal, five sorting algorithms with sound, Conway's Game of Life and a four-color map.",
"BULLET_2": "2D fractals (Mandelbrot, Julia, Burning Ship, Newton) in one GLSL shader, and 3D fractals (Mandelbulb, Mandelbox, Julia-Bulb) by raymarching with normals from the distance field.",
"BULLET_3": "A double pendulum and a cloth simulation run as compute shaders on WebGPU. Without WebGPU they fall back to the CPU or WebGL and say so in a snackbar.",
"BULLET_4": "Angular with Material and Tailwind, signals for state, German and English, and a service worker (PWA).",
"BULLET_5": "CI on Gitea Actions: lint, i18n check, tests, production build and a Lighthouse report. On main it builds a Docker image and pushes it to my own registry.",
"BULLET_4": "Angular with Material and Tailwind, signals for state, German and English. Every route is prerendered at build time into its own HTML with its own title and meta tags, so a shared link gets a proper preview. In the browser, Angular and a service worker (PWA) take over from the static page.",
"BULLET_5": "Privacy by design: the fonts come from the site itself, there are no cookies, no tracking and no access log on the server. Only language and theme are kept in the browser's local storage.",
"BULLET_6": "Operations: CI on Gitea Actions on my home server Tribble runs lint, i18n check, tests, the production build, a prerender check and Lighthouse. A push to main builds a Docker image; on the VPS, Watchtower pulls it, nginx serves the static files and Traefik handles HTTPS. Gitea itself is reached through Traefik over Tailscale.",
"CHALLENGE_1": "The cloth solver on the GPU. XPBD constraints that share a particle cannot be solved in parallel without a race. I group the constraints by graph coloring into eight phases, so inside one phase no two constraints touch the same particle and each phase runs fully in parallel.",
"CHALLENGE_2": "Not every browser has WebGPU. Both simulations have a CPU or WebGL path and a snackbar that says the fallback is active, so the page works everywhere and nobody wonders why it is slower.",
"CHALLENGE_3": "Prerendering a SPA that already existed. At build time every page is rendered in Node, and there is no WebGPU, no canvas for Babylon.js and no localStorage there. Browser-only code had to be guarded component by component until the build ran through. The client-side shell index.csr.html stays, because the service worker and the 404 page still need it.",
"LEARNING_1": "Compute shaders change how you think about an algorithm. The data layout and the order of writes matter more than the math.",
"LEARNING_2": "A small CI with a key-parity check and a Lighthouse run catches the boring mistakes, a missing translation or a slow page, before I see them on the live site."
"LEARNING_2": "A small CI with a key-parity check and a Lighthouse run catches the boring mistakes, a missing translation or a slow page, before I see them on the live site.",
"LEARNING_3": "A privacy page is a spec for the server. Once it says there is no access log and no connection to Google, nginx must keep no log and the fonts must ship with the site. Writing it down first made those decisions for me."
},
"TRIBBLE": {
"TITLE": "Self-Hosted Infrastructure",
@@ -0,0 +1,99 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630" role="img" aria-labelledby="title desc">
<title id="title">Playground</title>
<desc id="desc">Architecture of this site: a push to Gitea on the home server runs lint, i18n check, unit tests, a production build with prerendering, a prerender check and Lighthouse, then builds a Docker image into the registry. Watchtower on the Hostinger VPS pulls it over Tailscale, Traefik terminates HTTPS and nginx serves the prerendered HTML. Traefik also forwards git.andreas-dahm.eu to Gitea over Tailscale. The browser gets prerendered HTML first, then Angular and the service worker take over; the WebGPU and WebGL demos run on the client.</desc>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1e2530"/>
<stop offset="100%" stop-color="#15181d"/>
</linearGradient>
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#26c6da"/>
<stop offset="100%" stop-color="#ffa726"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#4a5563"/>
</marker>
<marker id="arrowCyan" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#26c6da"/>
</marker>
</defs>
<style>
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
.sans { font-family: Roboto, Arial, sans-serif; }
.lbl { fill: #8b97a6; font-size: 17px; letter-spacing: .04em; }
.chip { fill: #cdd6e0; font-size: 19px; }
.cap { fill: #6c7887; font-size: 15px; letter-spacing: .18em; }
.name { fill: #cdd6e0; font-size: 22px; font-weight: 600; }
.sub { fill: #8b97a6; font-size: 16px; }
.arr { fill: #8b97a6; font-size: 15px; }
</style>
<rect width="1200" height="630" fill="url(#bg)"/>
<rect x="0" y="0" width="1200" height="4" fill="url(#accent)"/>
<text x="64" y="72" class="mono" fill="#26c6da" font-size="26" font-weight="600">Playground</text>
<text x="64" y="102" class="sans lbl">push to main, prerender at build time, serve static</text>
<rect x="120" y="140" width="320" height="390" rx="18" fill="#1a222c" stroke="#26c6da" stroke-width="2"/>
<text x="144" y="168" class="sans cap" text-anchor="start">HOME SERVER "TRIBBLE"</text>
<rect x="144" y="186" width="272" height="98" rx="10" fill="#212a35" stroke="#2f3a47"/>
<text x="164" y="220" class="sans name">Gitea</text>
<text x="164" y="246" class="sans sub">git, Actions runner, registry</text>
<text x="164" y="268" class="sans sub">git.andreas-dahm.eu</text>
<text x="144" y="316" class="sans cap" text-anchor="start">ACTIONS RUNNER</text>
<rect x="144" y="328" width="130" height="40" rx="10" fill="#243040" stroke="#33475c"/>
<text x="209" y="353" class="sans chip" style="font-size:15px" text-anchor="middle">lint</text>
<rect x="286" y="328" width="130" height="40" rx="10" fill="#243040" stroke="#33475c"/>
<text x="351" y="353" class="sans chip" style="font-size:15px" text-anchor="middle">i18n check</text>
<rect x="144" y="376" width="130" height="40" rx="10" fill="#243040" stroke="#33475c"/>
<text x="209" y="401" class="sans chip" style="font-size:15px" text-anchor="middle">unit tests</text>
<rect x="286" y="376" width="130" height="40" rx="10" fill="#243040" stroke="#33475c"/>
<text x="351" y="401" class="sans chip" style="font-size:15px" text-anchor="middle">build + prerender</text>
<rect x="144" y="424" width="130" height="40" rx="10" fill="#243040" stroke="#33475c"/>
<text x="209" y="449" class="sans chip" style="font-size:15px" text-anchor="middle">prerender check</text>
<rect x="286" y="424" width="130" height="40" rx="10" fill="#243040" stroke="#33475c"/>
<text x="351" y="449" class="sans chip" style="font-size:15px" text-anchor="middle">Lighthouse</text>
<rect x="144" y="476" width="272" height="40" rx="10" fill="#243040" stroke="#33475c"/>
<text x="280" y="501" class="mono chip" style="font-size:17px" text-anchor="middle">Docker image → registry</text>
<rect x="556" y="140" width="320" height="390" rx="18" fill="#1a222c" stroke="#ffa726" stroke-width="2"/>
<text x="580" y="168" class="sans cap" text-anchor="start">HOSTINGER VPS</text>
<rect x="580" y="186" width="272" height="98" rx="10" fill="#2a2118" stroke="#5c4326"/>
<text x="600" y="220" class="sans name">Traefik</text>
<text x="600" y="246" class="sans sub">HTTPS, Let's Encrypt</text>
<text x="600" y="268" class="sans sub">andreas-dahm.eu</text>
<rect x="580" y="300" width="272" height="98" rx="10" fill="#212a35" stroke="#2f3a47"/>
<text x="600" y="334" class="sans name">nginx</text>
<text x="600" y="360" class="sans sub">prerendered HTML, gzip</text>
<text x="600" y="382" class="sans sub">no access log</text>
<rect x="580" y="440" width="272" height="76" rx="10" fill="#243040" stroke="#33475c"/>
<text x="600" y="474" class="sans name">Watchtower</text>
<text x="600" y="500" class="sans sub">pulls the new image</text>
<path d="M 716 284 V 292" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
<path d="M 716 440 V 406" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
<text x="730" y="424" class="sans arr" text-anchor="start">deploy</text>
<rect x="936" y="140" width="216" height="390" rx="18" fill="#1a222c" stroke="#2f3a47" stroke-width="2"/>
<text x="960" y="168" class="sans cap" text-anchor="start">BROWSER</text>
<rect x="952" y="186" width="184" height="76" rx="10" fill="#212a35" stroke="#2f3a47"/>
<text x="972" y="220" class="sans name" style="font-size:17px">Prerendered HTML</text>
<text x="972" y="246" class="sans sub">title + meta per route</text>
<rect x="952" y="300" width="184" height="76" rx="10" fill="#212a35" stroke="#2f3a47"/>
<text x="972" y="334" class="sans name" style="font-size:17px">Angular + PWA</text>
<text x="972" y="360" class="sans sub">service worker, offline</text>
<rect x="952" y="414" width="184" height="76" rx="10" fill="#212a35" stroke="#2f3a47"/>
<text x="972" y="448" class="sans name" style="font-size:17px">WebGPU / WebGL</text>
<text x="972" y="474" class="sans sub">demos on the client</text>
<path d="M 1044 262 V 292" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
<path d="M 1044 376 V 406" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
<path d="M 40 235 H 112" stroke="#26c6da" stroke-width="2" fill="none" marker-end="url(#arrowCyan)"/>
<text x="44" y="224" class="mono arr" text-anchor="start">git push</text>
<path d="M 548 224 H 448" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
<text x="498" y="212" class="sans arr" text-anchor="middle">via Tailscale</text>
<path d="M 424 496 H 548" stroke="#26c6da" stroke-width="2" fill="none" marker-end="url(#arrowCyan)"/>
<text x="486" y="466" class="sans arr" text-anchor="middle">new image</text>
<text x="486" y="484" class="sans arr" text-anchor="middle">via Tailscale</text>
<path d="M 884 224 H 928" stroke="#4a5563" stroke-width="2" fill="none" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
<text x="906" y="212" class="sans arr" text-anchor="middle">HTTPS</text>
<line x1="64" y1="566" x2="1136" y2="566" stroke="#2a333e"/>
<text x="64" y="598" class="mono" fill="#5b6673" font-size="15">angular · prerender · nginx · traefik · gitea actions · docker</text>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB