docs: move what lived only in agent memory into the repo
CI / Typecheck, test, feed (push) Successful in 13s
CI / Build site (push) Successful in 7s
CI / Container image (push) Successful in 15s

Four lessons were known to one agent's private notes and to nobody else.
They now live where every agent reads: how Gitea Actions differs from
GitHub Actions, the owner's decision to refresh unreachable sources by
hand rather than relax the robots gate, how to look at the built app in
a headless browser and its traps, and one command per shell call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-13 14:38:28 +02:00
co-authored by Claude Opus 5.5
parent 2da7f20905
commit 08bbba65ff
2 changed files with 53 additions and 0 deletions
+32
View File
@@ -110,6 +110,25 @@ bun test --test-name-pattern "year-less"
BASE_PATH=/gacha-event-tracker/ bun run build
```
**Looking at the built app.** UI work is checked in a real browser, not reasoned about. Build the
shell without touching the feed — `bun run build:css && bun run build:js && bun run build:static` —
and serve it with `PORT=<port> bun run serve.ts`. Drive a headless Chrome with `puppeteer-core`
installed in a scratch directory, never in the project (`zod` is the only runtime dependency); on
the owner's machine the browser is `/usr/bin/google-chrome-stable`. Traps that have each cost a
cycle:
- **Use a fresh, distinctive port for every rebuild**, and stop your own server by its PID. The shell
is cached cache-first (PRD F14), so the same origin serves the *previous* bundle after a rebuild;
a new origin has no service worker at all. Other sessions may be serving on nearby ports — never
`pkill -f serve.ts`.
- **Reach a state through `localStorage`, then reload.** `gacha-tracker:v1:prefs` with
`onboarded: true` skips first run; `view` is `"soon"` (the list) or `"timeline"` — anything else
falls back to the default. Wait for something the view renders, not for `networkidle0`, which
returns before React mounts.
- **Read text with `textContent`.** Eyebrows are uppercased in CSS, so `innerText` returns
`TODAY'S DAILIES` and a wait for `Today's dailies` never ends.
- **Keep total capture scale at or below about 3×**; above it sticky panes rasterise blank.
**`build:js` passes `--production`, and that flag is load-bearing.** Without it Bun bundles React's
*development* build: 566 KB rather than 344 KB (164 KB vs 103 KB gzipped), every element creation
validated at runtime, and `StrictMode` double-invoking effects — which in this app means
@@ -378,6 +397,15 @@ same reason `uma.moe` was declined below — and unlike `uma.moe` we would be do
`robots.txt` was welcoming, which makes it worse, not better. The legitimate options are to run the
refresh from an address game8 will serve, or to find those games another source.
**When CI cannot reach a source but the owner's own machine can, the owner refreshes by hand** —
`bun run refresh`, then commits the snapshots as `chore(data): refresh source snapshots`. Asked on
2026-08-17 whether to add a recorded-robots-policy override for `reverse1999.fandom.com`, whose
`/robots.txt` refused a datacenter address, the owner declined the machinery and took the manual
route. So register such a source normally, write down why CI cannot fetch it, and say plainly that
refreshing it is a manual run. Do not propose header shaping, a proxy or a robots override: a
`skipped_robots` outcome is only a warning and never touches the failure streak, so the source costs
a warning line per cycle and nothing else.
**CI moved to a Gitea runner on 2026-09-16, which is the first of those two options — and its result
is not yet known.** A different address is exactly the variable this failure turns on, so the nine
game8 sources may now refresh on schedule, or may not. Nothing here should be rewritten on the
@@ -1034,6 +1062,10 @@ to an open page). Four things hold it up:
test and existence in a script run from a full checkout, as `scripts/check-changelog.ts` does.
To see what the image sees, copy the Dockerfile's `COPY` list into a scratch directory, link
`node_modules`, and run the suite there.
- **One command per shell call when someone is approving them.** A call that runs an edit, `git add`
and `git commit` chained with `&&` is one approve-or-deny decision over three actions: the edit
cannot be inspected before the commit lands, and the transcript hides the diff at the moment it is
approved. Read-only commands may share a call; a commit never rides along with anything.
- **Only ever commit your own work.** Stage the files you changed, by path, and nothing else. The
working tree may already hold edits, untracked fixtures or a half-finished experiment that someone
else — the user, or another agent — put there and has not decided about yet; `git add -A`, `git
+21
View File
@@ -228,6 +228,27 @@ gates and publish it, while `.github/workflows/ci.yml` builds and deploys to Git
only needs to build and serve, not refresh data sources; the scheduled refresh and container image
publishing run exclusively on Gitea Actions.
**Gitea Actions is GitHub-Actions-compatible, not GitHub Actions.** That compatibility makes
GitHub-shaped answers look right and then fail in specific places, so ask what *Gitea* does before
changing CI, the registry or secrets. The directory name `.github/` is not evidence of which forge
runs a workflow: Gitea reads `.gitea/workflows/` natively, and `test/refresh.test.ts` asserts on the
workflow files by path. The divergences found so far, each one paid for once:
- **The registry does not take the Actions token.** A Gitea package belongs to a user or org, not a
repository, so `secrets.GITHUB_TOKEN` carries no authority over it and `permissions: packages:
write` authorises nothing. The image push logs in with an access token with the *package* scope,
stored as `REGISTRY_TOKEN` — the comments in `.gitea/workflows/ci.yml` carry the detail.
- **`uses:` takes full URLs** (`https://github.com/actions/checkout@v4`); a bare reference resolves
against the instance's `DEFAULT_ACTIONS_URL`.
- **No `gh` CLI on the runner.** `refresh.yml` dispatches `ci.yml` through Gitea's REST API
(`${{ github.server_url }}/api/v1`) with `curl`.
- **A push made with the Actions token *does* trigger workflows**, unlike on GitHub, which suppresses
them. `refresh.yml` dispatches `ci.yml` anyway, and the per-ref concurrency group absorbs the
duplicate.
- **No Pages.** Gitea's deploy artefact is the container image, built without `BASE_PATH`; Pages is
the GitHub mirror's job alone.
- **Gitea push-mirrors to GitHub** — see `AGENTS.md` § Conventions on pushing.
### Hosting under a subpath
Assets resolve against a `<base href>` substituted at build time, the feed URL resolves against