diff --git a/AGENTS.md b/AGENTS.md index 993281d..46d9cbb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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, eight parsers, sixteen sources across -fifteen 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, eight parsers, seventeen sources across +sixteen 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 @@ -119,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/ 660 tests +test/ 670 tests fixtures// raw HTML + .expected.json per source — pinned, kept forever snapshots/ current page per source, rewritten by refresh — see its README ``` @@ -164,7 +164,7 @@ These come from how gacha games actually schedule things, and they cause most bu - **Skip, never guess.** Every function in `dates.ts` returns `null` rather than inferring a missing 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; +- **Parsers are keyed by site, not game.** One `game8` parser serves nine sources and `fandom` two; `wikigg`, `akwiki`, `bawiki`, `holodoriwiki`, `iopwiki` and `stellasorawiki` 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 @@ -242,9 +242,9 @@ 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 - sixteen 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 +- **Space requests to one host**, honouring its `Crawl-delay` and defaulting to 2s. Nine of the + seventeen sources are game8.co pages, so the per-source floor alone still permits one cycle to arrive + as nine 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". - Cache raw snapshots so re-parsing never re-fetches. **Iterate against fixtures, not the network.** @@ -256,9 +256,9 @@ training, and no `User-agent: *` rule applies to our paths. Keep it that way: do rate, and do not add an LLM that consumes page content. **game8.co does not answer a GitHub Actions runner** (confirmed 2026-08-17). Its edge returns -`202 Accepted` with a bot-management body to every one of the eight game8 sources, from the first +`202 Accepted` with a bot-management body to every one of the nine game8 sources, from the first scheduled cycle onward — `last confirmed: never` — while the same URLs return `200` and parse -cleanly from a normal address. So `robots.txt` permits us and the network does not, and those eight +cleanly from a normal address. So `robots.txt` permits us and the network does not, and those nine games have only ever been built from checked-in fixtures in CI. The per-host spacing above does not fix this and was not meant to: a 202 on the very first request @@ -285,6 +285,7 @@ re-litigated each pass: | `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 | | `stellasora.miraheze.org` | **Built** (2026-08-19), from the front page's `Current Banners` module and **not** `/wiki/Banner_List` — see § Stella Sora below | +| `game8.co/games/Chaos-Zero-Nightmare` | **Built** (2026-08-19). Zero parser work — the existing `game8` parser reads it. The ninth game8 source, so fixture-backed in CI from day one | `.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 @@ -610,6 +611,6 @@ to an open page). Four things hold it up: (PRD F7). `freshness()` in `src/shared/feed.ts` is the one definition: it takes the newest `lastSuccessAt` and **never `generatedAt`**, which is a build stamp that would call a fixture-backed calendar minutes old, and it treats a game as only as fresh as its *oldest* source, - so one live wiki cannot vouch for a stalled sibling. Given that eight sources cannot be fetched + so one live wiki cannot vouch for a stalled sibling. Given that nine sources cannot be fetched from CI at all (§ Scraping conduct), this disclosure is the only thing standing between a reader and a confidently stale calendar — do not let a future change source it from the build clock. diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index c769ebe..b1610c7 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -10,7 +10,7 @@ import { z } from "zod"; export const GameId = z.enum([ "genshin", "hsr", "zzz", "wuwa", "arknights", "endfield", "nte", "nikki", "p5x", "r1999", - "ba", "fgo", "holodori", "gfl2", "stellasora", + "ba", "fgo", "holodori", "gfl2", "stellasora", "czn", ]); export const EventType = z.enum([ @@ -308,7 +308,7 @@ nothing to add, which is the point worth carrying to the next game: an override the game moves nobody, while the same override added a year later re-labels every tick already logged under the old clock. Get it right at introduction or accept a migration. -Infinity Nikki, P5X and Blue Archive carry **no `resetOffsets` +Infinity Nikki, P5X, Blue Archive and Chaos Zero Nightmare carry **no `resetOffsets` entry**, so they take the regional default. That is an assumption, not a verified server map — none of those sources states one. Two of the 2026-08-19 additions are worth separating out, because they are silent for different reasons and neither is the usual one: diff --git a/docs/INGESTION.md b/docs/INGESTION.md index c3e2528..f3787e3 100644 --- a/docs/INGESTION.md +++ b/docs/INGESTION.md @@ -48,7 +48,7 @@ Consequences worth internalising: | Parser | Site | Sources using it | |---|---|---| -| `game8` | game8.co article calendars | Genshin, Star Rail, Wuthering Waves, ZZZ, Endfield, NTE, Infinity Nikki, Persona 5: The Phantom X | +| `game8` | game8.co article calendars | Genshin, Star Rail, Wuthering Waves, ZZZ, Endfield, NTE, Infinity Nikki, Persona 5: The Phantom X, Chaos Zero Nightmare | | `wikigg` | wiki.gg MediaWiki `mp-event` templates | Endfield | | `akwiki` | arknights.wiki.gg's `mrfz-wtable` "Ongoing/upcoming" table | Arknights | | `fandom` | Fandom wikis via the MediaWiki `action=parse` API — two page templates: `Event \| Time Period \| Version` wikitables, and FGO's picture-fenced `ONGOING EVENTS` blocks | Reverse: 1999, Fate/Grand Order | diff --git a/docs/PRD.md b/docs/PRD.md index 5bfb896..f1c9863 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -59,7 +59,7 @@ have a source — this table is the launch scope, not a roster to keep in sync. Adding a game must require no change to `GachaEvent` — only a `GameId` entry, its `games.ts` metadata, and a source registration. That is the test of whether the data model is right, and it has -held: the fifteen games here have cost the event schema nothing. Per-game *metadata* does occasionally +held: the sixteen games here have cost the event schema nothing. Per-game *metadata* does occasionally grow (Reverse: 1999 needed `resetHourLocal` for a 05:00 reset), which is a different file and moves no stored key. A game may have several sources; see `docs/INGESTION.md` § Three layers. @@ -90,7 +90,7 @@ default `"game"` — the board every existing reader already has). Merged, the o `endingSoonestFirst`, the same comparator behind the list's "Ending soonest" (F2), so the two views cannot mean different things by the same words and an unannounced end still sorts behind every dated one rather than claiming a place in the queue. Merged there is also no lane heading to say whose -event a bar is, and hue alone cannot answer that once fifteen games share a stack — so each bar +event a bar is, and hue alone cannot answer that once sixteen games share a stack — so each bar carries its game's short name, with the full name in its tooltip. A bar too narrow for a tag and a title both keeps the title: a chopped game name reads as a broken word, and the hue and the tooltip still answer it. diff --git a/fixtures/czn/game8-events-2026-08-19.expected.json b/fixtures/czn/game8-events-2026-08-19.expected.json new file mode 100644 index 0000000..a31af11 --- /dev/null +++ b/fixtures/czn/game8-events-2026-08-19.expected.json @@ -0,0 +1,86 @@ +[ + { + "id": "czn:following-the-foxs-footsteps:2026-05-27", + "game": "czn", + "title": "Following the Fox's Footsteps", + "type": "other", + "summary": "Check out Hilde's story by Following the Fox's Footsteps!", + "startsAt": "2026-05-27T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-06-17T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", + "sourceId": "czn-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "czn:beach-cafe-festival:2026-07-29", + "game": "czn", + "title": "Beach Cafe Festival", + "type": "other", + "summary": "Beach Cafe Festival has you collecting puzzle pieces by fulfilling seasonal missions to unlock pieces of Midsummer Night Memory photos, and earn rewards based on the number of puzzle pieces you've acquired.", + "startsAt": "2026-07-29T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-09-30T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", + "sourceId": "czn-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "czn:chasing-the-remanants-of-light:2026-07-29", + "game": "czn", + "title": "Chasing the Remanants of Light", + "type": "other", + "summary": "Login daily for a total of 14 days and earn rewards!", + "startsAt": "2026-07-29T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-09-08T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", + "sourceId": "czn-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, + { + "id": "czn:virtual-tactical-simulation-hilde:2026-07-29", + "game": "czn", + "title": "Virtual Tactical Simulation - Hilde", + "type": "other", + "summary": "Virtual Tactical Simulation for combatant Hilde will be available for use during Hilde's banner duration. Play her trial stage using fixed save data to obtain rewards while also testing the new combatant.", + "startsAt": "2026-07-29T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", + "sourceId": "czn-game8-events", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + } +] diff --git a/fixtures/czn/game8-events-2026-08-19.html b/fixtures/czn/game8-events-2026-08-19.html new file mode 100644 index 0000000..01a996b --- /dev/null +++ b/fixtures/czn/game8-events-2026-08-19.html @@ -0,0 +1,966 @@ +List of All Events | Chaos Zero Nightmare (CZN) Page|Game8 + + + + + + + + +

What can you do as a free member?

Member benefits illustration

Create your free account today and unlock all our premium features and tools to enhance your gaming experience.

Member benefits illustration

Create your free account today and save articles to your watchlist and get notified when they're updated with new information.

Member benefits illustration

Create your free account today and save your favorite games for quick access later, synced across all your devices.

Member benefits illustration

By creating a Game8 account and logging in, you'll receive instant notifications when someone replies to your posts.

Comment rating feature illustration

By creating a Game8 account and logging in, you can make use of convenient features in the comments section, such as rating and sorting comments.

Premium archive feature illustration

By creating a Game8 account and logging in, you can access Premium articles that are exclusively available to members.

Map tool feature illustration

By creating a Game8 account and logging in, the items you checked on the map tool will be saved.

Site Interface

Guest
Free Member
Article Watchlist
Game Bookmarks
Cross-device Sync
Light/Dark Theme Toggle
User Profiles
Direct Feedback
Comment Rating
Comment Notification
Premium Articles

Game Tools

Guest
Free Member
Interactive Map Access
Interactive Map Pins
Interactive Map Comments
Interactive Map Pins Cross-Device
Check List
Event Choice Checker
Deck Builder Cross-Device
Message Board Notification
Message Board Cross-Device
Build Planner
Stat Calculator
Diagnostic Tool
Weapon/Armor Wishlist
Team Builder
Character Scoring
Tier List Maker

Want more information?Learn more

+
Chaos Zero Nightmare (CZN)

List of All Events

+ + +Add to Watchlist

Chaos Zero Nightmare - All Event Top Banner
A list of all current events available in Chaos Zero Nightmare (CZN). Check out all event schedules, rewards, and how to participate!

+ + +

Upcoming Events

+ + + + +
There is currently no news about upcoming events. We will update this section as soon as we get info of any new events!
+

List of Current Events

+ + + + + + + + + + + + + +
Beach Cafe FestivalChasing the Remanants of Light
Following the Fox's FootstepsFull-Scale Offensive Season 3
Virtual Tactical Simulation - HildeVirtual Tactical Simulation - Yuki
+

Beach Cafe Festival

+
Beach Cafe Festival
+ + + + + + + + + + + + + + +
Start DateJuly 29, 2026
End DateSeptember 30, 2026
RewardsCores, Save Data backgrounds and stickers, Rei's Outfit, and Command Delegation Modules
+ +

Beach Cafe Festival has you collecting puzzle pieces by fulfilling seasonal missions to unlock pieces of Midsummer Night Memory photos, and earn rewards based on the number of puzzle pieces you've acquired.

+

Chasing the Remanants of Light

+
Chasing the Remanants of Light
+ + + + + + + + + + + + + + +
Start DateJuly 29, 2026
End DateSeptember 08, 2026
RewardsRescue Anchors, Tactical and Support Certificates, Potential Disk, Prism Lens, and Tears of God
+ +

Login daily for a total of 14 days and earn rewards!

+

Following the Fox's Footsteps

+
Following the Fox
+ + + + + + + + + + + + + + +
Start DateMay 27, 2026
End DateJune 17, 2026
RewardsCrystals](/games/Chaos-Zero-Nightmare/archives/561072), Prism Lens, Presents, and Stickers
+ +

Check out Hilde's story by Following the Fox's Footsteps!

+ +

Following the Fox's Footsteps Event Guide

+

Full-Scale Offensive Season 3

+
Full Scale Offensive Season 3
+ + + + + + + + + + + + + + +
Start Date-
End DateAugust 19, 2026
RewardsCrystals, Prism Lens, Memory Fragment and Character Growth Materials
+ +

Face bosses from previous Great Rifts to earn points and rewards based on how far you go in battle. Plan your team, select the best combatants for each boss, and use your cards efficiently within 10 turns to conquer the Full-Scale Offensive.

+

Virtual Tactical Simulation - Hilde

+
Virtual Tactical Simulation - Hilde
+ + + + + + + + + + + + + + +
Start DateJuly 29, 2026
End DateAugust 19, 2026
RewardsCrystals, Memory Fragment and Character Growth Materials
+ +

Virtual Tactical Simulation for combatant Hilde will be available for use during Hilde's banner duration. Play her trial stage using fixed save data to obtain rewards while also testing the new combatant.

+ +

Virtual Tactical Simulation Event Guide

+

Virtual Tactical Simulation - Yuki

+
Virtual Tactical Simulation - Yuki
+ + + + + + + + + + + + + + +
Start Date-
End DateAugust 12, 2026
RewardsCrystals, Memory Fragment and Character Growth Materials
+ +

Virtual Tactical Simulation for combatant Yuki will be available for use during Yuki rerun banner duration!

+ +

Virtual Tactical Simulation Event Guide

+

Chaos Zero Nightmare (CZN) Related News

+
Partial Banner - Latest News
+ +

Latest News and Updates

+

Permanent Events

+ + + + + + + + + + + + + +
Garden Cafe's Returnee Present EventSee You on the Unigram Event
Spiral Tower of ScreamsVirtual Tactical Simulation
A Story Rewritten-
+

Current Events

+ + + + + + + + + + + + + +
Following the Fox's FootstepsChasing the Remnants of Light
Full Scale OffensiveOverdrive - Simulation
Sweet Rewards After Missions-
+

Past Events

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Monster's Arena EventChaos Regular Survery
Overdrive - Memory FragmentInto the Depths of Discord
Daily Free Codex MergingBonds of Time
Rei's Gift EventOperation Firebreak Event and Rewards
SS Nightmare Verification EventLaunch Celebration Follow and RP Event
Launch Celebration Comment EventHaru Trauma Code Event
Special Daily Attendance Rewards EventGuardian of the Ark Event
SS Edenity Patrol OperationInvitation of Dreams
Lost Fairy Tale BookEmergency Rescue Protocol
+

Comment

1 Фанат Трессы Фанат Трессы 10 months

Тресса лучшая

Author

    +

    All rights reserved

    + + +
    Guide Index
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    \ No newline at end of file diff --git a/src/ingest/adapters/index.ts b/src/ingest/adapters/index.ts index 5ed7ef0..8ac5288 100644 --- a/src/ingest/adapters/index.ts +++ b/src/ingest/adapters/index.ts @@ -143,6 +143,17 @@ const SOURCES: SourceSpec[] = [ url: "https://stellasora.miraheze.org/wiki/Main_Page", parserId: "stellasorawiki", }, + { + id: "czn-game8-events", + game: "czn", + // The ninth game8 source, and the cost is worth stating plainly: game8's + // edge answers the Actions runner with a 202 and a bot-management body + // (AGENTS.md § Scraping conduct), so this lane is built from a checked-in + // fixture in CI from day one and only a manual `bun run refresh` moves it. + // `freshness()` discloses that in the footer, which is what it is for. + url: "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", + parserId: "game8", + }, ]; function toAdapter(spec: SourceSpec): Adapter { diff --git a/src/shared/games.ts b/src/shared/games.ts index 3b33149..e03bb73 100644 --- a/src/shared/games.ts +++ b/src/shared/games.ts @@ -132,6 +132,9 @@ export const GAMES: Record = { // starts, 12:59 and 19:59 ends), so there is no reset hour to read off it // either. See the fgo entry below for the same reasoning at more length. stellasora: { id: "stellasora", name: "Stella Sora", short: "Stella Sora", hue: "#2E9E9E", studio: "Yostar", dailyTasks: "Daily missions" }, + // No `resetOffsets`: the source is Game8, which states day-precision prose + // and no clock at all — the same silence p5x has, and the same answer. + czn: { id: "czn", name: "Chaos Zero Nightmare", short: "CZN", hue: "#B84A9C", studio: "Smilegate", dailyTasks: "Daily missions" }, }; export const GAME_LIST: GameMeta[] = Object.values(GAMES); diff --git a/src/shared/schema.ts b/src/shared/schema.ts index 52a6104..51b7529 100644 --- a/src/shared/schema.ts +++ b/src/shared/schema.ts @@ -16,6 +16,7 @@ export const GameId = z.enum([ "holodori", // hololive Dreams "gfl2", // Girls' Frontline 2: Exilium "stellasora", // Stella Sora + "czn", // Chaos Zero Nightmare ]); export type GameId = z.infer; diff --git a/test/adapters/game8.test.ts b/test/adapters/game8.test.ts index 64e6862..a27e35e 100644 --- a/test/adapters/game8.test.ts +++ b/test/adapters/game8.test.ts @@ -44,6 +44,7 @@ const CASES: Array<{ adapter: Adapter; fixture: string }> = [ { adapter: adapter("holodori-holodoriwiki-events"), fixture: "fixtures/holodori/holodoriwiki-events-2026-08-18" }, { adapter: adapter("gfl2-iopwiki-events"), fixture: "fixtures/gfl2/iopwiki-events-2026-08-19" }, { adapter: adapter("stellasora-stellasorawiki-events"), fixture: "fixtures/stellasora/stellasorawiki-events-2026-08-19" }, + { adapter: adapter("czn-game8-events"), fixture: "fixtures/czn/game8-events-2026-08-19" }, ]; async function runAdapter(adapter: Adapter, fixture: string) { @@ -1314,3 +1315,39 @@ describe("Stella Sora wiki", () => { ).toThrow(/redesigned/); }); }); + +describe("Chaos Zero Nightmare (game8)", () => { + const fixture = "fixtures/czn/game8-events-2026-08-19"; + const czn = adapter("czn-game8-events"); + + test("reuses the game8 parser with no new parsing code", () => { + // The whole point of this source: a ninth game8 page is a registry entry, + // a fixture and a test. If this ever stops being true the page has moved + // to a template the parser does not know. + expect(czn.parserId).toBe("game8"); + }); + + test("publishes the four datable events and skips the two without a start", async () => { + // The page carries six current events. Two print `Start Date: -`, and no + // start means no event ID — so skipping them is the rule working rather + // than a silent drop. Counted independently off the fixture: six + // `Start Date` rows, two of them `-`. + const events = await runAdapter(czn, fixture); + expect(events).toHaveLength(4); + + const titles = events.map((e) => e.title); + expect(titles).toContain("Beach Cafe Festival"); + expect(titles).toContain("Chasing the Remanants of Light"); + // Both appear on the page and neither states a start. + expect(titles).not.toContain("Full-Scale Offensive Season 3"); + expect(titles).not.toContain("Virtual Tactical Simulation - Yuki"); + }); + + test("keeps Game8's day precision rather than inventing a clock", async () => { + // Game8 prints "July 29, 2026" and no time of day anywhere on this page. + for (const e of await runAdapter(czn, fixture)) { + expect(e.startPrecision).toBe("day"); + expect(e.startsAt.endsWith("T00:00:00.000Z")).toBe(true); + } + }); +});