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>
This commit is contained in:
207
CHANGELOG.md
Normal file
207
CHANGELOG.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user