From 329cea7bb372dd745289713ac8ac68cf01bcfc3b Mon Sep 17 00:00:00 2001 From: YuYu1015 Date: Fri, 28 Aug 2026 22:33:50 +0800 Subject: [PATCH] fix(bug-tracker): follow the tracker to its v1 endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix(zh-Hant): 修正已回報錯誤讀不到內容,並改由伺服器過濾,清單不再被其他產品的討論串佔位 Fix(en-US): fix the reported-bugs list failing to load, and let the server filter it so other products' threads no longer take up slots --- lib/features/bug_tracker/data/bug_api.dart | 14 ++++++- .../bug_tracker/data/bug_repository_impl.dart | 40 ++++++++----------- .../bug_tracker/bug_repository_test.dart | 18 +++++---- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/lib/features/bug_tracker/data/bug_api.dart b/lib/features/bug_tracker/data/bug_api.dart index 8da3ec27d..37212b53a 100644 --- a/lib/features/bug_tracker/data/bug_api.dart +++ b/lib/features/bug_tracker/data/bug_api.dart @@ -3,6 +3,11 @@ library; import 'package:dpip/core/network/api_client.dart'; +/// The forum tag that marks a thread as about THIS app. A routing marker, not +/// a category — it is filtered on, both by the server and again here, and +/// never rendered. +const String appBugTag = 'dpip'; + /// Reads the reported-bug threads from the tracker host. /// /// Absolute URL on purpose: `bamboo.exptech.dev` is a single host outside the @@ -14,13 +19,18 @@ class BugApi { final ApiClient _client; - static const String _base = 'https://bamboo.exptech.dev/api/dc/bug'; + static const String _base = 'https://bamboo.exptech.dev/api/v1/dc/bug'; /// Every thread in the index, capped at 50 so the payload stays bounded as /// the tracker grows. The query rides the URL, so the ETag store keys it as /// its own resource. + /// + /// `tag` asks the server for this app's threads only. It matters for the cap + /// as much as for correctness: the tracker is shared with other products, so + /// an unfiltered page of 50 spends some of its rows on threads this app then + /// throws away — measured against the live index, 3 of 50. Future list() => - _client.getAbsolute(_base, query: const {'limit': 50}); + _client.getAbsolute(_base, query: const {'limit': 50, 'tag': appBugTag}); /// One thread with its replies. Future thread(int id) => _client.getAbsolute('$_base/$id'); diff --git a/lib/features/bug_tracker/data/bug_repository_impl.dart b/lib/features/bug_tracker/data/bug_repository_impl.dart index 8a6a1c96b..3b78d3ec0 100644 --- a/lib/features/bug_tracker/data/bug_repository_impl.dart +++ b/lib/features/bug_tracker/data/bug_repository_impl.dart @@ -9,13 +9,6 @@ import 'package:dpip/features/bug_tracker/domain/bug_thread.dart'; import 'package:dpip/features/bug_tracker/domain/bug_repository.dart'; import 'package:flutter/foundation.dart'; -/// The forum tag that marks a thread as about THIS app. A routing marker, not -/// a category — it is filtered on and never rendered. -/// -/// In canonical form: see [canonicalBugTag] for why the two endpoints disagree -/// about how they spell it. -const String appBugTag = 'dpip'; - class BugRepositoryImpl implements BugRepository { const BugRepositoryImpl(this._api); @@ -44,22 +37,20 @@ String _normalise(String body) => body.replaceAllMapped( (match) => match.group(1) ?? match.input, ); -/// The two tracker endpoints spell the same tag two different ways. +/// A tag as the app matches it: the forum's slug, lower-cased. /// -/// The index sends the forum's slugs — `["dpip", "bug"]`. The detail endpoint -/// still reflects Discord's raw forum labels, which are bilingual — `["臭蟲 -/// bug", "DPIP", "已解決 fixed"]`. Left alone, one thread carries `bug` in the -/// list and `臭蟲` on its own page, and the routing marker matches `dpip` in -/// one place and `DPIP` in the other — which drops every thread from the -/// index, because nothing equals `DPIP` once the server started sending slugs. +/// Both endpoints now send slugs — `["bug", "dpip", "fixed"]` — verified +/// across the index and ten detail replies covering every tag in the +/// vocabulary. They did not always agree: the detail endpoint used to reflect +/// Discord's raw bilingual labels (`臭蟲 bug`, `DPIP`, `已解決 fixed`), so this +/// took a label's English tail to reconcile the two. That split is gone with +/// the behaviour it compensated for; a tag with a space in it is now a tag +/// with a space in it. /// -/// The slug is the canonical form, so a bilingual label yields its English -/// tail. `臭蟲 bug` → `bug`, `DPIP` → `dpip`, `bug` → `bug`. -String canonicalBugTag(String tag) { - final trimmed = tag.trim(); - final space = trimmed.lastIndexOf(' '); - return (space < 0 ? trimmed : trimmed.substring(space + 1)).toLowerCase(); -} +/// The lower-casing stays. It is what the routing marker actually needed — +/// `DPIP` had to equal `dpip` or every thread fell out of the index — and it +/// is one comparison against a regression whose other failure mode is silent. +String canonicalBugTag(String tag) => tag.trim().toLowerCase(); List _canonicalTags(Object? raw) { if (raw is! List) return const []; @@ -145,8 +136,11 @@ List parseBugThreads(Object? body) { ]; // `dpip` is the forum's routing marker: threads without it are not about // this app (other products share the tracker), so they never reach the - // index. Locked threads are staff-side conversations — same. Matched in - // canonical form, because the marker arrives spelt both ways. + // index. `BugApi.list` already asks the server for that tag, so this is the + // backstop rather than the mechanism — a server that stops honouring the + // query would otherwise put another product's threads in this list with no + // other symptom. Locked threads are staff-side conversations, which the + // query cannot express at all. threads.removeWhere( (thread) => thread.locked || !thread.tags.map(canonicalBugTag).contains(appBugTag), diff --git a/test/features/bug_tracker/bug_repository_test.dart b/test/features/bug_tracker/bug_repository_test.dart index 2c7d43d81..eed5c1aab 100644 --- a/test/features/bug_tracker/bug_repository_test.dart +++ b/test/features/bug_tracker/bug_repository_test.dart @@ -2,10 +2,11 @@ /// live tracker's updated contract (2026-08-27): a `users` directory keyed by /// Discord snowflake plus `threads`/`msg` entries that reference it by id. /// -/// The two endpoints spell tags differently — the index sends the forum's -/// slugs, the detail endpoint still reflects Discord's bilingual labels — so -/// the fixtures deliberately use both forms and both must canonicalise the -/// same way. +/// Both endpoints send the forum's slugs, lower-case (`["bug", "dpip"]`). The +/// detail endpoint used to reflect Discord's bilingual labels instead; the +/// fixtures still put a upper-case marker through the parser, because that is +/// the spelling whose failure is silent — a routing tag that stops matching +/// empties the whole list rather than mislabelling one badge. library; import 'package:dpip/features/bug_tracker/data/bug_repository_impl.dart'; @@ -50,14 +51,14 @@ void main() { ); }); - test('the index accepts the detail endpoint\'s bilingual labels too', () { + test('a tag is matched by its slug, whatever case it arrives in', () { final threads = parseBugThreads({ 'users': {_chenId: _user(_chenId, '陳')}, 'threads': [ { 'threads_id': 1, 'title': 't', - 'tags': ['DPIP', '臭蟲 bug', '已解決 fixed'], + 'tags': ['DPIP', 'Bug', 'fixed'], 'body': 'b', 'author': _chenId, 'created_at': 1787511150, @@ -66,8 +67,9 @@ void main() { ], }); - // Canonical form is the slug, so the English tail wins and the routing - // marker matches whichever way the server spelt it. + // The marker matched despite its case (the thread survived at all), and + // the categories come through lower-cased, which is the form the badge + // table is keyed by. expect(threads.single.tags, ['bug', 'fixed']); });