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