feat(projects): add decisions and result to project dialog
Add optional Decisions and Result blocks to the project dialog. Fill them for ToBePlayed, Tribble, Claude-in-a-Box and Playground. Remove Portainer and the Google Drive backup claims from Tribble.
This commit is contained in:
1 parent
1cff9e6eb2
commit
0857ce482b
6 files changed
+132
-21
No files matched your search
+26
-9
@@ -250,7 +250,7 @@
|
||||
},
|
||||
"TRIBBLE": {
|
||||
"TITLE": "Homeserver 'Tribble'",
|
||||
"DESCRIPTION": "A mini PC at home runs Gitea with its Actions runner and registry, plus Jellyfin, Paperless-ngx and AdGuard Home. A VPS serves the public sites and pulls new images over Tailscale with Watchtower. Running since May 2024, backups to Google Drive.",
|
||||
"DESCRIPTION": "A mini PC at home runs Gitea with its Actions runner and registry, plus Jellyfin, Paperless-ngx and AdGuard Home. A VPS serves the public sites and pulls new images over Tailscale with Watchtower. Running since May 2024.",
|
||||
"LINK_INTERNAL": "Project details",
|
||||
"HIGHLIGHTS": {
|
||||
"P1": "Gitea CI builds and tests this site and To Be Played at home; Watchtower deploys the images on the VPS.",
|
||||
@@ -305,7 +305,9 @@
|
||||
"CLOSE": "Close",
|
||||
"SECTION": {
|
||||
"TECHNICAL": "Technical Challenges",
|
||||
"LEARNINGS": "Learnings & Soft Skills"
|
||||
"LEARNINGS": "Learnings & Soft Skills",
|
||||
"DECISIONS": "Decisions",
|
||||
"RESULT": "Result"
|
||||
},
|
||||
"PLAYGROUND": {
|
||||
"TITLE": "Playground",
|
||||
@@ -322,7 +324,11 @@
|
||||
"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_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."
|
||||
"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.",
|
||||
"DECISION_1": "Prerender at build time instead of client rendering or an SSR server. Every route becomes its own HTML file at build time, and nginx serves it as a static file; no Node process runs on the VPS. The reason was the link preview: a shared link needs title and meta tags in the HTML, not only after the app has loaded. A Node server would have been too much effort for a site whose content runs in the browser anyway.",
|
||||
"DECISION_2": "Vitest with jsdom instead of Karma with Chrome. Karma needed a headless Chrome in the CI container; Vitest runs with no browser. The downside: jsdom applies no real CSS, so two tests on the loaded fonts were dropped.",
|
||||
"DECISION_3": "Tailwind without preflight instead of SCSS-first or all of Tailwind. The styles moved from one big styles.scss to utility classes in the templates, so a style sits next to the markup it affects; SCSS stays only for the Material theme and its overrides. Tailwind's own reset (preflight) stays out, so the reset of Angular Material keeps priority.",
|
||||
"RESULT": "The site runs the way the privacy page promises: no cookies, no tracking, no Google Fonts request, and the build fails as soon as one appears. A push to main is live within a few minutes, with no manual step. Shared links now show a title and a preview, and CI catches missing translations and a missing title or canonical tag before I see them on the live site."
|
||||
},
|
||||
"TRIBBLE": {
|
||||
"TITLE": "Self-Hosted Infrastructure",
|
||||
@@ -330,13 +336,16 @@
|
||||
"INTRODUCTION": "Two machines. At home a mini PC called \"Tribble\" runs Gitea with its Actions runner and container registry, plus the private services. A Hostinger VPS serves the public sites, this one included. A git push builds the image at home, the VPS pulls it over Tailscale with Watchtower, and Traefik does TLS and routing. It has been running since May 2024.",
|
||||
"BULLET_1": "Hardware: a KODLIX GD50 mini PC with an 8-core Intel Core i5, 32 GB RAM and a 512 GB SSD.",
|
||||
"BULLET_2": "Gitea for git, the Actions runner and the container registry. This site and To Be Played are built and tested there.",
|
||||
"BULLET_3": "Private services: Jellyfin for media, Paperless-ngx for documents, AdGuard Home for DNS and ad blocking, Portainer to look at the containers, Traefik in front.",
|
||||
"BULLET_3": "Private services: Jellyfin for media, Paperless-ngx for documents, AdGuard Home for DNS and ad blocking, Traefik in front.",
|
||||
"BULLET_4": "Tailscale links the two machines. The VPS fetches new images from the registry at home over it, and Watchtower swaps the running container.",
|
||||
"BULLET_5": "Backups go to Google Drive.",
|
||||
"CHALLENGE_1": "DNS. Names have to resolve to the right address from inside the house, over Tailscale and from the public internet, and AdGuard Home sits in the middle of it. Getting that right took longer than any single service.",
|
||||
"CHALLENGE_2": "Getting everything to run at the same time, and then making it repeatable. The first setup was a long list of manual steps. I automated it so the server can be rebuilt without me remembering each one.",
|
||||
"LEARNING_1": "Self-hosting is cheap in money and expensive in time. Most of the time goes into the parts nobody sees: DNS, certificates and backups.",
|
||||
"LEARNING_2": "A git push that ends in a running container on the VPS, with no manual step in between, is the one part of this setup I would not give up."
|
||||
"LEARNING_1": "Self-hosting is cheap in money and expensive in time. Most of the time goes into the parts nobody sees: DNS, certificates and updates.",
|
||||
"LEARNING_2": "A git push that ends in a running container on the VPS, with no manual step in between, is the one part of this setup I would not give up.",
|
||||
"DECISION_1": "The VPS is the only public entrance. Traefik on the VPS forwards the names hosted at home over Tailscale to the home server, with a few YAML files in its file provider. The alternative would have been a port forward on the home router. This way no port is open at home, and the home server's address stays private.",
|
||||
"DECISION_2": "Watchtower pulls on the VPS, tags stay pinned at home. The VPS checks for new images every minute and replaces only the labelled containers, one at a time; CI needs no access to the VPS for that. The home server gets no \"latest\": every image tag is pinned there, and an update is one manual step per stack, because with \"latest\" a database migration happens as a side effect, and that cannot be undone.",
|
||||
"DECISION_3": "Rebuild by script instead of from memory. bootstrap.sh creates the proxy network and the directories and warns about the DNS port clash between systemd-resolved and AdGuard; rebuild.sh starts the stacks in a fixed order, Traefik first, because alphabetically it would come last. For six Compose stacks, Ansible would cost more effort than it saves. Two shell scripts are enough and easier to read. The scripts cover the infrastructure, not the data.",
|
||||
"RESULT": "Today six stacks run at home: Traefik, AdGuard Home, Gitea with its runner, Jellyfin, Paperless-ngx and Leantime; Nextcloud is switched off again. The VPS runs Traefik, Watchtower and the public sites. Every push of this site and of To Be Played is built at home, pushed to my own registry on Tribble and reaches the VPS with no manual step. In September I updated all stacks in one pass, stack by stack with the pinned tags, and no service went down."
|
||||
},
|
||||
"EL_MUCHO": {
|
||||
"TITLE": "El Mucho (Steam Release)",
|
||||
@@ -397,7 +406,11 @@
|
||||
"CHALLENGE_4": "Knowing whether a change to the box helps. Every edit to a rule felt like an improvement, so I built the eval suite: fixed tasks with hidden checks, run before and after a change with the same models. The suite checks itself too: the untouched fixture must fail every task and the reference solution must pass, otherwise a green run means nothing.",
|
||||
"LEARNING_1": "Prompt wording mattered far less than I expected. Almost all of the difference came from the scaffolding.",
|
||||
"LEARNING_2": "Reviewing and gating the agent's output the same way you would a new colleague's pull request turned out to be the most useful rule of the lot.",
|
||||
"LEARNING_3": "Small agents with a fresh context each beat one long chat. Not because the model gets smarter, but because nobody carries earlier mistakes forward, and every hand-over is a file I can read and correct."
|
||||
"LEARNING_3": "Small agents with a fresh context each beat one long chat. Not because the model gets smarter, but because nobody carries earlier mistakes forward, and every hand-over is a file I can read and correct.",
|
||||
"DECISION_1": "Hooks instead of permission rules. Claude Code can block tools by rule, but a rule is per tool: a deny on reading .env files does not stop \"cat .env\" in the shell. So the secret guard, the commit gate and the type check are hooks that see every call. And since a hook timeout once let commits through unchecked, the gate fails closed: no answer means red.",
|
||||
"DECISION_2": "The host's Docker socket instead of a Docker daemon inside the container. That way the Testcontainers tests of the Java repos run in the commit gate instead of being skipped. The downside is in the README: whoever has the socket is root on the host. So the box is a work environment with pinned tools, not a sandbox.",
|
||||
"DECISION_3": "A second model, not only a second context. At first one model did everything and the second was never used. Now the model that plans and reviews is not the one that implements, because a second opinion is worth more when it does not come from the same model. For a one-file fix the switch is not worth it, so small changes skip steps of the pipeline.",
|
||||
"RESULT": "The box has been my daily tool since July 2026, in every repo I work in, at work and at home. The build with real Testcontainers runs in the commit gate. The pipeline passes every task of the eval suite, while the untouched starting point fails every one. Mostly that shows the tasks are still too easy; harder ones are the next step."
|
||||
},
|
||||
"TBP": {
|
||||
"TITLE": "To Be Played",
|
||||
@@ -413,7 +426,11 @@
|
||||
"CHALLENGE_3": "Suggesting a game is harder than it sounds. Each row of suggestions needs its own rules, and the lists behind every number must use the same rules as the number, so a game counts the same way everywhere.",
|
||||
"CHALLENGE_4": "Login abuse. Too many failed logins get a 429 with Retry-After. An unknown email still pays for a full BCrypt check, so the timing gives nothing away. A header guard blocks CSRF, CORS allows exact origins only, and the PlayStation token is encrypted at rest.",
|
||||
"LEARNING_1": "Splitting an app into services only pays off with a clear contract. The token claims are pinned in a contract file that both services test against, and a script I run by hand checks that both copies match.",
|
||||
"LEARNING_2": "A hobby project still needs the boring parts: daily database dumps kept for seven days, four weeks and six months, migrations, and a written deploy order. They decided more about how stable it runs than any feature did."
|
||||
"LEARNING_2": "A hobby project still needs the boring parts: daily database dumps kept for seven days, four weeks and six months, migrations, and a written deploy order. They decided more about how stable it runs than any feature did.",
|
||||
"DECISION_1": "A key pair instead of a shared secret. The first version signed the tokens with one HMAC secret that Auth and Core both knew. Now Auth holds the private Ed25519 key and publishes the public one on a JWKS endpoint. Core can check a login but never issue one, and when a token carries an unknown key id, Core fetches the new key itself.",
|
||||
"DECISION_2": "Mail through an outbox table instead of sending over SMTP inline. Every mail is written as a row in mail_outbox inside the transaction of its event, and a job sends the rows every minute. So a registration never fails because of the mail server, and no mail is lost when Auth restarts. With one Auth instance, at-least-once is enough.",
|
||||
"DECISION_3": "Installable, but no service worker. The app has a web app manifest and installs on phone and desktop, but no service worker, so a deploy never meets an old cached bundle. The downside is that there is no offline mode.",
|
||||
"RESULT": "Today Auth, Core and the frontend run as their own containers next to this site, with one PostgreSQL instance with two databases, a nightly sync and daily dumps. Anyone with a confirmed e-mail can register; accounts are approved by hand. I use it every day: what I play next now comes from a list, not from scrolling through the Steam library."
|
||||
}
|
||||
},
|
||||
"IMPRINT": {
|
||||
|
||||
Reference in new issue
Block a user