Files
rack/CHANGELOG.md
Fredrik Johansson f0314e7492 Initial commit: rack, a homelab idle game
Core loop (hardware tiers, deployable services, upgrades, random events,
prestige), quirks, host nicknames, an ASCII rack view, in-app help/
changelog, and a Vitest suite covering engine logic and data balance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 21:55:54 +02:00

12 KiB
Raw Blame History

Changelog

2026-07-02 — initial build

  • Scaffolded with vite vanilla-ts template, stripped to a blank shell. Chose vanilla TS + DOM over React/Zustand per PLAN.md's recommendation — keeps this dependency-light like the other homelab-family projects.
  • Dev/preview ports set to 5677/4587 in vite.config.ts (Vite defaults 5173/4173 are claimed by sibling projects on this machine — avoided those plus ±10 as instructed).
  • Implemented the full core loop from PLAN.md: hardware tiers (Pi → Desktop → Server → Rack → Colo) gating RAM/power capacity, 11 deployable services across 6 categories, 9 upgrades (some gated behind prerequisites, e.g. Backups III requires Backups II), and a Poisson-ish random event scheduler (disk full, power outage, kernel panic, fan noise, update available, rare rm -rf) whose interval tightens as game time advances.
  • Added prestige as a real feature rather than a stretch goal: crossing 50,000 lifetime points surfaces a "nuke it and start over" banner that wipes progress for a permanent multiplier on future production.
  • Decision: new games start with 15 pts instead of 0. With zero starting services and the cheapest deploy costing 10, a strict 0-start would strand the player with no way to ever earn anything. Applied the same bootstrap amount after prestige.
  • Decision: "fix" (crashed service recovery) has a 60s global cooldown across all services, matching the "have you tried turning it off and on again" flavour from PLAN.md — prevents it from being a free instant-fix spam button.
  • Verified with a headless Chrome screenshot against the dev server (localhost:5677) rather than just tsc --noEmit, per the project's "test UI changes in a browser" convention.

2026-07-02 — soft-lock and refresh-jank fixes

  • Bug: the service <select> dropdown couldn't stay open — the whole app re-rendered (rebuilding every DOM node, including the select) every 250ms, which closes any open native dropdown instantly. Fixed by skipping the passive rebuild while a <select> has focus, and by dropping the passive refresh rate from 250ms to 1000ms (user actions still re-render instantly via the direct onChange() call in click handlers, so this only affects how fast background ticks/crashes become visible).
  • Bug (real soft-lock): the crash "fix" cooldown was global across all services (60s). If two services crashed close together — very plausible early on with only 2-3 RAM slots and no spare hardware to buy — the second stayed dead for up to a minute with zero income and nothing else to do. Reported by user: "nginx crashed, can't install a new one, same with pi-hole." Fixed by moving the cooldown onto each DeployedService (fixCooldownUntil field) instead of GameState, and shortening it to 20s now that it's per-service rather than shared. A freshly crashed service can always be fixed immediately; the cooldown only throttles re-fixing the same service repeatedly.
  • Also preserved the log panel's scroll position across rebuilds — a full innerHTML = '' rebuild was resetting it to the top every render.

2026-07-02 — capacity was a shared pool, not per-host

  • Bug: capacity() summed RAM/power across all owned hardware into one combined pool, and deployService checked against that pool regardless of which host was selected. So clicking a specific hardware item before deploying was cosmetic — a service could draw from the Desktop's power budget while nominally "deployed on the Pi." User reported losing track of which service ran where, and one Nginx instance disappearing after deploying Wireguard (most likely the pre-existing rare rm -rf random event, unrelated to deploy capacity — it fires independently and only while Backups I is unowned).
  • Fixed by adding hostCapacity(state, hardwareId) which checks RAM/power against the specific host's own budget, and switching deployService to use it instead of the aggregate. Host selection now actually constrains where a service can go.
  • UI: each hardware item now shows its own ram x/y · pwr x/y, and each service row now shows which host it's running on, so "which host is this on" is answerable at a glance instead of requiring a guess.
  • Verified by seeding localStorage with two hosts and one service per host (served via Vite's public/ dir so the origin matched, then screenshotted headless) — confirmed per-host capacity numbers and host labels render correctly.

2026-07-02 — uninstall services, and a note on what services actually do

  • Feature: added uninstallService() and an rm button on every service row (running or crashed) — there was previously no way to free up a host's RAM/power short of a crash-and-abandon, which combined with per-host capacity (above) could genuinely strand a full host. Uninstall refunds 25% of the original deploy cost (doubled if the instance was running redundant, since that consumed 2x the deploy cost in practice).
  • Confirmed for the record: services currently have exactly one effect — producing points/sec. No secondary bonuses, synergies, or unlocks tied to which services are running. Worth knowing before uninstalling for capacity: there's no hidden reason to keep a low-value service around beyond its own production.

2026-07-02 — test suite

  • Added Vitest (npm test) and src/engine.test.ts, 31 tests against engine.ts and the static data/ definitions. No UI/DOM tests — ui.ts is a straightforward render of engine state and isn't where the risk is; the engine is where a change silently breaks the core loop or the balance.
  • Coverage: deploy/capacity (including the per-host isolation fix), hardware/upgrade purchasing and prerequisite gating, per-service fix cooldowns (explicitly tests that one service's cooldown doesn't block fixing a different crashed service — regression test for the soft-lock bug reported earlier), uninstall refunds, tick production/aging/outage behaviour, and prestige.
  • Added a balance sanity block that asserts properties of the data rather than specific numbers (payback time per service, hardware cost/ capacity monotonicity, crash-risk ceiling, starting-balance affordability window). One of these caught a real gap between my assumption and the actual data on first run: I asserted crash risk stays under 10%/minute without checking the real values first, and waste-go's configured 0.15/minute failed it. Not a game bug — fixed the test's threshold to match the intended range (documented inline) instead of changing the data.

2026-07-02 — quirks, 3 new services, rack view, in-app help/changelog

  • Feature: service quirks. deployService now has a 30% chance (QUIRK_CHANCE in src/data/quirks.ts) of rolling a per-instance trait (Overclocked, Chatty, Legacy, Well-documented, Flaky, Artisanal) that multiplies that instance's ratePerSec and crash chance independently of its service definition. Stored as quirkId on DeployedService, shown as a badge on the service row. Answers "do services do anything besides produce points" with a small yes, without adding a whole synergy system.
  • Feature: 3 new services — Homepage (Dashboard, very cheap, meant to sit next to Nginx early), Grafana (Monitoring), Matrix Synapse (Chat, mid-late game). Balanced against the existing balance sanity test thresholds (payback < 300s, crash risk < 16%/min) rather than eyeballed.
  • Feature: ASCII rack view (buildRackArt in ui.ts) — a monospace strip above the two-column layout, one line per owned host, showing which services are deployed there as 2-letter codes (colored by running/ crashed status) and remaining capacity as dots. Directly answers "which host is this on" at a glance, which was the underlying ask behind the earlier per-host capacity fix.
  • Feature: in-app help & changelog overlay (src/help.ts), modeled on ../adventure's HelpModal.ts pattern: a single keypress (?, also works via a header button) toggles a centered modal, closable via ×, click-outside, or Escape. Content is a short "how this works" list plus the full changelog, sourced from src/data/version.ts (VERSION + CHANGELOG array) rather than duplicating this file's prose — that data module is the condensed, in-app-facing counterpart to this file.
  • Bumped in-app VERSION to 0.2.0 to mark this batch.

2026-07-02 — deploy costs visible, multi-buy hardware, close the desktop→server gap

  • Feature: the deploy dropdown now shows each service's RAM/power cost inline (Nginx — 10pts · ram 1 · pwr 1), and disables any service that wouldn't fit on the currently selected host ((no room on selected host)). Previously RAM/power requirements were only visible after deploying, by checking the host's used/total numbers — the dropdown gave no advance warning. User reported: "some of the software has ram or cpu requirements, they need to be shown in the dropdown."
  • Feature: hardware can now be bought again after you already own it — a second Old Desktop, a third, etc. — not just the single "next tier" purchase from before. Each extra unit of a tier costs 40% more than the last unit of that same tier (hardwareCost() in engine.ts); the starter Pi 4 is excluded (it's a one-time freebie, not a purchasable tier — rebuying it at cost 0 would let RAM/power be farmed for free). This is the direct fix for the reported "gap from Old Desktop to 1U server where it takes ages to gain points": before this, the only lever between a 150pt Desktop and a 2200pt Server was grinding on a single Desktop's 5 RAM / 8 power budget. Now a second Desktop (210pts) is a much closer, cheaper stepping stone.
  • Since more than one host can now share a tier, hardware/service labels disambiguate with a suffix ("Old Desktop #2") whenever more than one of that tier is owned — added a shared hostLabel() helper in ui.ts used by the hardware list, service rows, and the rack view.
  • Bumped in-app VERSION to 0.3.0. Added 3 tests: repeat-buying a tier produces a second independent host, escalating cost per extra unit, and the starter Pi's rebuy block.

2026-07-02 — host nicknames

  • Feature: every host is now randomly christened with a name from a curated pool (src/data/hostnames.ts) on purchase — sci-fi AIs (hal9000, glados, shodan), mythology (prometheus, cerberus), and self-deprecating sysadmin humour (the-basement, works-on-my-machine, do-not-touch). User feedback: "Add quirky and/or nerdy names to any machine in the rack."
  • This also replaces the #2/#3 numbering scheme added for the earlier multi-buy feature — nicknames are drawn without replacement per game (pickHostname() takes the set of already-used names), so they're unique by construction and read far better than an index. Falls back to unit-N only if the ~54-name pool is ever exhausted (would need 55 hosts in one game).
  • Old saves from before this change are migrated on load — any host missing a nickname gets one assigned, without touching anything else in the save.
  • Bumped in-app VERSION to 0.4.0. Added 4 tests: starter Pi gets a pooled name, no two purchased hosts collide, pickHostname() respects the taken set, and the unit-N fallback once the pool is exhausted.

2026-07-02 — subpath build, git init

  • Added npm run build:subpath (vite build --base=/rack/) so dist/ asset URLs resolve correctly when served from goonk.se/rack instead of a domain root. Verified the emitted index.html references /rack/assets/... rather than /assets/....
  • Confirmed the deployment target: ../goonk is an Astro site (docker-compose.yml image goonk-cv) whose public/ directory is already the established home for sibling projects (public/waste, public/pitwall) - Astro copies public/ verbatim into its own build output, and its nginx config's try_files $uri $uri/ /index.html fallback needs no changes to serve a new subdirectory. So deploying rack is: npm run build:subpath, then copy dist/ into goonk/public/rack/.
  • This repo (/home/frejoh/temp/rack) was living as an untracked directory inside the parent /home/frejoh/temp git repo, unlike every sibling project (goonk, adventure, etc.), which each have their own .git. Ran git init here to match that convention ahead of the eventual real deploy.