Each result of the IP lookup gains geo, from the new package internal/geo:
One request to geojs carries every uncached address of a lookup, 1 second in all.
On a timeout, an error or an answer with no country, city or coordinates, the lookup reads DB-IP City Lite, downloaded each month into the state directory. A new file replaces the copy only once it opens.
geojs answers stay in memory (at most 250,000, least recently used dropped first) and in a new geolocations table in cache.db, for 7 days. An answer with no location is not cached.
The home page footer carries the credit to DB-IP.
The handler passes the whole address list to the locator before its loop, so the geojs batch is one request. This adds at most 1 second per lookup.
A real lookup returned geo from geojs, and from the database with geojs unreachable.
New libraries beyond the two the issue names: github.com/hashicorp/golang-lru/v2, added to the Go package defaults file with the MMDB reader and writer.
Judgement call: a reserved address goes to neither source, and its geo is null. Removing the check in the handler reverses it.
Judgement call: updated for DB-IP is when the copy was downloaded.
Judgement call: one coordinate alone, with no country or city, counts as a location.
Deviation: the cache is SQLite, by sneak's words on #203, not the JSON file the standing rule asks for.
Disclosure: the server test uses public addresses, because a reserved address never reaches the locator; the internal/geo tests use documentation ranges.
Model: opus-5-5
Implements https://git.eeqj.de/sneak/routewatch/issues/239.
Each result of the IP lookup gains `geo`, from the new package `internal/geo`:
- One request to geojs carries every uncached address of a lookup, 1 second in all.
- On a timeout, an error or an answer with no country, city or coordinates, the lookup reads DB-IP City Lite, downloaded each month into the state directory. A new file replaces the copy only once it opens.
- geojs answers stay in memory (at most 250,000, least recently used dropped first) and in a new `geolocations` table in `cache.db`, for 7 days. An answer with no location is not cached.
- The home page footer carries the credit to DB-IP.
The handler passes the whole address list to the locator before its loop, so the geojs batch is one request. This adds at most 1 second per lookup.
A real lookup returned `geo` from geojs, and from the database with geojs unreachable.
New libraries beyond the two the issue names: `github.com/hashicorp/golang-lru/v2`, added to the Go package defaults file with the MMDB reader and writer.
Judgement call: a reserved address goes to neither source, and its `geo` is null. Removing the check in the handler reverses it.
Judgement call: `updated` for DB-IP is when the copy was downloaded.
Judgement call: one coordinate alone, with no country or city, counts as a location.
Deviation: the cache is SQLite, by sneak's words on https://git.eeqj.de/sneak/routewatch/issues/203, not the JSON file the standing rule asks for.
Disclosure: the server test uses public addresses, because a reserved address never reaches the locator; the `internal/geo` tests use documentation ranges.
Model: opus-5-5
clawbot
self-assigned this 2026-11-10 14:04:13 +01:00
For the reviewer: a conflict with next confined to TODO.md is not a finding. Rebase locally and judge the code; it is rebased after a pass.
Model: opus-5-5
For the reviewer: a conflict with `next` confined to `TODO.md` is not a finding. Rebase locally and judge the code; it is rebased after a pass.
Model: opus-5-5
internal/geo/locator.go:144 and internal/geo/geojs.go:63: the zone of an IPv6 address (the text after %) goes unescaped into the request to geojs, and a client chooses that text. /ip/2606:4700:4700::1111%25x%26callback=evil sends ?ip=2606:4700:4700::1111%x&callback=evil, and any later address of the batch lands in that added parameter. Drop the zone before making the string (addr.WithZone("")), so the request and the cache key both use the plain address. Add a test that a zoned address sends no zone text to geojs.
README.md, Geolocation, "What goes outside": "A lookup answered from the cache or from the database sends nothing." is false. The database answers only after geojs was asked, so the address has already gone out. Write "A lookup answered from the cache sends nothing."
internal/geo/database.go:119-122: result.Found() is also false when the lookup fails, so an error from a damaged copy is dropped with no log and geo is null. Check result.Err() first and log it the way the Decode failure below it is logged, and correct the comment on fromDatabase.
PR body: the Go package defaults file is named by a path containing a vendor's name, which the no-attribution policy forbids. Write "the Go package defaults file" and drop the path.
Statements about what a licence requires are not allowed in a README, comment or PR body. State the fact without the reason:
README.md, Geolocation: "DB-IP's licence requires an attribution, so the home page's footer says ..." becomes "The home page's footer says ...".
internal/templates/templates_test.go, comment on TestHomePageCreditsDBIP: "the attribution the DB-IP licence requires" becomes "the credit to DB-IP".
PR body: "the DB-IP attribution the licence requires" becomes "the credit to DB-IP".
Direct instruction: the SQLite cache follows sneak's words on #203, against the policy that small state is saved as a JSON file.
Not a finding, for sneak to decide: geojs answers with no location at all for some real addresses (1.1.1.1 today), and that empty answer is kept for 7 days although DB-IP has a city for it. The code follows the issue, which falls back only on a timeout or an error.
Model: opus-5-5
**FAIL: `needs-rework`**
1. `internal/geo/locator.go:144` and `internal/geo/geojs.go:63`: the zone of an IPv6 address (the text after `%`) goes unescaped into the request to geojs, and a client chooses that text. `/ip/2606:4700:4700::1111%25x%26callback=evil` sends `?ip=2606:4700:4700::1111%x&callback=evil`, and any later address of the batch lands in that added parameter. Drop the zone before making the string (`addr.WithZone("")`), so the request and the cache key both use the plain address. Add a test that a zoned address sends no zone text to geojs.
2. `README.md`, Geolocation, "What goes outside": "A lookup answered from the cache or from the database sends nothing." is false. The database answers only after geojs was asked, so the address has already gone out. Write "A lookup answered from the cache sends nothing."
3. `internal/geo/database.go:119-122`: `result.Found()` is also false when the lookup fails, so an error from a damaged copy is dropped with no log and `geo` is null. Check `result.Err()` first and log it the way the `Decode` failure below it is logged, and correct the comment on `fromDatabase`.
4. PR body: the Go package defaults file is named by a path containing a vendor's name, which the no-attribution policy forbids. Write "the Go package defaults file" and drop the path.
5. Statements about what a licence requires are not allowed in a README, comment or PR body. State the fact without the reason:
- `README.md`, Geolocation: "DB-IP's licence requires an attribution, so the home page's footer says ..." becomes "The home page's footer says ...".
- `internal/templates/templates_test.go`, comment on `TestHomePageCreditsDBIP`: "the attribution the DB-IP licence requires" becomes "the credit to DB-IP".
- PR body: "the DB-IP attribution the licence requires" becomes "the credit to DB-IP".
Direct instruction: the SQLite cache follows sneak's words on https://git.eeqj.de/sneak/routewatch/issues/203, against the policy that small state is saved as a JSON file.
Not a finding, for sneak to decide: geojs answers with no location at all for some real addresses (`1.1.1.1` today), and that empty answer is kept for 7 days although DB-IP has a city for it. The code follows the issue, which falls back only on a timeout or an error.
Model: opus-5-5
For the reworker: make the five changes in #249 (comment), each as it says.
Also settle that review's last point, which is decided here: a geojs answer with no location at all (no country, city or coordinates) counts as no answer. The lookup reads the database, and that empty answer is not cached. A test with a fake geojs covers it, and the README's Geolocation section says so.
Change nothing else. Rebase onto current next last, keeping every TODO.md entry.
Model: opus-5-5
For the reworker: make the five changes in https://git.eeqj.de/sneak/routewatch/pulls/249#issuecomment-174236, each as it says.
Also settle that review's last point, which is decided here: a geojs answer with no location at all (no country, city or coordinates) counts as no answer. The lookup reads the database, and that empty answer is not cached. A test with a fake geojs covers it, and the README's Geolocation section says so.
Change nothing else. Rebase onto current `next` last, keeping every `TODO.md` entry.
Model: opus-5-5
plainAddress in internal/geo/locator.go drops the zone for the request and the cache key; TestLookupSendsNoZoneToGeoJS covers it.
Fixed in README.md.
fromDatabase logs result.Err() and its comment is corrected; TestFailedDatabaseLookupIsLogged is added, as AGENTS.md asks of a bug fix.
Fixed in the PR body.
Fixed in all three places.
Decided point: askGeoJS leaves out an answer with no country, city or coordinates, so the lookup reads the database and caches nothing. A new row of TestLookupFallsBackToTheDatabase covers it, TestAnyCountryCityOrCoordinateIsAnAnswer covers what still counts, and the README's Geolocation section says so.
Rebased onto next, with every TODO.md entry kept.
Test fixture changed: the second entry of geojsTwoAddresses now has a country, because without one it would count as no answer.
PR body: the judgement call that an empty answer is still an answer is gone; a new one says one coordinate alone counts as a location.
Model: opus-5-5
For https://git.eeqj.de/sneak/routewatch/pulls/249#issuecomment-174247, at `c8ae355`:
1. `plainAddress` in `internal/geo/locator.go` drops the zone for the request and the cache key; `TestLookupSendsNoZoneToGeoJS` covers it.
2. Fixed in `README.md`.
3. `fromDatabase` logs `result.Err()` and its comment is corrected; `TestFailedDatabaseLookupIsLogged` is added, as `AGENTS.md` asks of a bug fix.
4. Fixed in the PR body.
5. Fixed in all three places.
6. Decided point: `askGeoJS` leaves out an answer with no country, city or coordinates, so the lookup reads the database and caches nothing. A new row of `TestLookupFallsBackToTheDatabase` covers it, `TestAnyCountryCityOrCoordinateIsAnAnswer` covers what still counts, and the README's Geolocation section says so.
7. Rebased onto `next`, with every `TODO.md` entry kept.
Test fixture changed: the second entry of `geojsTwoAddresses` now has a country, because without one it would count as no answer.
PR body: the judgement call that an empty answer is still an answer is gone; a new one says one coordinate alone counts as a location.
Model: opus-5-5
README.md, Geolocation: "That takes about 75 MB of the Go heap." is too low. At 250,000 addresses the cache takes about 100 MB when answers have every field, and about 90 MB when they have no city or region. Write "That takes about 100 MB of the Go heap." Nothing else needs to change.
Direct instruction: the SQLite cache follows sneak's words on #203, against the policy that small state is saved as a JSON file.
Judgement call: the PR body's 265 words are taken as within the limit of about 250.
Model: opus-5-5
**FAIL: `needs-rework`**
1. `README.md`, Geolocation: "That takes about 75 MB of the Go heap." is too low. At 250,000 addresses the cache takes about 100 MB when answers have every field, and about 90 MB when they have no city or region. Write "That takes about 100 MB of the Go heap." Nothing else needs to change.
Direct instruction: the SQLite cache follows sneak's words on https://git.eeqj.de/sneak/routewatch/issues/203, against the policy that small state is saved as a JSON file.
Judgement call: the PR body's 265 words are taken as within the limit of about 250.
Model: opus-5-5
For the reworker: make the one change in #249 (comment) and nothing else. Rebase onto current next if it has moved.
Model: opus-5-5
For the reworker: make the one change in https://git.eeqj.de/sneak/routewatch/pulls/249#issuecomment-174274 and nothing else. Rebase onto current `next` if it has moved.
Model: opus-5-5
For https://git.eeqj.de/sneak/routewatch/pulls/249#issuecomment-174276, at `dfc3945`:
1. Fixed in `README.md`.
2. `next` has not moved, so there was no rebase.
Model: opus-5-5
PASS: this meets the definition of done on #239, and its SQLite cache follows sneak's words on #203, against the policy that small state is saved as a JSON file.
Model: opus-5-5
**PASS**: this meets the definition of done on https://git.eeqj.de/sneak/routewatch/issues/239, and its SQLite cache follows sneak's words on https://git.eeqj.de/sneak/routewatch/issues/203, against the policy that small state is saved as a JSON file.
Model: opus-5-5
For the reworker: the review passed. Rebase onto current next, keeping every TODO.md entry and this PR's first, re-run make check, push, and change nothing else.
Model: opus-5-5
For the reworker: the review passed. Rebase onto current `next`, keeping every `TODO.md` entry and this PR's first, re-run `make check`, push, and change nothing else.
Model: opus-5-5
Each result gains `geo`. One request to geojs carries every address, 1 second in all. On a timeout, an error or an answer with no country, city or coordinates, the lookup reads DB-IP City Lite, downloaded monthly into the state directory. geojs answers stay in memory (250,000, least recently used dropped) and in a new `cache.db` table for 7 days.
Judgement call: a reserved address goes to neither source, and its `geo` is null.
Judgement call: `updated` for DB-IP is when the copy was downloaded.
Deviation: the cache is SQLite, by sneak's words.
Model: opus-5-5
Rebased onto next at 2c3f6f7. The only conflict was in TODO.md: every entry is kept, this PR's first.
Pushed, with nothing else changed.
Model: opus-5-5
For https://git.eeqj.de/sneak/routewatch/pulls/249#issuecomment-174326, at `fe392db`:
1. Rebased onto `next` at `2c3f6f7`. The only conflict was in `TODO.md`: every entry is kept, this PR's first.
2. Pushed, with nothing else changed.
Model: opus-5-5
clawbot
merged commit cca402c73d into next2026-11-10 15:48:12 +01:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements #239.
Each result of the IP lookup gains
geo, from the new packageinternal/geo:geolocationstable incache.db, for 7 days. An answer with no location is not cached.The handler passes the whole address list to the locator before its loop, so the geojs batch is one request. This adds at most 1 second per lookup.
A real lookup returned
geofrom geojs, and from the database with geojs unreachable.New libraries beyond the two the issue names:
github.com/hashicorp/golang-lru/v2, added to the Go package defaults file with the MMDB reader and writer.Judgement call: a reserved address goes to neither source, and its
geois null. Removing the check in the handler reverses it.Judgement call:
updatedfor DB-IP is when the copy was downloaded.Judgement call: one coordinate alone, with no country or city, counts as a location.
Deviation: the cache is SQLite, by sneak's words on #203, not the JSON file the standing rule asks for.
Disclosure: the server test uses public addresses, because a reserved address never reaches the locator; the
internal/geotests use documentation ranges.Model: opus-5-5
For the reviewer: a conflict with
nextconfined toTODO.mdis not a finding. Rebase locally and judge the code; it is rebased after a pass.Model: opus-5-5
FAIL:
needs-reworkinternal/geo/locator.go:144andinternal/geo/geojs.go:63: the zone of an IPv6 address (the text after%) goes unescaped into the request to geojs, and a client chooses that text./ip/2606:4700:4700::1111%25x%26callback=evilsends?ip=2606:4700:4700::1111%x&callback=evil, and any later address of the batch lands in that added parameter. Drop the zone before making the string (addr.WithZone("")), so the request and the cache key both use the plain address. Add a test that a zoned address sends no zone text to geojs.README.md, Geolocation, "What goes outside": "A lookup answered from the cache or from the database sends nothing." is false. The database answers only after geojs was asked, so the address has already gone out. Write "A lookup answered from the cache sends nothing."internal/geo/database.go:119-122:result.Found()is also false when the lookup fails, so an error from a damaged copy is dropped with no log andgeois null. Checkresult.Err()first and log it the way theDecodefailure below it is logged, and correct the comment onfromDatabase.PR body: the Go package defaults file is named by a path containing a vendor's name, which the no-attribution policy forbids. Write "the Go package defaults file" and drop the path.
Statements about what a licence requires are not allowed in a README, comment or PR body. State the fact without the reason:
README.md, Geolocation: "DB-IP's licence requires an attribution, so the home page's footer says ..." becomes "The home page's footer says ...".internal/templates/templates_test.go, comment onTestHomePageCreditsDBIP: "the attribution the DB-IP licence requires" becomes "the credit to DB-IP".Direct instruction: the SQLite cache follows sneak's words on #203, against the policy that small state is saved as a JSON file.
Not a finding, for sneak to decide: geojs answers with no location at all for some real addresses (
1.1.1.1today), and that empty answer is kept for 7 days although DB-IP has a city for it. The code follows the issue, which falls back only on a timeout or an error.Model: opus-5-5
For the reworker: make the five changes in #249 (comment), each as it says.
Also settle that review's last point, which is decided here: a geojs answer with no location at all (no country, city or coordinates) counts as no answer. The lookup reads the database, and that empty answer is not cached. A test with a fake geojs covers it, and the README's Geolocation section says so.
Change nothing else. Rebase onto current
nextlast, keeping everyTODO.mdentry.Model: opus-5-5
6e903d511ftoc5bd4a63d0c5bd4a63d0toc8ae355787For #249 (comment), at
c8ae355:plainAddressininternal/geo/locator.godrops the zone for the request and the cache key;TestLookupSendsNoZoneToGeoJScovers it.README.md.fromDatabaselogsresult.Err()and its comment is corrected;TestFailedDatabaseLookupIsLoggedis added, asAGENTS.mdasks of a bug fix.askGeoJSleaves out an answer with no country, city or coordinates, so the lookup reads the database and caches nothing. A new row ofTestLookupFallsBackToTheDatabasecovers it,TestAnyCountryCityOrCoordinateIsAnAnswercovers what still counts, and the README's Geolocation section says so.next, with everyTODO.mdentry kept.Test fixture changed: the second entry of
geojsTwoAddressesnow has a country, because without one it would count as no answer.PR body: the judgement call that an empty answer is still an answer is gone; a new one says one coordinate alone counts as a location.
Model: opus-5-5
FAIL:
needs-reworkREADME.md, Geolocation: "That takes about 75 MB of the Go heap." is too low. At 250,000 addresses the cache takes about 100 MB when answers have every field, and about 90 MB when they have no city or region. Write "That takes about 100 MB of the Go heap." Nothing else needs to change.Direct instruction: the SQLite cache follows sneak's words on #203, against the policy that small state is saved as a JSON file.
Judgement call: the PR body's 265 words are taken as within the limit of about 250.
Model: opus-5-5
For the reworker: make the one change in #249 (comment) and nothing else. Rebase onto current
nextif it has moved.Model: opus-5-5
c8ae355787todfc3945bc0For #249 (comment), at
dfc3945:README.md.nexthas not moved, so there was no rebase.Model: opus-5-5
PASS: this meets the definition of done on #239, and its SQLite cache follows sneak's words on #203, against the policy that small state is saved as a JSON file.
Model: opus-5-5
For the reworker: the review passed. Rebase onto current
next, keeping everyTODO.mdentry and this PR's first, re-runmake check, push, and change nothing else.Model: opus-5-5
dfc3945bc0tofe392db4fcFor #249 (comment), at
fe392db:nextat2c3f6f7. The only conflict was inTODO.md: every entry is kept, this PR's first.Model: opus-5-5