AGENTS.md
Rama master · réplica de lectura del repositorio original.
# AGENTS.md
Guidance for AI coding agents working in this repo. GrubDrops is a self-hosted
Twitch + Kick drops miner: a single Go daemon serving a JSON + SSE API and an
embedded SvelteKit SPA, a SQLite store, and on-demand Chrome sidecars for Kick
playback.
## Build, test, format
```bash
go build ./... # build everything
go test ./... # run all unit tests — keep green before pushing
gofmt -w . # CI has a gofmt gate that fails fast; always format Go
docker build -f deploy/Dockerfile.miner . # build the miner image
```
- The CI gofmt gate fails the build if any Go file is unformatted. Run `gofmt -w`
before every push.
- Always `go build ./...` **and** `go test ./...` after edits, including changes
authored by others before you build on them. Don't assume a branch compiles.
- **CodeQL code scanning** (GitHub default setup, no workflow file) runs on every
push to `master`. Keep it at zero open alerts. When embedding outside-controlled
data — game names, drop titles, channel names — into a chromedp `Evaluate`
script, **base64-encode it and decode in-script** (`jsB64JSON` +
`TextDecoder`); never concatenate raw/JSON values into the eval literal.
## Run locally
```bash
mkdir -p data
GRUB_MASTER_KEY=$(go run ./cmd/miner keygen) \
GRUB_DB_PATH=./data/miner.db GRUB_SECURE_COOKIES=0 \
go run ./cmd/miner
```
Build the SPA into `internal/web/dist` (Go serves a 503 "UI not built" page
without it):
```bash
npm --prefix web ci && npm --prefix web run build
```
UI dev loop (Vite proxies `/api` and `/auth` to the miner):
```bash
web/scripts/dev-miner.sh # temp DB miner on :18080, prints GRUB_DEV_DIR
GRUB_DEV_DIR=<dir> web/scripts/dev-seed.sh # admin plus sample games and accounts
GRUB_DEV_API=http://127.0.0.1:18080 npm --prefix web run dev
```
Create the admin with `POST /api/setup` (CSRF token from `GET /api/session`)
when not seeding. No env var is required (an unset `GRUB_MASTER_KEY` is
generated into `master.key` next to the DB); see the README
"Configuration" table for the rest (`GRUB_*` prefix).
## Layout
```
cmd/miner main daemon
cmd/browser-sidecar chromedp sidecar binary (Kick IVS playback) — core, not dead code
internal/platform/... per-platform backends (twitch, kick)
internal/plan pure planner: picks what each account mines next
internal/runner per-account runner (watch, mine, claim) and the pool
internal/collect runner hooks into the store (claims, minutes, scans)
internal/summary daily watch summary at local midnight
internal/dockerctl on-demand sidecar start/stop over the docker socket
internal/api JSON API + SSE (`/api/events`) for the SPA
internal/web embedded SPA build in dist/ (go:embed)
internal/store SQLite (sqlc + goose), age-encrypted sessions
internal/i18n locale JSON: Go-sent strings and the Svelte UI
web/ SvelteKit SPA source (pages in src/routes, components in src/lib)
proto/ buf config + gRPC defs for the sidecar — core, not dead code
```
## Architecture constraints (do not violate)
- **The browser sidecar stack is core, not dead code.** Do not delete
`cmd/browser-sidecar`, `internal/auth/browser`, `internal/dockerctl`, `proto/`
+ buf config, or the Twitch `BrowserBackend`. Kick watch-time (IVS playback
credit) depends on the on-demand chromedp sidecars. Cleanup passes have
wrongly flagged these as removable — leave them.
- **UI is a SvelteKit static SPA embedded in the Go binary; the Go side serves
JSON + SSE.**
## Platform gotchas
- **Twitch = Android device-code OAuth over direct HTTP.** Never send a
`Client-Integrity` header — adding it is a self-inflicted integrity wall. Do
not re-implement logged-out catalog scraping; `/drops/campaigns` while logged
out is just a login wall.
- **Twitch credit beacon is gated by `runner.DefaultTick`** (1 minute credited
per beacon), so anything above 60s under-credits. Keep it at 60s. Kick is
immune.
- **Kick detection/claims** ride a Chrome-TLS-fingerprinted HTTP client (`utls`)
with no browser; **watch-time** needs the Chrome sidecar (real IVS playback).
## sqlc
- **Never put `?` or parentheses in a `queries/*.sql` comment.** It corrupts
placeholder rewriting for later queries, producing a cryptic SQL syntax error.
## Internationalization
- Go strings go through `internal/i18n` (`i18n.T(lang, key)`); the Svelte UI
reads the same files through `t()`/`tn()`. Add every key to **all** files in
`internal/i18n/locales/` (`TestLocaleParity`; `npm --prefix web run check`
fails on unused or unequal keys).
- Tag every new user-facing feature (nav link, setting, panel) with
`<NewPill since="X.Y" />` (the minor that ships it). It shows on that minor
only, flags "new, may be rough", and drops on the next minor by itself.
- GUI text is lean and minimal: run the humanizer skill on it, then cut to the
shortest wording that says what happens ("Mining: War Thunder").
- Help text under inputs uses `$lib/components/Hint.svelte`: one short line plus
optional platform-tagged examples that fill the field. No toggles or paragraphs.
## Releasing
A commit is **not** a release. Day-to-day work lands on `master` freely.
1. **Log every change to `docs/CHANGELOG.md`** under `## [Unreleased]`
(Added / Changed / Fixed / Removed) as you commit — every bug fix, addition,
removal, and change, no matter how small. This is the complete running record.
2. **Verify before cutting a version.** Live drops are rarely available to test
against, so the gate is tiered:
- **Default gate (no live drop available):** a green build + passing unit tests
+ a green Kick WS transport canary (Heartbeat Health Checker). This is enough
to tag changes that do **not** touch accrual/claim logic (UI, settings,
watcher lifecycle, discovery, plumbing). Cover the fix with a unit test —
that's the durable proof, since most of the time a live drop can't be.
- **Accrual/claim changes:** when a change touches how watch-time accrues or how
a drop is claimed (Twitch beacon, Kick WS/IVS watch path, claim flow), confirm
against a **live drop** before tagging (Twitch and/or Kick, whichever the
change touches). If no live drop is available, hold the tag in `[Unreleased]`
until one is — do not tag accrual changes on unit tests alone.
3. Once verified: move `[Unreleased]` to the new version in `docs/CHANGELOG.md`, push
the `v*` tag (this triggers the ghcr image build), and write the patch notes
in the GitHub **Releases** tab — **cherry-pick** the user-facing highlights
from `[Unreleased]`. The release notes are a curated subset, not the whole
changelog.
A broken miner must never ship under a version tag.
Release notes format (title, emoji sections, humanized voice): see
[docs/release-notes.md](docs/release-notes.md).