@mhosaic/feedback 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -153,10 +153,14 @@ function createApiClient(options) {
153
153
  if (response.status === 404) return [];
154
154
  return readJsonArray(response, "listChangelog");
155
155
  }
156
- async function getReport(reportId, externalId) {
156
+ async function getReport(reportId, externalId, opts) {
157
157
  const response = await fetcher(
158
158
  `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
159
- { method: "GET", headers: widgetHeaders(externalId) }
159
+ {
160
+ method: "GET",
161
+ headers: widgetHeaders(externalId),
162
+ ...opts?.signal ? { signal: opts.signal } : {}
163
+ }
160
164
  );
161
165
  return readJsonObject(response, "getReport");
162
166
  }
@@ -268,10 +272,14 @@ function createApiClient(options) {
268
272
  const qs = params.toString();
269
273
  return qs ? `?${qs}` : "";
270
274
  }
271
- async function listBoard(externalId, filters) {
275
+ async function listBoard(externalId, filters, opts) {
272
276
  const response = await fetcher(
273
277
  `${endpoint}/api/feedback/v1/reports/widget/board/${boardQueryString(filters)}`,
274
- { method: "GET", headers: widgetHeaders(externalId) }
278
+ {
279
+ method: "GET",
280
+ headers: widgetHeaders(externalId),
281
+ ...opts?.signal ? { signal: opts.signal } : {}
282
+ }
275
283
  );
276
284
  if (response.status === 404) {
277
285
  return { count: 0, next: null, previous: null, results: [] };
@@ -282,10 +290,14 @@ function createApiClient(options) {
282
290
  }
283
291
  return page;
284
292
  }
285
- async function fetchBoardKpis(externalId, filters) {
293
+ async function fetchBoardKpis(externalId, filters, opts) {
286
294
  const response = await fetcher(
287
295
  `${endpoint}/api/feedback/v1/reports/widget/board/kpis/${boardQueryString(filters)}`,
288
- { method: "GET", headers: widgetHeaders(externalId) }
296
+ {
297
+ method: "GET",
298
+ headers: widgetHeaders(externalId),
299
+ ...opts?.signal ? { signal: opts.signal } : {}
300
+ }
289
301
  );
290
302
  if (response.status === 404) {
291
303
  return { total: 0, by_status: {}, resolution_rate: 0, scope: "mine" };
@@ -705,6 +717,7 @@ var DEFAULT_STRINGS = {
705
717
  "board.retry": "Try again",
706
718
  "board.list.you": "you",
707
719
  "board.list.count": "{n} of {total}",
720
+ "board.list.loadMore": "Show more",
708
721
  "board.detail.empty": "Pick a report on the left to see its full thread.",
709
722
  "board.detail.by": "By",
710
723
  "board.detail.confirm_resolution": "Confirm resolution",
@@ -891,6 +904,7 @@ var FRENCH_STRINGS = {
891
904
  "board.retry": "R\xE9essayer",
892
905
  "board.list.you": "vous",
893
906
  "board.list.count": "{n} sur {total}",
907
+ "board.list.loadMore": "Afficher plus",
894
908
  "board.detail.empty": "S\xE9lectionnez un rapport \xE0 gauche pour voir le fil complet.",
895
909
  "board.detail.by": "Par",
896
910
  "board.detail.confirm_resolution": "Confirmer la r\xE9solution",
@@ -1038,6 +1052,26 @@ function onLocationChange(cb) {
1038
1052
  // src/widget/BoardView.tsx
1039
1053
  import { useEffect as useEffect2, useMemo, useState as useState2 } from "preact/hooks";
1040
1054
 
1055
+ // src/widget/boardCache.ts
1056
+ var TTL_MS = 6e4;
1057
+ var MAX_ENTRIES = 50;
1058
+ var cache = /* @__PURE__ */ new Map();
1059
+ function boardCacheKey(externalId, fetchKey) {
1060
+ return `${externalId}|${fetchKey}`;
1061
+ }
1062
+ function readBoardCache(key) {
1063
+ const hit = cache.get(key);
1064
+ return hit && Date.now() - hit.at < TTL_MS ? hit : null;
1065
+ }
1066
+ function writeBoardCache(key, page, kpis) {
1067
+ if (!cache.has(key) && cache.size >= MAX_ENTRIES) {
1068
+ const oldest = cache.keys().next().value;
1069
+ if (oldest !== void 0) cache.delete(oldest);
1070
+ }
1071
+ cache.delete(key);
1072
+ cache.set(key, { page, kpis, at: Date.now() });
1073
+ }
1074
+
1041
1075
  // src/widget/boardViewStore.ts
1042
1076
  var PREFIX = "mhosaic.boardView.";
1043
1077
  function loadBoardView(externalId) {
@@ -1146,7 +1180,7 @@ function linkifySeq(text, onOpenSeq) {
1146
1180
 
1147
1181
  // src/widget/CommentBubble.tsx
1148
1182
  import { jsx, jsxs as jsxs2 } from "preact/jsx-runtime";
1149
- function CommentBubble({ comment, strings, onOpenSeq }) {
1183
+ function CommentBubble({ comment, strings, onOpenSeq, stableUrl }) {
1150
1184
  const isMine = comment.is_mine;
1151
1185
  const isAgent = !isMine && comment.author_source === "mcp";
1152
1186
  const isSystem = !isMine && comment.author_source === "system";
@@ -1157,17 +1191,20 @@ function CommentBubble({ comment, strings, onOpenSeq }) {
1157
1191
  return /* @__PURE__ */ jsxs2("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1158
1192
  !isMine && label && /* @__PURE__ */ jsx("div", { class: `comment-author comment-author--${variant}`, children: label }),
1159
1193
  comment.body && /* @__PURE__ */ jsx("div", { class: "comment-body", children: linkifySeq(comment.body, onOpenSeq) }),
1160
- images.length > 0 && /* @__PURE__ */ jsx("div", { class: "comment-attachments", children: images.map((a) => /* @__PURE__ */ jsx(
1161
- "a",
1162
- {
1163
- class: "comment-attachment",
1164
- href: a.url,
1165
- target: "_blank",
1166
- rel: "noopener noreferrer",
1167
- children: /* @__PURE__ */ jsx("img", { src: a.url, alt: strings["detail.attachment_alt"], loading: "lazy" })
1168
- },
1169
- a.id
1170
- )) }),
1194
+ images.length > 0 && /* @__PURE__ */ jsx("div", { class: "comment-attachments", children: images.map((a) => {
1195
+ const src = stableUrl ? stableUrl(`att:${a.id}`, a.url) : a.url;
1196
+ return /* @__PURE__ */ jsx(
1197
+ "a",
1198
+ {
1199
+ class: "comment-attachment",
1200
+ href: src,
1201
+ target: "_blank",
1202
+ rel: "noopener noreferrer",
1203
+ children: /* @__PURE__ */ jsx("img", { src, alt: strings["detail.attachment_alt"], loading: "lazy" })
1204
+ },
1205
+ a.id
1206
+ );
1207
+ }) }),
1171
1208
  /* @__PURE__ */ jsx("div", { class: "comment-time", children: formatTime(comment.created_at) })
1172
1209
  ] });
1173
1210
  }
@@ -1234,7 +1271,8 @@ function ReportDetailView({
1234
1271
  canModerate = true,
1235
1272
  variant = "modal",
1236
1273
  onOpenSeq,
1237
- buildPermalink
1274
+ buildPermalink,
1275
+ seed
1238
1276
  }) {
1239
1277
  const [detail, setDetail] = useState(null);
1240
1278
  const [error, setError] = useState(null);
@@ -1251,6 +1289,20 @@ function ReportDetailView({
1251
1289
  const [attachError, setAttachError] = useState("");
1252
1290
  const fileInputRef = useRef(null);
1253
1291
  const mountedRef = useRef(true);
1292
+ const urlPins = useRef(/* @__PURE__ */ new Map());
1293
+ const [, bumpPinTick] = useState(0);
1294
+ const PIN_MAX_AGE_MS = 45 * 6e4;
1295
+ const pinUrl = (key, url) => {
1296
+ if (!url) return url;
1297
+ const pin = urlPins.current.get(key);
1298
+ if (pin && Date.now() - pin.at < PIN_MAX_AGE_MS) return pin.url;
1299
+ urlPins.current.set(key, { url, at: Date.now() });
1300
+ return url;
1301
+ };
1302
+ const unpinUrl = (key) => {
1303
+ urlPins.current.delete(key);
1304
+ bumpPinTick((t) => t + 1);
1305
+ };
1254
1306
  const shotsRef = useRef(composeShots);
1255
1307
  shotsRef.current = composeShots;
1256
1308
  useEffect(() => {
@@ -1421,6 +1473,39 @@ function ReportDetailView({
1421
1473
  }
1422
1474
  };
1423
1475
  if (!detail && !error) {
1476
+ if (seed) {
1477
+ const seedPill = /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${seed.status}`, children: strings[`status.${seed.status}`] ?? seed.status });
1478
+ return /* @__PURE__ */ jsxs3("div", { class: `report-detail variant-${variant} is-seeded`, children: [
1479
+ variant === "modal" && /* @__PURE__ */ jsxs3("div", { class: "report-detail-header", children: [
1480
+ /* @__PURE__ */ jsxs3("button", { type: "button", class: "btn", onClick: onBack, children: [
1481
+ "\u2190 ",
1482
+ strings["detail.back"]
1483
+ ] }),
1484
+ seedPill
1485
+ ] }),
1486
+ variant === "board" && /* @__PURE__ */ jsxs3("div", { class: "report-detail-header report-detail-header--board", children: [
1487
+ /* @__PURE__ */ jsxs3(
1488
+ "button",
1489
+ {
1490
+ type: "button",
1491
+ class: "btn btn--ghost report-detail-back",
1492
+ onClick: onBack,
1493
+ "aria-label": strings["board.back"],
1494
+ children: [
1495
+ "\u2190 ",
1496
+ strings["board.back"]
1497
+ ]
1498
+ }
1499
+ ),
1500
+ seedPill
1501
+ ] }),
1502
+ /* @__PURE__ */ jsxs3("div", { class: "report-detail-body", children: [
1503
+ /* @__PURE__ */ jsx2("div", { class: "report-detail-description-row", children: /* @__PURE__ */ jsx2("p", { class: "report-detail-description", children: linkifySeq(seed.description, onOpenSeq) }) }),
1504
+ /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1505
+ /* @__PURE__ */ jsx2("div", { class: "mine-loading", children: strings["mine.loading"] })
1506
+ ] })
1507
+ ] });
1508
+ }
1424
1509
  return /* @__PURE__ */ jsx2("div", { class: "mine-loading", children: strings["mine.loading"] });
1425
1510
  }
1426
1511
  if (!detail) {
@@ -1530,7 +1615,7 @@ function ReportDetailView({
1530
1615
  children: copied ? strings["detail.copied"] : strings["detail.copy_link"]
1531
1616
  }
1532
1617
  ) }),
1533
- (detail.screenshots?.length ? detail.screenshots.map((s) => s.url) : [detail.screenshot_url]).filter((url) => Boolean(url)).map((url) => {
1618
+ (detail.screenshots?.length ? detail.screenshots.map((s) => s.url) : [detail.screenshot_url]).map((url, i) => ({ url: pinUrl(`shot:${i}`, url ?? null), pinKey: `shot:${i}` })).filter((s) => Boolean(s.url)).map(({ url, pinKey }) => {
1534
1619
  const safeHref = safeExternalHref(url);
1535
1620
  return safeHref ? /* @__PURE__ */ jsx2(
1536
1621
  "a",
@@ -1539,9 +1624,9 @@ function ReportDetailView({
1539
1624
  href: safeHref,
1540
1625
  target: "_blank",
1541
1626
  rel: "noopener noreferrer",
1542
- children: /* @__PURE__ */ jsx2("img", { src: url, alt: "", loading: "lazy" })
1627
+ children: /* @__PURE__ */ jsx2("img", { src: url, alt: "", loading: "lazy", onError: () => unpinUrl(pinKey) })
1543
1628
  }
1544
- ) : /* @__PURE__ */ jsx2("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx2("img", { src: url, alt: "", loading: "lazy" }) });
1629
+ ) : /* @__PURE__ */ jsx2("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx2("img", { src: url, alt: "", loading: "lazy", onError: () => unpinUrl(pinKey) }) });
1545
1630
  }),
1546
1631
  /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1547
1632
  detail.comments.length === 0 ? /* @__PURE__ */ jsx2("p", { class: "report-detail-empty", children: strings["detail.no_replies"] }) : /* @__PURE__ */ jsx2("ul", { class: "report-comments", children: detail.comments.map((c) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2(
@@ -1549,7 +1634,8 @@ function ReportDetailView({
1549
1634
  {
1550
1635
  comment: c,
1551
1636
  strings,
1552
- ...onOpenSeq && { onOpenSeq }
1637
+ ...onOpenSeq && { onOpenSeq },
1638
+ stableUrl: (key, url) => pinUrl(key, url) ?? url
1553
1639
  }
1554
1640
  ) })) }),
1555
1641
  detail.status_history && detail.status_history.length > 0 && /* @__PURE__ */ jsx2(StatusHistorySection, { rows: detail.status_history, strings }),
@@ -1833,10 +1919,13 @@ function BoardView({
1833
1919
  const [page, setPage] = useState2(null);
1834
1920
  const [kpis, setKpis] = useState2(null);
1835
1921
  const [loading, setLoading] = useState2(true);
1922
+ const [stale, setStale] = useState2(false);
1836
1923
  const [error, setError] = useState2(null);
1837
1924
  const [selectedId, setSelectedId] = useState2(initialSelectedId ?? null);
1838
1925
  const [detailKey, setDetailKey] = useState2(0);
1839
1926
  const [reloadTick, setReloadTick] = useState2(0);
1927
+ const [extra, setExtra] = useState2(null);
1928
+ const [loadingMore, setLoadingMore] = useState2(false);
1840
1929
  const [qDraft, setQDraft] = useState2(filters.q ?? "");
1841
1930
  const debouncedQ = useDebouncedValue(qDraft, SEARCH_DEBOUNCE_MS);
1842
1931
  useEffect2(() => {
@@ -1866,16 +1955,36 @@ function BoardView({
1866
1955
  }, [filtersHash(filters), thisPage, reloadTick]);
1867
1956
  useEffect2(() => {
1868
1957
  let cancelled = false;
1869
- setLoading(true);
1958
+ const controller = new AbortController();
1959
+ const cacheKey = boardCacheKey(externalId, JSON.stringify(fetchFilters));
1960
+ const cached = readBoardCache(cacheKey);
1961
+ if (cached) {
1962
+ setPage(cached.page);
1963
+ setKpis(cached.kpis);
1964
+ setStale(false);
1965
+ setLoading(false);
1966
+ } else {
1967
+ setPage(
1968
+ (prev) => prev ? { ...prev, results: applyLocalFilters(prev.results, fetchFilters) } : null
1969
+ );
1970
+ setStale(true);
1971
+ setLoading(true);
1972
+ }
1973
+ setExtra(null);
1974
+ setLoadingMore(false);
1870
1975
  const poll = startPoll(async () => {
1871
1976
  try {
1872
- const [list, k] = await Promise.all([
1873
- api.listBoard(externalId, fetchFilters),
1874
- api.fetchBoardKpis(externalId, fetchFilters)
1875
- ]);
1977
+ const list = await api.listBoard(externalId, fetchFilters, {
1978
+ signal: controller.signal
1979
+ });
1980
+ const k = list.kpis ?? await api.fetchBoardKpis(externalId, fetchFilters, {
1981
+ signal: controller.signal
1982
+ });
1876
1983
  if (cancelled) return true;
1984
+ writeBoardCache(cacheKey, list, k);
1877
1985
  setPage(list);
1878
1986
  setKpis(k);
1987
+ setStale(false);
1879
1988
  setError(null);
1880
1989
  return true;
1881
1990
  } catch (e) {
@@ -1889,17 +1998,41 @@ function BoardView({
1889
1998
  return () => {
1890
1999
  cancelled = true;
1891
2000
  poll.stop();
2001
+ controller.abort();
1892
2002
  };
1893
2003
  }, [api, externalId, JSON.stringify(fetchFilters)]);
2004
+ const visibleRows = useMemo(() => {
2005
+ if (!page) return [];
2006
+ if (!extra || extra.rows.length === 0) return page.results;
2007
+ const seen = new Set(page.results.map((r) => r.id));
2008
+ return [...page.results, ...extra.rows.filter((r) => !seen.has(r.id))];
2009
+ }, [page, extra]);
2010
+ const hasMore = extra ? extra.hasMore : Boolean(page?.next);
2011
+ const loadMore = async () => {
2012
+ if (loadingMore || !page) return;
2013
+ const pageNum = extra ? extra.nextPage : 2;
2014
+ setLoadingMore(true);
2015
+ try {
2016
+ const res = await api.listBoard(externalId, { ...fetchFilters, page: pageNum });
2017
+ setExtra((prev) => ({
2018
+ rows: [...prev?.rows ?? [], ...res.results],
2019
+ nextPage: pageNum + 1,
2020
+ hasMore: res.next !== null
2021
+ }));
2022
+ } catch {
2023
+ } finally {
2024
+ setLoadingMore(false);
2025
+ }
2026
+ };
1894
2027
  const selectedRow = useMemo(() => {
1895
- if (!selectedId || !page) return null;
1896
- return page.results.find((r) => r.id === selectedId) ?? null;
1897
- }, [selectedId, page]);
2028
+ if (!selectedId) return null;
2029
+ return visibleRows.find((r) => r.id === selectedId) ?? null;
2030
+ }, [selectedId, visibleRows]);
1898
2031
  const onPickRow = (row) => {
1899
2032
  setSelectedId(row.id);
1900
2033
  setDetailKey((k) => k + 1);
1901
2034
  };
1902
- return /* @__PURE__ */ jsxs4("div", { class: "board-view", children: [
2035
+ return /* @__PURE__ */ jsxs4("div", { class: `board-view ${stale ? "is-stale" : ""}`, children: [
1903
2036
  /* @__PURE__ */ jsx3(BoardHeader, { strings, kpis, thisPage }),
1904
2037
  /* @__PURE__ */ jsx3(
1905
2038
  BoardFilters,
@@ -1915,7 +2048,7 @@ function BoardView({
1915
2048
  }
1916
2049
  ),
1917
2050
  /* @__PURE__ */ jsxs4("div", { class: "board-body", children: [
1918
- /* @__PURE__ */ jsxs4("div", { class: "board-list-wrap", "aria-busy": loading && !page, children: [
2051
+ /* @__PURE__ */ jsxs4("div", { class: "board-list-wrap", "aria-busy": loading, children: [
1919
2052
  error && /* @__PURE__ */ jsxs4("div", { class: "board-error", role: "alert", children: [
1920
2053
  /* @__PURE__ */ jsx3("span", { children: error }),
1921
2054
  /* @__PURE__ */ jsx3(
@@ -1933,7 +2066,7 @@ function BoardView({
1933
2066
  )
1934
2067
  ] }),
1935
2068
  !error && loading && !page && /* @__PURE__ */ jsx3(BoardListSkeleton, {}),
1936
- !error && page && page.results.length === 0 && !loading && /* @__PURE__ */ jsx3(
2069
+ !error && page && visibleRows.length === 0 && (!loading || stale) && /* @__PURE__ */ jsx3(
1937
2070
  BoardEmpty,
1938
2071
  {
1939
2072
  strings,
@@ -1941,27 +2074,40 @@ function BoardView({
1941
2074
  onAllPages: () => setThisPage(false)
1942
2075
  }
1943
2076
  ),
1944
- !error && page && page.results.length > 0 && /* @__PURE__ */ jsx3(
1945
- BoardList,
1946
- {
1947
- rows: page.results,
1948
- selectedId,
1949
- onPick: onPickRow,
1950
- strings,
1951
- total: page.count,
1952
- thisPage
1953
- }
1954
- )
2077
+ !error && page && visibleRows.length > 0 && /* @__PURE__ */ jsxs4(Fragment2, { children: [
2078
+ /* @__PURE__ */ jsx3(
2079
+ BoardList,
2080
+ {
2081
+ rows: visibleRows,
2082
+ selectedId,
2083
+ onPick: onPickRow,
2084
+ strings,
2085
+ total: page.count,
2086
+ thisPage
2087
+ }
2088
+ ),
2089
+ hasMore && !stale && /* @__PURE__ */ jsx3(
2090
+ "button",
2091
+ {
2092
+ type: "button",
2093
+ class: "btn btn--ghost board-load-more",
2094
+ onClick: () => void loadMore(),
2095
+ disabled: loadingMore,
2096
+ children: strings["board.list.loadMore"]
2097
+ }
2098
+ )
2099
+ ] })
1955
2100
  ] }),
1956
- /* @__PURE__ */ jsx3("div", { class: `board-detail-wrap ${selectedRow ? "has-selection" : ""}`, children: selectedRow ? /* @__PURE__ */ jsx3(
2101
+ /* @__PURE__ */ jsx3("div", { class: `board-detail-wrap ${selectedId ? "has-selection" : ""}`, children: selectedId ? /* @__PURE__ */ jsx3(
1957
2102
  ReportDetailView,
1958
2103
  {
1959
2104
  api,
1960
2105
  externalId,
1961
- reportId: selectedRow.id,
2106
+ reportId: selectedId,
1962
2107
  strings,
1963
2108
  onBack: () => setSelectedId(null),
1964
- canModerate: selectedRow.can_moderate ?? selectedRow.is_mine,
2109
+ canModerate: selectedRow ? selectedRow.can_moderate ?? selectedRow.is_mine : false,
2110
+ ...selectedRow ? { seed: selectedRow } : {},
1965
2111
  variant: "board",
1966
2112
  onOpenSeq: (seq) => {
1967
2113
  void api.getReportBySeq(seq, externalId).then((r) => {
@@ -2283,6 +2429,33 @@ function useDebouncedValue(value, ms) {
2283
2429
  }, [value, ms]);
2284
2430
  return debounced;
2285
2431
  }
2432
+ var SEVERITY_RANK = { blocker: 0, high: 1, medium: 2, low: 3 };
2433
+ function applyLocalFilters(rows, f) {
2434
+ let out = rows;
2435
+ if (f.status?.length) out = out.filter((r) => f.status.includes(r.status));
2436
+ if (f.type?.length) out = out.filter((r) => f.type.includes(r.feedback_type));
2437
+ if (f.severity?.length) out = out.filter((r) => f.severity.includes(r.severity));
2438
+ if (f.mine) out = out.filter((r) => r.is_mine);
2439
+ const sorted = out.slice();
2440
+ switch (f.ordering ?? "recent") {
2441
+ case "oldest":
2442
+ sorted.sort((a, b) => a.created_at.localeCompare(b.created_at));
2443
+ break;
2444
+ case "updated":
2445
+ sorted.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
2446
+ break;
2447
+ case "severity":
2448
+ sorted.sort(
2449
+ (a, b) => (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9)
2450
+ );
2451
+ break;
2452
+ case "status":
2453
+ break;
2454
+ default:
2455
+ sorted.sort((a, b) => b.created_at.localeCompare(a.created_at));
2456
+ }
2457
+ return sorted;
2458
+ }
2286
2459
  function filtersHash(f) {
2287
2460
  return JSON.stringify({
2288
2461
  s: f.status?.slice().sort(),
@@ -2338,7 +2511,7 @@ function repliesLabel(count, strings) {
2338
2511
  if (count === 1) return strings["mine.replies_one"];
2339
2512
  return strings["mine.replies_many"].replace("{count}", String(count));
2340
2513
  }
2341
- function ReportRow({ row, strings, onClick }) {
2514
+ function ReportRow({ row, strings, onClick, extraMeta }) {
2342
2515
  const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
2343
2516
  return /* @__PURE__ */ jsxs5("button", { type: "button", class: "mine-row", onClick, children: [
2344
2517
  /* @__PURE__ */ jsxs5("div", { class: "mine-row-pills", children: [
@@ -2352,6 +2525,10 @@ function ReportRow({ row, strings, onClick }) {
2352
2525
  row.comment_count > 0 && /* @__PURE__ */ jsxs5("span", { children: [
2353
2526
  "\xB7 ",
2354
2527
  repliesLabel(row.comment_count, strings)
2528
+ ] }),
2529
+ extraMeta && /* @__PURE__ */ jsxs5("span", { children: [
2530
+ "\xB7 ",
2531
+ extraMeta
2355
2532
  ] })
2356
2533
  ] })
2357
2534
  ] });
@@ -2456,7 +2633,15 @@ function ChangelogList({ api, externalId, strings, onSelect }) {
2456
2633
  /* @__PURE__ */ jsx5("span", { class: "changelog-group-rule", "aria-hidden": "true" }),
2457
2634
  /* @__PURE__ */ jsx5("span", { class: "changelog-group-count", children: strings[g.rows.length === 1 ? "changelog.resolved_one" : "changelog.resolved_many"].replace("{count}", String(g.rows.length)) })
2458
2635
  ] }),
2459
- /* @__PURE__ */ jsx5("ul", { class: "mine-rows", children: g.rows.map((row) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
2636
+ /* @__PURE__ */ jsx5("ul", { class: "mine-rows", children: g.rows.map((row) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(
2637
+ ReportRow,
2638
+ {
2639
+ row,
2640
+ strings,
2641
+ onClick: () => onSelect(row),
2642
+ extraMeta: row.pr_number ? `PR #${row.pr_number}` : void 0
2643
+ }
2644
+ ) })) })
2460
2645
  ] }, g.weekKey)) })
2461
2646
  ] });
2462
2647
  }
@@ -3633,30 +3818,32 @@ function PagePeekPanel({
3633
3818
 
3634
3819
  // src/widget/pageSignal.ts
3635
3820
  import { useCallback, useEffect as useEffect9, useState as useState8 } from "preact/hooks";
3636
- var TTL_MS = 6e4;
3821
+ var TTL_MS2 = 6e4;
3637
3822
  function computeOpenCount(kpis) {
3638
3823
  const s = kpis.by_status;
3639
3824
  return (s.new ?? 0) + (s.in_progress ?? 0) + (s.awaiting_validation ?? 0);
3640
3825
  }
3641
- var cache = /* @__PURE__ */ new Map();
3826
+ var cache2 = /* @__PURE__ */ new Map();
3642
3827
  var inflight = /* @__PURE__ */ new Map();
3643
3828
  function invalidatePageSignal() {
3644
- cache.clear();
3829
+ cache2.clear();
3645
3830
  }
3646
- function fetchOpen(api, externalId, path) {
3831
+ function fetchPageKpis(api, externalId, path) {
3647
3832
  const key = `${externalId}|${path}`;
3648
- const hit = cache.get(key);
3649
- if (hit && Date.now() - hit.fetchedAt < TTL_MS) return Promise.resolve(hit.open);
3833
+ const hit = cache2.get(key);
3834
+ if (hit && Date.now() - hit.fetchedAt < TTL_MS2) return Promise.resolve(hit.kpis);
3650
3835
  const pending = inflight.get(key);
3651
3836
  if (pending) return pending;
3652
3837
  const p = api.fetchBoardKpis(externalId, { pagePath: path }).then((kpis) => {
3653
- const open = computeOpenCount(kpis);
3654
- cache.set(key, { open, fetchedAt: Date.now() });
3655
- return open;
3838
+ cache2.set(key, { kpis, fetchedAt: Date.now() });
3839
+ return kpis;
3656
3840
  }).finally(() => inflight.delete(key));
3657
3841
  inflight.set(key, p);
3658
3842
  return p;
3659
3843
  }
3844
+ function fetchOpen(api, externalId, path) {
3845
+ return fetchPageKpis(api, externalId, path).then(computeOpenCount);
3846
+ }
3660
3847
  function usePageOpenCount(opts) {
3661
3848
  const { api, externalId, getCurrentPage, enabled } = opts;
3662
3849
  const [open, setOpen] = useState8(0);
@@ -5450,6 +5637,20 @@ button.report-detail-edit {
5450
5637
  min-height: 0;
5451
5638
  overflow: hidden;
5452
5639
  }
5640
+ /* Stale-while-revalidate: a query change keeps the previous rows visible
5641
+ * (locally pre-filtered where the row data allows) and dims them while the
5642
+ * server confirms \u2014 the instant local response is the #352 "never inert"
5643
+ * guarantee, the dim is the "still confirming" affordance. */
5644
+ .board-view.is-stale .board-list,
5645
+ .board-view.is-stale .board-list-count,
5646
+ .board-view.is-stale .board-kpi-strip {
5647
+ opacity: 0.55;
5648
+ transition: opacity 0.15s ease;
5649
+ }
5650
+ .board-load-more {
5651
+ margin: 4px auto 8px;
5652
+ flex: 0 0 auto;
5653
+ }
5453
5654
  .board-list-count {
5454
5655
  font-size: var(--mfb-text-xs);
5455
5656
  color: var(--mfb-text-muted);
@@ -5771,7 +5972,7 @@ function mountWidget(options) {
5771
5972
  function openWidget(externalId) {
5772
5973
  rerender({ ...currentState, open: true, tab: "send" });
5773
5974
  if (options.openToCurrentPageFeedback && options.api && externalId) {
5774
- options.api.fetchBoardKpis(externalId, { pagePath: currentPagePath(options) }).then((kpis) => {
5975
+ fetchPageKpis(options.api, externalId, currentPagePath(options)).then((kpis) => {
5775
5976
  if (kpis.total > 0 && currentState.open && currentState.tab === "send") {
5776
5977
  rerender({ ...currentState, tab: "board" });
5777
5978
  }
@@ -6203,8 +6404,8 @@ function createFeedback(config) {
6203
6404
  capture_method: captureMethod,
6204
6405
  technical_context
6205
6406
  };
6206
- if ("0.30.0") {
6207
- payload.widget_version = "0.30.0";
6407
+ if ("0.32.0") {
6408
+ payload.widget_version = "0.32.0";
6208
6409
  }
6209
6410
  if (manualScreenshots?.length) {
6210
6411
  payload.screenshots = manualScreenshots;
@@ -6330,4 +6531,4 @@ function createFeedback(config) {
6330
6531
  export {
6331
6532
  createFeedback
6332
6533
  };
6333
- //# sourceMappingURL=chunk-5F36UFXG.mjs.map
6534
+ //# sourceMappingURL=chunk-YNDBZR37.mjs.map