feat: add Girls' Frontline 2, from IOP Wiki

The best date material this project has after wiki.gg: every row states an
exact instant on both boundaries and names the zone, so nothing is converted
and nothing is assumed. `parseIsoClockRangeUtc` requires that `(UTC)` rather
than defaulting to it — a row that ever loses it should drop out instead of
landing hours off a boundary the reader is watching in-game.

The hazard here is the Server column. CN, EN and JP rows share one table and
the Chinese schedule runs about a year ahead, which is the akwiki CN-column
problem verbatim: publish EN, skip the rest. Betas are fenced off separately,
being dated exactly like everything else and playable by nobody, and the page
is an archive back to 2023, so currency is decided against ctx.now as in
bawiki. That leaves one live event today — thin, and true.

No resetOffsets: the EN boundaries land on three different clocks, which is a
patch window rather than a reset hour.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-19 03:50:05 +02:00
co-authored by Claude Opus 5
parent c1cd1148d9
commit 4dff14087f
14 changed files with 6029 additions and 21 deletions
+31 -8
View File
@@ -37,8 +37,8 @@ A web app that aggregates live and upcoming events across popular gacha games, p
calendar, sorts them by end date or by what the reader is partway through, tracks day-by-day
progress on events that repeat daily, and lets a user mark events completed.
**Status: working app, refreshing itself on a schedule.** Schema, six parsers, fourteen sources across
thirteen games, the full interface, offline support, a static server, a Docker image and CI all exist and
**Status: working app, refreshing itself on a schedule.** Schema, seven parsers, fifteen sources across
fourteen games, the full interface, offline support, a static server, a Docker image and CI all exist and
are tested. The refresh runner (`bun run refresh`) fetches, caches raw snapshots and rebuilds the
feed; `.github/workflows/refresh.yml` runs it twice a day and commits only when a page actually
changed. The SQLite layer and the review queue are still specified in `docs/` but not built, so the
@@ -106,8 +106,9 @@ re-verify a sample against the live page afterward.
```
src/shared/ schema.ts (the contract), time.ts, daily.ts, effort.ts, games.ts, feed.ts
custom.ts — reader-authored games and events, and their key spaces
src/ingest/ html.ts, dates.ts (twelve formats), merge.ts, sanitize.ts, robots.ts, snapshots.ts
parsers/ game8.ts, wikigg.ts, akwiki.ts, fandom.ts, bawiki.ts, holodori.ts — keyed by SITE, not game
src/ingest/ html.ts, dates.ts (thirteen formats), merge.ts, sanitize.ts, robots.ts, snapshots.ts
parsers/ game8.ts, wikigg.ts, akwiki.ts, fandom.ts, bawiki.ts, holodori.ts,
iopwiki.ts — keyed by SITE, not game
adapters/ index.ts — SOURCES registry binding url+game+parser, and the sanitize seam
src/client/ React app, service worker, manifest
state/ progress, daily log, ignores, prefs, sort — all localStorage
@@ -118,7 +119,7 @@ src/client/ React app, service worker, manifest
theme.ts — dark or light, and what a game hue reads as on each
scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches)
serve.ts static server + /api/health
test/ 620 tests
test/ 641 tests
fixtures/<game>/ raw HTML + .expected.json per source — pinned, kept forever
snapshots/ current page per source, rewritten by refresh — see its README
```
@@ -164,7 +165,7 @@ These come from how gacha games actually schedule things, and they cause most bu
year, month, or end. `readColumnTable` drops a row it cannot date. An omitted event is a
recoverable disappointment; a confidently wrong date is the failure this product exists to prevent.
- **Parsers are keyed by site, not game.** One `game8` parser serves eight sources and `fandom` two;
`wikigg`, `akwiki`, `bawiki` and `holodoriwiki` serve one each — the first two share a host family
`wikigg`, `akwiki`, `bawiki`, `holodoriwiki` and `iopwiki` serve one each — the first two share a host family
and have entirely different templates, and the last two are both Miraheze wikis whose page
templates have nothing in common. Adding a source for a known site is one `SOURCES` entry; a new
site is a parser module.
@@ -242,7 +243,7 @@ Sources are community wikis. Treat them as a guest would:
- Honor `robots.txt`; set a descriptive `User-Agent` with a contact URL.
- One request per source per refresh cycle, minimum 6 hours apart.
- **Space requests to one host**, honouring its `Crawl-delay` and defaulting to 2s. Eight of the
fourteen sources are game8.co pages, so the per-source floor alone still permits one cycle to arrive
fifteen sources are game8.co pages, so the per-source floor alone still permits one cycle to arrive
as eight back-to-back requests to a single site — which is the shape an edge network throttles, and
what a burst looks like from the far end regardless of our intent.
- Send `If-None-Match` / `If-Modified-Since`; treat `304` as "skip, unchanged".
@@ -269,7 +270,8 @@ refresh from an address game8 will serve, or to find those games another source.
A source whose ToS forbids automated access does not get an adapter. Flag it and ask.
**Sources assessed and declined** (2026-08-17), so these are not re-litigated each pass:
**Sources assessed and declined** (2026-08-17, extended 2026-08-19), so these are not
re-litigated each pass:
| Source | Verdict |
|---|---|
@@ -281,6 +283,7 @@ A source whose ToS forbids automated access does not get an adapter. Flag it and
| `fategrandorder.fandom.com` | **Built** (2026-08-18), via `api.php` like Reverse: 1999 — but off `Event_List_(US)`, **not** `Event_List`, which is the Japanese server. See § Fandom below |
| `holodori.wiki` | **Built** (2026-08-18), from the rendered `/wiki/Events` page. Miraheze again, so the same call as Blue Archive; CC BY-SA 4.0, no `Content-Signal`, no `Crawl-delay` for `*` |
| `prydwen.gg`, `gametora.com` | **Cleared, unbuilt.** `User-agent: *` allows the paths we would want. prydwen sets `Crawl-delay: 10`, far below our one-per-6h |
| `iopwiki.com` | **Built** (2026-08-19), Girls' Frontline 2 — see § IOP Wiki below. `robots.txt` is two lines, `User-agent: *` and `Crawl-Delay: 20`, no `Disallow` anywhere |
`.github/ISSUE_TEMPLATE/feature_request.yml` points readers at that table by heading, so a source
request can be checked against it before anyone writes it up — the loudest feedback on the first
@@ -391,6 +394,26 @@ calendar of deadlines is for. An `Unknown` **end** is kept, though — that is `
unlike `bawiki.ts` this parser does not drop a started-but-undated row, because the heading has
already said the event is running.
**IOP Wiki: the Server column is the whole safety story.** `iopwiki.com/wiki/GFL2_Events` is the
best date material here after wiki.gg — every row states an exact instant on both boundaries *and*
names the zone (`2026-08-06 13:00 - 2026-08-26 22:59 (UTC)`), so `parseIsoClockRangeUtc` converts
nothing and both sides are `exact`. Three things about it:
- **CN, EN and JP rows share one table**, and the Chinese schedule runs about a year ahead. This is
the `akwiki` CN-column hazard verbatim and gets the same answer: publish `EN`, skip the rest. It
would be wrong by *months* on a row that otherwise looks perfect.
- **`Betas` is a section, not an event type.** Closed beta rows are dated exactly like everything
else and would parse cleanly onto a calendar of things nobody can play. Fenced on the `<h2>`.
- **The page is an archive**, 145 rows back to 2023, so inclusion is decided against `ctx.now` as in
`bawiki.ts`. The lane is therefore thin by design — one live event on a quiet week is the truth,
not a gap.
The zone requirement is deliberate: `parseIsoClockRangeUtc` refuses a row that loses its `(UTC)`
rather than assuming it, exactly as `parseSlashClockZone` does. GFL2 takes no `resetOffsets`: its EN
boundaries land on three different clocks (22:59, 08:59 and 02:59 UTC), which is a patch window
rather than a reset hour — Arknights and Reverse: 1999 each earned an override from a single
boundary their whole page agreed on.
`scripts/refresh-sources.ts` enforces all of the above in code — the 6h floor, one request, no
retries, conditional headers, per-host spacing, robots (failing closed when `robots.txt` cannot be
read). Anything that would make it fetch more often is a change to this section first.