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

208 lines
12 KiB
Markdown
Raw 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.
# 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.