diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..90c2ff5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,103 @@
+name: Bug report
+description: A wrong date, a missing event, a lost tick, or anything else the app got wrong.
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for reporting this.
+
+ **If a date looks wrong, please open the event and check its source link first.**
+ This app does not compile schedules; it rearranges what a wiki published. If the wiki
+ page says the same thing, the fix belongs there — and it is still worth telling us,
+ because we may be able to use a better source for that game.
+
+ Nothing you have marked, typed or ticked leaves your browser, so there is no account
+ for us to look up. That means the details below are all we have to go on.
+
+ - type: dropdown
+ id: area
+ attributes:
+ label: What part of the app?
+ options:
+ - A wrong date or time
+ - An event is missing
+ - An event that should not be there
+ - Dailies, streaks or the checklist
+ - The list, calendar or sorting
+ - Events I entered myself
+ - Offline, installing, or updating
+ - Export or import
+ - Something else
+ validations:
+ required: true
+
+ - type: input
+ id: event
+ attributes:
+ label: Which game and event?
+ description: The game, and the event title exactly as the app shows it.
+ placeholder: "Genshin Impact — Mutual Aid in Bloom: Into the Frostlands"
+ validations:
+ required: false
+
+ - type: textarea
+ id: what
+ attributes:
+ label: What happened, and what did you expect?
+ description: |
+ For a date problem, the useful shape is: what this app shows, what the source page
+ shows, and which one you believe.
+ placeholder: |
+ The app says it ends 24 Aug, 09:00. The Game8 page says 31 Aug.
+ validations:
+ required: true
+
+ - type: input
+ id: refreshed
+ attributes:
+ label: "\"Event data last refreshed\" line"
+ description: |
+ Copy it from the bottom of the page. Several sources cannot be fetched automatically,
+ so a stale calendar is a normal state with its own explanation — this line tells the
+ two cases apart before anyone goes looking for a parser bug.
+ placeholder: Event data last refreshed 15 Aug 2026, 04:12 — 2 days ago.
+ validations:
+ required: false
+
+ - type: input
+ id: source
+ attributes:
+ label: Source page
+ description: The wiki page the event links to, if the problem involves its data.
+ placeholder: https://game8.co/games/Genshin-Impact/archives/...
+ validations:
+ required: false
+
+ - type: input
+ id: where
+ attributes:
+ label: Where are you running it, and in what browser?
+ placeholder: stereotypicalcat.github.io/gacha-event-tracker — Firefox 141 on Android
+ validations:
+ required: false
+
+ - type: textarea
+ id: extra
+ attributes:
+ label: Anything else
+ description: |
+ Screenshots help. If you attach an export file, remember it is a record of your own
+ progress — trim it to the event in question if you would rather not share the rest.
+ validations:
+ required: false
+
+ - type: checkboxes
+ id: checks
+ attributes:
+ label: Before you post
+ options:
+ - label: I checked the event's source page, or this is not about a date.
+ required: false
+ - label: I searched the open issues for the same problem.
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..b07504a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+# Blank issues stay available on purpose. The two forms cover the reports we can
+# act on fastest, but a reader who cannot fit what they saw into either of them
+# should still be able to say so — a form that turns someone away loses the
+# report, not the noise.
+blank_issues_enabled: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..8b42eff
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,68 @@
+name: Feature request
+description: Ask for a game, a source, or something the app should do.
+labels: ["enhancement"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Ideas are welcome. Three things are settled, though, so you know before you type:
+
+ - **No accounts, no logins, no server-side records.** Everything you mark stays in your
+ browser. "Sync across my devices" is answered by exporting a file and importing it on
+ the other device, not by an account.
+ - **No LLM reads any page.** Every date comes from deterministic parsers. A source that
+ cannot be parsed reliably gets no adapter, rather than a model guessing at it.
+ - **No date is ever invented.** An event whose end was not announced shows no end date.
+ That is the intended behaviour, not a gap to fill in.
+
+ **Asking for a game?** Include the page you would want it read from. Some sources have
+ already been assessed and declined — a wiki that forbids automated access, hides its
+ data behind a bot check, or only archives finished events cannot be used. The current
+ list is in `AGENTS.md` under "Sources assessed and declined", so you can check before
+ writing it up.
+
+ - type: dropdown
+ id: kind
+ attributes:
+ label: What kind of request is this?
+ options:
+ - A game the app does not cover
+ - A better or extra source for a game it does cover
+ - The list, calendar or sorting
+ - Dailies, streaks or the checklist
+ - Events I enter myself
+ - Offline, installing, or updating
+ - Export or import
+ - Something else
+ validations:
+ required: true
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: What are you trying to do?
+ description: |
+ The situation, rather than the solution. Knowing what you were doing when the app got
+ in your way often turns up a smaller fix than the one either of us would have guessed.
+ placeholder: |
+ I play six games and only two of them are on here, so I still end up opening tabs.
+ validations:
+ required: true
+
+ - type: textarea
+ id: idea
+ attributes:
+ label: What would you like it to do?
+ validations:
+ required: false
+
+ - type: input
+ id: source
+ attributes:
+ label: Source page
+ description: |
+ For a game or source request: the page that lists that game's events, with real dates
+ on it. A page whose newest event has already finished cannot be used.
+ placeholder: https://bluearchive.wiki/wiki/Events
+ validations:
+ required: false
diff --git a/AGENTS.md b/AGENTS.md
index a2eeb94..10a8522 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -114,7 +114,7 @@ src/client/ React app, service worker, manifest
lens.ts — who sees which rows (focus, outstanding, next-to-expire); pure
scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches)
serve.ts static server + /api/health
-test/ 480 tests
+test/ 494 tests
fixtures/
+ Something wrong, missing, or worth adding?{" "} + + Report a problem + {" "} + or{" "} + + request a feature + + {"."} +
); } diff --git a/test/issue-templates.test.tsx b/test/issue-templates.test.tsx new file mode 100644 index 0000000..065e715 --- /dev/null +++ b/test/issue-templates.test.tsx @@ -0,0 +1,174 @@ +import { describe, expect, test } from "bun:test"; +import { renderToStaticMarkup } from "react-dom/server"; +import { + BUG_URL, + Colophon, + FEATURE_URL, + bugReportUrl, +} from "../src/client/components/Colophon.tsx"; + +/** + * The issue forms, and the app's links into them. + * + * These pin a seam nothing else can see: GitHub answers a malformed form with a + * rendered error only a visitor notices, and it drops a prefill parameter that + * names no field with no error at all. Both failures look exactly like a working + * link from here, so the only place they can be caught is a test that reads the + * YAML the links point at. + * + * Reads repo files, never build output — see AGENTS.md § Commands. + */ + +const DIR = ".github/ISSUE_TEMPLATE"; + +type Field = { + type: string; + id?: string; + attributes?: { label?: string }; + validations?: { required?: boolean }; +}; +type Form = { name?: string; description?: string; labels?: string[]; body?: Field[] }; + +async function form(file: string): Promise