@mhosaic/feedback 0.31.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,18 +1955,36 @@ function BoardView({
1866
1955
  }, [filtersHash(filters), thisPage, reloadTick]);
1867
1956
  useEffect2(() => {
1868
1957
  let cancelled = false;
1869
- setPage(null);
1870
- setKpis(null);
1871
- 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);
1872
1975
  const poll = startPoll(async () => {
1873
1976
  try {
1874
- const [list, k] = await Promise.all([
1875
- api.listBoard(externalId, fetchFilters),
1876
- api.fetchBoardKpis(externalId, fetchFilters)
1877
- ]);
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
+ });
1878
1983
  if (cancelled) return true;
1984
+ writeBoardCache(cacheKey, list, k);
1879
1985
  setPage(list);
1880
1986
  setKpis(k);
1987
+ setStale(false);
1881
1988
  setError(null);
1882
1989
  return true;
1883
1990
  } catch (e) {
@@ -1891,17 +1998,41 @@ function BoardView({
1891
1998
  return () => {
1892
1999
  cancelled = true;
1893
2000
  poll.stop();
2001
+ controller.abort();
1894
2002
  };
1895
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
+ };
1896
2027
  const selectedRow = useMemo(() => {
1897
- if (!selectedId || !page) return null;
1898
- return page.results.find((r) => r.id === selectedId) ?? null;
1899
- }, [selectedId, page]);
2028
+ if (!selectedId) return null;
2029
+ return visibleRows.find((r) => r.id === selectedId) ?? null;
2030
+ }, [selectedId, visibleRows]);
1900
2031
  const onPickRow = (row) => {
1901
2032
  setSelectedId(row.id);
1902
2033
  setDetailKey((k) => k + 1);
1903
2034
  };
1904
- return /* @__PURE__ */ jsxs4("div", { class: "board-view", children: [
2035
+ return /* @__PURE__ */ jsxs4("div", { class: `board-view ${stale ? "is-stale" : ""}`, children: [
1905
2036
  /* @__PURE__ */ jsx3(BoardHeader, { strings, kpis, thisPage }),
1906
2037
  /* @__PURE__ */ jsx3(
1907
2038
  BoardFilters,
@@ -1917,7 +2048,7 @@ function BoardView({
1917
2048
  }
1918
2049
  ),
1919
2050
  /* @__PURE__ */ jsxs4("div", { class: "board-body", children: [
1920
- /* @__PURE__ */ jsxs4("div", { class: "board-list-wrap", "aria-busy": loading && !page, children: [
2051
+ /* @__PURE__ */ jsxs4("div", { class: "board-list-wrap", "aria-busy": loading, children: [
1921
2052
  error && /* @__PURE__ */ jsxs4("div", { class: "board-error", role: "alert", children: [
1922
2053
  /* @__PURE__ */ jsx3("span", { children: error }),
1923
2054
  /* @__PURE__ */ jsx3(
@@ -1935,7 +2066,7 @@ function BoardView({
1935
2066
  )
1936
2067
  ] }),
1937
2068
  !error && loading && !page && /* @__PURE__ */ jsx3(BoardListSkeleton, {}),
1938
- !error && page && page.results.length === 0 && !loading && /* @__PURE__ */ jsx3(
2069
+ !error && page && visibleRows.length === 0 && (!loading || stale) && /* @__PURE__ */ jsx3(
1939
2070
  BoardEmpty,
1940
2071
  {
1941
2072
  strings,
@@ -1943,27 +2074,40 @@ function BoardView({
1943
2074
  onAllPages: () => setThisPage(false)
1944
2075
  }
1945
2076
  ),
1946
- !error && page && page.results.length > 0 && /* @__PURE__ */ jsx3(
1947
- BoardList,
1948
- {
1949
- rows: page.results,
1950
- selectedId,
1951
- onPick: onPickRow,
1952
- strings,
1953
- total: page.count,
1954
- thisPage
1955
- }
1956
- )
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
+ ] })
1957
2100
  ] }),
1958
- /* @__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(
1959
2102
  ReportDetailView,
1960
2103
  {
1961
2104
  api,
1962
2105
  externalId,
1963
- reportId: selectedRow.id,
2106
+ reportId: selectedId,
1964
2107
  strings,
1965
2108
  onBack: () => setSelectedId(null),
1966
- canModerate: selectedRow.can_moderate ?? selectedRow.is_mine,
2109
+ canModerate: selectedRow ? selectedRow.can_moderate ?? selectedRow.is_mine : false,
2110
+ ...selectedRow ? { seed: selectedRow } : {},
1967
2111
  variant: "board",
1968
2112
  onOpenSeq: (seq) => {
1969
2113
  void api.getReportBySeq(seq, externalId).then((r) => {
@@ -2285,6 +2429,33 @@ function useDebouncedValue(value, ms) {
2285
2429
  }, [value, ms]);
2286
2430
  return debounced;
2287
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
+ }
2288
2459
  function filtersHash(f) {
2289
2460
  return JSON.stringify({
2290
2461
  s: f.status?.slice().sort(),
@@ -2340,7 +2511,7 @@ function repliesLabel(count, strings) {
2340
2511
  if (count === 1) return strings["mine.replies_one"];
2341
2512
  return strings["mine.replies_many"].replace("{count}", String(count));
2342
2513
  }
2343
- function ReportRow({ row, strings, onClick }) {
2514
+ function ReportRow({ row, strings, onClick, extraMeta }) {
2344
2515
  const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
2345
2516
  return /* @__PURE__ */ jsxs5("button", { type: "button", class: "mine-row", onClick, children: [
2346
2517
  /* @__PURE__ */ jsxs5("div", { class: "mine-row-pills", children: [
@@ -2354,6 +2525,10 @@ function ReportRow({ row, strings, onClick }) {
2354
2525
  row.comment_count > 0 && /* @__PURE__ */ jsxs5("span", { children: [
2355
2526
  "\xB7 ",
2356
2527
  repliesLabel(row.comment_count, strings)
2528
+ ] }),
2529
+ extraMeta && /* @__PURE__ */ jsxs5("span", { children: [
2530
+ "\xB7 ",
2531
+ extraMeta
2357
2532
  ] })
2358
2533
  ] })
2359
2534
  ] });
@@ -2458,7 +2633,15 @@ function ChangelogList({ api, externalId, strings, onSelect }) {
2458
2633
  /* @__PURE__ */ jsx5("span", { class: "changelog-group-rule", "aria-hidden": "true" }),
2459
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)) })
2460
2635
  ] }),
2461
- /* @__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
+ ) })) })
2462
2645
  ] }, g.weekKey)) })
2463
2646
  ] });
2464
2647
  }
@@ -3635,30 +3818,32 @@ function PagePeekPanel({
3635
3818
 
3636
3819
  // src/widget/pageSignal.ts
3637
3820
  import { useCallback, useEffect as useEffect9, useState as useState8 } from "preact/hooks";
3638
- var TTL_MS = 6e4;
3821
+ var TTL_MS2 = 6e4;
3639
3822
  function computeOpenCount(kpis) {
3640
3823
  const s = kpis.by_status;
3641
3824
  return (s.new ?? 0) + (s.in_progress ?? 0) + (s.awaiting_validation ?? 0);
3642
3825
  }
3643
- var cache = /* @__PURE__ */ new Map();
3826
+ var cache2 = /* @__PURE__ */ new Map();
3644
3827
  var inflight = /* @__PURE__ */ new Map();
3645
3828
  function invalidatePageSignal() {
3646
- cache.clear();
3829
+ cache2.clear();
3647
3830
  }
3648
- function fetchOpen(api, externalId, path) {
3831
+ function fetchPageKpis(api, externalId, path) {
3649
3832
  const key = `${externalId}|${path}`;
3650
- const hit = cache.get(key);
3651
- 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);
3652
3835
  const pending = inflight.get(key);
3653
3836
  if (pending) return pending;
3654
3837
  const p = api.fetchBoardKpis(externalId, { pagePath: path }).then((kpis) => {
3655
- const open = computeOpenCount(kpis);
3656
- cache.set(key, { open, fetchedAt: Date.now() });
3657
- return open;
3838
+ cache2.set(key, { kpis, fetchedAt: Date.now() });
3839
+ return kpis;
3658
3840
  }).finally(() => inflight.delete(key));
3659
3841
  inflight.set(key, p);
3660
3842
  return p;
3661
3843
  }
3844
+ function fetchOpen(api, externalId, path) {
3845
+ return fetchPageKpis(api, externalId, path).then(computeOpenCount);
3846
+ }
3662
3847
  function usePageOpenCount(opts) {
3663
3848
  const { api, externalId, getCurrentPage, enabled } = opts;
3664
3849
  const [open, setOpen] = useState8(0);
@@ -5452,6 +5637,20 @@ button.report-detail-edit {
5452
5637
  min-height: 0;
5453
5638
  overflow: hidden;
5454
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
+ }
5455
5654
  .board-list-count {
5456
5655
  font-size: var(--mfb-text-xs);
5457
5656
  color: var(--mfb-text-muted);
@@ -5773,7 +5972,7 @@ function mountWidget(options) {
5773
5972
  function openWidget(externalId) {
5774
5973
  rerender({ ...currentState, open: true, tab: "send" });
5775
5974
  if (options.openToCurrentPageFeedback && options.api && externalId) {
5776
- options.api.fetchBoardKpis(externalId, { pagePath: currentPagePath(options) }).then((kpis) => {
5975
+ fetchPageKpis(options.api, externalId, currentPagePath(options)).then((kpis) => {
5777
5976
  if (kpis.total > 0 && currentState.open && currentState.tab === "send") {
5778
5977
  rerender({ ...currentState, tab: "board" });
5779
5978
  }
@@ -6205,8 +6404,8 @@ function createFeedback(config) {
6205
6404
  capture_method: captureMethod,
6206
6405
  technical_context
6207
6406
  };
6208
- if ("0.31.0") {
6209
- payload.widget_version = "0.31.0";
6407
+ if ("0.32.0") {
6408
+ payload.widget_version = "0.32.0";
6210
6409
  }
6211
6410
  if (manualScreenshots?.length) {
6212
6411
  payload.screenshots = manualScreenshots;
@@ -6332,4 +6531,4 @@ function createFeedback(config) {
6332
6531
  export {
6333
6532
  createFeedback
6334
6533
  };
6335
- //# sourceMappingURL=chunk-AA24TFKX.mjs.map
6534
+ //# sourceMappingURL=chunk-YNDBZR37.mjs.map