@mhosaic/feedback 0.27.1 → 0.28.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.
@@ -32,6 +32,42 @@ function assertSafeEndpoint(endpoint) {
32
32
  `[mhosaic-feedback] \`endpoint\` must use https:// (got ${parsed.protocol}//${parsed.hostname}). http:// is only allowed for localhost in dev.`
33
33
  );
34
34
  }
35
+ var RateLimitError = class extends Error {
36
+ retryAfterSeconds;
37
+ constructor(retryAfterSeconds) {
38
+ super(`rate limited; retry after ${retryAfterSeconds}s`);
39
+ this.name = "RateLimitError";
40
+ this.retryAfterSeconds = retryAfterSeconds;
41
+ }
42
+ };
43
+ function parseRetryAfter(response) {
44
+ const raw = response.headers.get("Retry-After");
45
+ const n = raw ? Number.parseInt(raw, 10) : NaN;
46
+ return Number.isFinite(n) && n >= 0 ? n : 0;
47
+ }
48
+ async function ensureReadable(response, label) {
49
+ if (response.status === 429) throw new RateLimitError(parseRetryAfter(response));
50
+ if (!response.ok) {
51
+ const text = await response.text().catch(() => "");
52
+ throw new Error(`${label} failed: ${response.status} ${text}`);
53
+ }
54
+ }
55
+ async function readJsonArray(response, label) {
56
+ await ensureReadable(response, label);
57
+ const data = await response.json().catch(() => void 0);
58
+ if (!Array.isArray(data)) {
59
+ throw new Error(`${label}: unexpected response shape (expected an array)`);
60
+ }
61
+ return data;
62
+ }
63
+ async function readJsonObject(response, label) {
64
+ await ensureReadable(response, label);
65
+ const data = await response.json().catch(() => void 0);
66
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
67
+ throw new Error(`${label}: unexpected response shape (expected an object)`);
68
+ }
69
+ return data;
70
+ }
35
71
  function createApiClient(options) {
36
72
  const endpoint = (options.endpoint ?? "").replace(/\/+$/, "");
37
73
  if (!endpoint) {
@@ -107,11 +143,7 @@ function createApiClient(options) {
107
143
  headers: widgetHeaders(externalId)
108
144
  });
109
145
  if (response.status === 404) return [];
110
- if (!response.ok) {
111
- const text = await response.text().catch(() => "");
112
- throw new Error(`listMine failed: ${response.status} ${text}`);
113
- }
114
- return response.json();
146
+ return readJsonArray(response, "listMine");
115
147
  }
116
148
  async function listChangelog(externalId) {
117
149
  const response = await fetcher(
@@ -119,22 +151,21 @@ function createApiClient(options) {
119
151
  { method: "GET", headers: widgetHeaders(externalId) }
120
152
  );
121
153
  if (response.status === 404) return [];
122
- if (!response.ok) {
123
- const text = await response.text().catch(() => "");
124
- throw new Error(`listChangelog failed: ${response.status} ${text}`);
125
- }
126
- return response.json();
154
+ return readJsonArray(response, "listChangelog");
127
155
  }
128
156
  async function getReport(reportId, externalId) {
129
157
  const response = await fetcher(
130
158
  `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
131
159
  { method: "GET", headers: widgetHeaders(externalId) }
132
160
  );
133
- if (!response.ok) {
134
- const text = await response.text().catch(() => "");
135
- throw new Error(`getReport failed: ${response.status} ${text}`);
136
- }
137
- return response.json();
161
+ return readJsonObject(response, "getReport");
162
+ }
163
+ async function getReportBySeq(seq, externalId) {
164
+ const response = await fetcher(
165
+ `${endpoint}/api/feedback/v1/reports/widget/by-seq/${seq}/`,
166
+ { method: "GET", headers: widgetHeaders(externalId) }
167
+ );
168
+ return readJsonObject(response, "getReportBySeq");
138
169
  }
139
170
  async function addComment(reportId, externalId, body, clientNonce) {
140
171
  const response = await fetcher(
@@ -215,11 +246,11 @@ function createApiClient(options) {
215
246
  if (response.status === 404) {
216
247
  return { count: 0, next: null, previous: null, results: [] };
217
248
  }
218
- if (!response.ok) {
219
- const text = await response.text().catch(() => "");
220
- throw new Error(`listBoard failed: ${response.status} ${text}`);
249
+ const page = await readJsonObject(response, "listBoard");
250
+ if (!Array.isArray(page.results)) {
251
+ throw new Error("listBoard: unexpected response shape (missing results[])");
221
252
  }
222
- return response.json();
253
+ return page;
223
254
  }
224
255
  async function fetchBoardKpis(externalId, filters) {
225
256
  const response = await fetcher(
@@ -229,11 +260,7 @@ function createApiClient(options) {
229
260
  if (response.status === 404) {
230
261
  return { total: 0, by_status: {}, resolution_rate: 0, scope: "mine" };
231
262
  }
232
- if (!response.ok) {
233
- const text = await response.text().catch(() => "");
234
- throw new Error(`fetchBoardKpis failed: ${response.status} ${text}`);
235
- }
236
- return response.json();
263
+ return readJsonObject(response, "fetchBoardKpis");
237
264
  }
238
265
  async function checkVisibility(externalId, email) {
239
266
  const response = await fetcher(
@@ -262,6 +289,7 @@ function createApiClient(options) {
262
289
  listMine,
263
290
  listChangelog,
264
291
  getReport,
292
+ getReportBySeq,
265
293
  addComment,
266
294
  closeAsResolved,
267
295
  reopenUnresolved,
@@ -573,6 +601,11 @@ var DEFAULT_STRINGS = {
573
601
  "form.close": "Close",
574
602
  "form.submitting": "Sending\u2026",
575
603
  "form.success": "Thanks \u2014 your feedback was sent.",
604
+ "form.success.seq": "Thanks \u2014 report #{seq} was sent. \u2713",
605
+ "form.discard.title": "Unsaved changes",
606
+ "form.discard.body": "Discard your feedback?",
607
+ "form.discard.keep": "Keep editing",
608
+ "form.discard.confirm": "Discard",
576
609
  "form.error": "Could not send. Please try again.",
577
610
  "form.description.required": "Please describe the issue before sending.",
578
611
  "form.screenshot.label": "Screenshot",
@@ -585,6 +618,7 @@ var DEFAULT_STRINGS = {
585
618
  "form.screenshot.error_size": "File too large (max {max} MB).",
586
619
  "form.screenshot.error_count": "Too many screenshots (max {max}).",
587
620
  "form.context.label": "Page",
621
+ "form.capture.notice": "Console, network activity and errors are captured automatically to help us debug.",
588
622
  "type.bug": "Bug",
589
623
  "type.feature": "Feature request",
590
624
  "type.question": "Question",
@@ -663,6 +697,8 @@ var DEFAULT_STRINGS = {
663
697
  "mine.refresh": "Refresh",
664
698
  "mine.loading": "Loading\u2026",
665
699
  "mine.error": "Could not load your reports.",
700
+ "error.rate_limited": "Too many requests \u2014 retry in {seconds}s.",
701
+ "error.rate_limited_generic": "Too many requests \u2014 try again in a moment.",
666
702
  "mine.replies_one": "1 reply",
667
703
  "mine.replies_many": "{count} replies",
668
704
  "mine.filter.empty": "No reports match this filter.",
@@ -737,6 +773,11 @@ var FRENCH_STRINGS = {
737
773
  "form.close": "Fermer",
738
774
  "form.submitting": "Envoi\u2026",
739
775
  "form.success": "Merci \u2014 votre commentaire a \xE9t\xE9 envoy\xE9.",
776
+ "form.success.seq": "Merci \u2014 rapport n\xB0{seq} envoy\xE9. \u2713",
777
+ "form.discard.title": "Modifications non enregistr\xE9es",
778
+ "form.discard.body": "Abandonner votre commentaire ?",
779
+ "form.discard.keep": "Continuer la saisie",
780
+ "form.discard.confirm": "Abandonner",
740
781
  "form.error": "\xC9chec d\u2019envoi. Veuillez r\xE9essayer.",
741
782
  "form.description.required": "D\xE9crivez le probl\xE8me avant d\u2019envoyer.",
742
783
  "form.screenshot.label": "Capture d\u2019\xE9cran",
@@ -749,6 +790,7 @@ var FRENCH_STRINGS = {
749
790
  "form.screenshot.error_size": "Fichier trop volumineux (max {max} Mo).",
750
791
  "form.screenshot.error_count": "Trop de captures d\u2019\xE9cran (max {max}).",
751
792
  "form.context.label": "Page",
793
+ "form.capture.notice": "La console, l\u2019activit\xE9 r\xE9seau et les erreurs sont captur\xE9es automatiquement pour faciliter le diagnostic.",
752
794
  "type.bug": "Bogue",
753
795
  "type.feature": "Suggestion",
754
796
  "type.question": "Question",
@@ -827,6 +869,8 @@ var FRENCH_STRINGS = {
827
869
  "mine.refresh": "Actualiser",
828
870
  "mine.loading": "Chargement\u2026",
829
871
  "mine.error": "Impossible de charger vos rapports.",
872
+ "error.rate_limited": "Trop de requ\xEAtes \u2014 r\xE9essaie dans {seconds} s.",
873
+ "error.rate_limited_generic": "Trop de requ\xEAtes \u2014 r\xE9essaie dans un instant.",
830
874
  "mine.replies_one": "1 r\xE9ponse",
831
875
  "mine.replies_many": "{count} r\xE9ponses",
832
876
  "mine.filter.empty": "Aucun rapport ne correspond \xE0 ce filtre.",
@@ -995,21 +1039,64 @@ function startPoll(tick, intervalMs) {
995
1039
  };
996
1040
  }
997
1041
 
1042
+ // src/widget/rateLimit.ts
1043
+ function rateLimitMessage(err, strings) {
1044
+ if (!(err instanceof RateLimitError)) return null;
1045
+ return err.retryAfterSeconds > 0 ? strings["error.rate_limited"].replace(
1046
+ "{seconds}",
1047
+ String(err.retryAfterSeconds)
1048
+ ) : strings["error.rate_limited_generic"];
1049
+ }
1050
+
998
1051
  // src/widget/ReportDetailView.tsx
999
1052
  import { useEffect, useRef, useState } from "preact/hooks";
1000
1053
 
1054
+ // src/widget/linkifySeq.tsx
1055
+ import { jsxs } from "preact/jsx-runtime";
1056
+ var SEQ_RE = /#(\d+)/g;
1057
+ function linkifySeq(text, onOpenSeq) {
1058
+ const value = text ?? "";
1059
+ if (!value || !onOpenSeq) return [value];
1060
+ const parts = [];
1061
+ let last = 0;
1062
+ SEQ_RE.lastIndex = 0;
1063
+ let m;
1064
+ while ((m = SEQ_RE.exec(value)) !== null) {
1065
+ if (m.index > last) parts.push(value.slice(last, m.index));
1066
+ const seq = Number(m[1]);
1067
+ parts.push(
1068
+ /* @__PURE__ */ jsxs(
1069
+ "button",
1070
+ {
1071
+ type: "button",
1072
+ class: "seq-link",
1073
+ onClick: () => onOpenSeq(seq),
1074
+ "aria-label": `Open report #${seq}`,
1075
+ children: [
1076
+ "#",
1077
+ seq
1078
+ ]
1079
+ }
1080
+ )
1081
+ );
1082
+ last = m.index + m[0].length;
1083
+ }
1084
+ if (last < value.length) parts.push(value.slice(last));
1085
+ return parts.length ? parts : [value];
1086
+ }
1087
+
1001
1088
  // src/widget/CommentBubble.tsx
1002
- import { jsx, jsxs } from "preact/jsx-runtime";
1003
- function CommentBubble({ comment, strings }) {
1089
+ import { jsx, jsxs as jsxs2 } from "preact/jsx-runtime";
1090
+ function CommentBubble({ comment, strings, onOpenSeq }) {
1004
1091
  const isMine = comment.is_mine;
1005
1092
  const isAgent = !isMine && comment.author_source === "mcp";
1006
1093
  const isSystem = !isMine && comment.author_source === "system";
1007
1094
  const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
1008
1095
  const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
1009
1096
  const label = comment.author_label || strings[labelKey];
1010
- return /* @__PURE__ */ jsxs("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1097
+ return /* @__PURE__ */ jsxs2("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1011
1098
  !isMine && label && /* @__PURE__ */ jsx("div", { class: `comment-author comment-author--${variant}`, children: label }),
1012
- /* @__PURE__ */ jsx("div", { class: "comment-body", children: comment.body }),
1099
+ /* @__PURE__ */ jsx("div", { class: "comment-body", children: linkifySeq(comment.body, onOpenSeq) }),
1013
1100
  /* @__PURE__ */ jsx("div", { class: "comment-time", children: formatTime(comment.created_at) })
1014
1101
  ] });
1015
1102
  }
@@ -1065,7 +1152,7 @@ function safeExternalHref(url) {
1065
1152
  }
1066
1153
 
1067
1154
  // src/widget/ReportDetailView.tsx
1068
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
1155
+ import { Fragment, jsx as jsx2, jsxs as jsxs3 } from "preact/jsx-runtime";
1069
1156
  var POLL_MS = 3e4;
1070
1157
  function ReportDetailView({
1071
1158
  api,
@@ -1074,7 +1161,8 @@ function ReportDetailView({
1074
1161
  strings,
1075
1162
  onBack,
1076
1163
  canModerate = true,
1077
- variant = "modal"
1164
+ variant = "modal",
1165
+ onOpenSeq
1078
1166
  }) {
1079
1167
  const [detail, setDetail] = useState(null);
1080
1168
  const [error, setError] = useState(null);
@@ -1093,7 +1181,8 @@ function ReportDetailView({
1093
1181
  } catch (err) {
1094
1182
  if (typeof console !== "undefined") console.warn("[mhosaic] getReport:", err);
1095
1183
  if (!mountedRef.current) return false;
1096
- setError("load_failed");
1184
+ const rl = rateLimitMessage(err, strings);
1185
+ setError(rl ?? "load_failed");
1097
1186
  return false;
1098
1187
  }
1099
1188
  };
@@ -1161,10 +1250,20 @@ function ReportDetailView({
1161
1250
  return /* @__PURE__ */ jsx2("div", { class: "mine-loading", children: strings["mine.loading"] });
1162
1251
  }
1163
1252
  if (!detail) {
1164
- return /* @__PURE__ */ jsxs2("div", { class: "report-detail-empty-state", role: "alert", children: [
1253
+ if (error && error !== "load_failed") {
1254
+ return /* @__PURE__ */ jsxs3("div", { class: "report-detail-empty-state", role: "alert", children: [
1255
+ /* @__PURE__ */ jsx2("p", { class: "report-detail-empty-state-body", children: error }),
1256
+ /* @__PURE__ */ jsx2("button", { type: "button", class: "btn", onClick: () => void fetchDetail(), children: strings["board.retry"] }),
1257
+ /* @__PURE__ */ jsxs3("button", { type: "button", class: "btn", onClick: onBack, children: [
1258
+ "\u2190 ",
1259
+ strings["detail.load_failed.cta"]
1260
+ ] })
1261
+ ] });
1262
+ }
1263
+ return /* @__PURE__ */ jsxs3("div", { class: "report-detail-empty-state", role: "alert", children: [
1165
1264
  /* @__PURE__ */ jsx2("p", { class: "report-detail-empty-state-title", children: strings["detail.load_failed.title"] }),
1166
1265
  /* @__PURE__ */ jsx2("p", { class: "report-detail-empty-state-body", children: strings["detail.load_failed.body"] }),
1167
- /* @__PURE__ */ jsxs2("button", { type: "button", class: "btn", onClick: onBack, children: [
1266
+ /* @__PURE__ */ jsxs3("button", { type: "button", class: "btn", onClick: onBack, children: [
1168
1267
  "\u2190 ",
1169
1268
  strings["detail.load_failed.cta"]
1170
1269
  ] })
@@ -1174,16 +1273,16 @@ function ReportDetailView({
1174
1273
  const canAct = detail.can_moderate ?? isMine;
1175
1274
  const showCloseCta = canAct && detail.status === "awaiting_validation";
1176
1275
  const showComposeBox = canAct;
1177
- return /* @__PURE__ */ jsxs2("div", { class: `report-detail variant-${variant}`, children: [
1178
- variant === "modal" && /* @__PURE__ */ jsxs2("div", { class: "report-detail-header", children: [
1179
- /* @__PURE__ */ jsxs2("button", { type: "button", class: "btn", onClick: onBack, children: [
1276
+ return /* @__PURE__ */ jsxs3("div", { class: `report-detail variant-${variant}`, children: [
1277
+ variant === "modal" && /* @__PURE__ */ jsxs3("div", { class: "report-detail-header", children: [
1278
+ /* @__PURE__ */ jsxs3("button", { type: "button", class: "btn", onClick: onBack, children: [
1180
1279
  "\u2190 ",
1181
1280
  strings["detail.back"]
1182
1281
  ] }),
1183
1282
  /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1184
1283
  ] }),
1185
- variant === "board" && /* @__PURE__ */ jsxs2("div", { class: "report-detail-header report-detail-header--board", children: [
1186
- /* @__PURE__ */ jsxs2(
1284
+ variant === "board" && /* @__PURE__ */ jsxs3("div", { class: "report-detail-header report-detail-header--board", children: [
1285
+ /* @__PURE__ */ jsxs3(
1187
1286
  "button",
1188
1287
  {
1189
1288
  type: "button",
@@ -1198,9 +1297,9 @@ function ReportDetailView({
1198
1297
  ),
1199
1298
  /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1200
1299
  ] }),
1201
- /* @__PURE__ */ jsxs2("div", { class: "report-detail-body", children: [
1300
+ /* @__PURE__ */ jsxs3("div", { class: "report-detail-body", children: [
1202
1301
  /* @__PURE__ */ jsx2(ContextBlock, { detail, strings }),
1203
- /* @__PURE__ */ jsx2("p", { class: "report-detail-description", children: detail.description }),
1302
+ /* @__PURE__ */ jsx2("p", { class: "report-detail-description", children: linkifySeq(detail.description, onOpenSeq) }),
1204
1303
  (detail.screenshots?.length ? detail.screenshots.map((s) => s.url) : [detail.screenshot_url]).filter((url) => Boolean(url)).map((url) => {
1205
1304
  const safeHref = safeExternalHref(url);
1206
1305
  return safeHref ? /* @__PURE__ */ jsx2(
@@ -1215,10 +1314,17 @@ function ReportDetailView({
1215
1314
  ) : /* @__PURE__ */ jsx2("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx2("img", { src: url, alt: "", loading: "lazy" }) });
1216
1315
  }),
1217
1316
  /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1218
- 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(CommentBubble, { comment: c, strings }) })) }),
1317
+ 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(
1318
+ CommentBubble,
1319
+ {
1320
+ comment: c,
1321
+ strings,
1322
+ ...onOpenSeq && { onOpenSeq }
1323
+ }
1324
+ ) })) }),
1219
1325
  detail.status_history && detail.status_history.length > 0 && /* @__PURE__ */ jsx2(StatusHistorySection, { rows: detail.status_history, strings }),
1220
1326
  detail.technical_context && /* @__PURE__ */ jsx2(TechnicalContextDrawer, { ctx: detail.technical_context, strings }),
1221
- showComposeBox ? /* @__PURE__ */ jsxs2("div", { class: "report-compose", children: [
1327
+ showComposeBox ? /* @__PURE__ */ jsxs3("div", { class: "report-compose", children: [
1222
1328
  /* @__PURE__ */ jsx2(
1223
1329
  "textarea",
1224
1330
  {
@@ -1228,7 +1334,7 @@ function ReportDetailView({
1228
1334
  disabled: sending
1229
1335
  }
1230
1336
  ),
1231
- /* @__PURE__ */ jsxs2("div", { class: "report-compose-actions", children: [
1337
+ /* @__PURE__ */ jsxs3("div", { class: "report-compose-actions", children: [
1232
1338
  showCloseCta && /* @__PURE__ */ jsx2(
1233
1339
  "button",
1234
1340
  {
@@ -1278,19 +1384,19 @@ function appendComment(current, next) {
1278
1384
  function ContextBlock({ detail, strings }) {
1279
1385
  const captureKey = `detail.context.capture.${detail.capture_method}`;
1280
1386
  const captureLabel = strings[captureKey] ?? detail.capture_method;
1281
- return /* @__PURE__ */ jsxs2("div", { class: "report-detail-context", children: [
1282
- /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-pills", children: [
1387
+ return /* @__PURE__ */ jsxs3("div", { class: "report-detail-context", children: [
1388
+ /* @__PURE__ */ jsxs3("div", { class: "report-detail-context-pills", children: [
1283
1389
  /* @__PURE__ */ jsx2("span", { class: "pill pill-type", children: strings[`type.${detail.feedback_type}`] }),
1284
1390
  /* @__PURE__ */ jsx2("span", { class: `pill pill-severity pill-severity--${detail.severity}`, children: strings[`severity.${detail.severity}`] }),
1285
1391
  /* @__PURE__ */ jsx2("span", { class: "pill pill-capture", children: captureLabel })
1286
1392
  ] }),
1287
- /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-line", children: [
1393
+ /* @__PURE__ */ jsxs3("div", { class: "report-detail-context-line", children: [
1288
1394
  /* @__PURE__ */ jsx2("span", { class: "report-detail-context-label", children: strings["detail.context.submitted_at"] }),
1289
1395
  /* @__PURE__ */ jsx2("span", { children: formatSubmittedAt(detail.created_at) })
1290
1396
  ] }),
1291
1397
  detail.page_url && (() => {
1292
1398
  const safeHref = safeExternalHref(detail.page_url);
1293
- return /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-line", title: detail.page_url, children: [
1399
+ return /* @__PURE__ */ jsxs3("div", { class: "report-detail-context-line", title: detail.page_url, children: [
1294
1400
  /* @__PURE__ */ jsx2("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
1295
1401
  safeHref ? /* @__PURE__ */ jsx2(
1296
1402
  "a",
@@ -1327,9 +1433,9 @@ function TechnicalContextDrawer({ ctx, strings }) {
1327
1433
  if (!hasAny) return null;
1328
1434
  const errorCount = errors.length;
1329
1435
  const summary = errorCount > 0 ? `${strings["detail.tech.title"]} \xB7 ${errorCount} ${errorCount === 1 ? strings["detail.tech.errors_one"] : strings["detail.tech.errors_many"]}` : strings["detail.tech.title"];
1330
- return /* @__PURE__ */ jsxs2("details", { class: "report-detail-tech", children: [
1436
+ return /* @__PURE__ */ jsxs3("details", { class: "report-detail-tech", children: [
1331
1437
  /* @__PURE__ */ jsx2("summary", { children: summary }),
1332
- /* @__PURE__ */ jsxs2("div", { class: "tech-body", children: [
1438
+ /* @__PURE__ */ jsxs3("div", { class: "tech-body", children: [
1333
1439
  device && /* @__PURE__ */ jsx2(DeviceSection, { device, strings }),
1334
1440
  errors.length > 0 && /* @__PURE__ */ jsx2(ErrorsSection, { errors, strings }),
1335
1441
  consoleLogs.length > 0 && /* @__PURE__ */ jsx2(ConsoleSection, { logs: consoleLogs, strings }),
@@ -1354,9 +1460,9 @@ function DeviceSection({
1354
1460
  if (device?.timezone) parts.push({ label: strings["detail.tech.device.timezone"], value: device.timezone });
1355
1461
  if (device?.connection) parts.push({ label: strings["detail.tech.device.connection"], value: device.connection });
1356
1462
  if (parts.length === 0) return null;
1357
- return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1463
+ return /* @__PURE__ */ jsxs3("div", { class: "tech-section", children: [
1358
1464
  /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.device"] }),
1359
- /* @__PURE__ */ jsx2("dl", { class: "tech-device", children: parts.map((p) => /* @__PURE__ */ jsxs2(Fragment, { children: [
1465
+ /* @__PURE__ */ jsx2("dl", { class: "tech-device", children: parts.map((p) => /* @__PURE__ */ jsxs3(Fragment, { children: [
1360
1466
  /* @__PURE__ */ jsx2("dt", { children: p.label }),
1361
1467
  /* @__PURE__ */ jsx2("dd", { children: p.value })
1362
1468
  ] })) })
@@ -1366,9 +1472,9 @@ function ErrorsSection({
1366
1472
  errors,
1367
1473
  strings
1368
1474
  }) {
1369
- return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1475
+ return /* @__PURE__ */ jsxs3("div", { class: "tech-section", children: [
1370
1476
  /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.errors"] }),
1371
- /* @__PURE__ */ jsx2("ul", { class: "tech-errors", children: errors.map((e) => /* @__PURE__ */ jsxs2("li", { children: [
1477
+ /* @__PURE__ */ jsx2("ul", { class: "tech-errors", children: errors.map((e) => /* @__PURE__ */ jsxs3("li", { children: [
1372
1478
  /* @__PURE__ */ jsx2("div", { class: "tech-errors-msg", children: e.message }),
1373
1479
  e.stack && /* @__PURE__ */ jsx2("pre", { class: "tech-errors-stack", children: truncateStack(e.stack) })
1374
1480
  ] })) })
@@ -1378,9 +1484,9 @@ function ConsoleSection({
1378
1484
  logs,
1379
1485
  strings
1380
1486
  }) {
1381
- return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1487
+ return /* @__PURE__ */ jsxs3("div", { class: "tech-section", children: [
1382
1488
  /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.console"] }),
1383
- /* @__PURE__ */ jsx2("ul", { class: "tech-console", children: logs.map((l) => /* @__PURE__ */ jsxs2("li", { class: `tech-console-row tech-console-row--${l.level}`, children: [
1489
+ /* @__PURE__ */ jsx2("ul", { class: "tech-console", children: logs.map((l) => /* @__PURE__ */ jsxs3("li", { class: `tech-console-row tech-console-row--${l.level}`, children: [
1384
1490
  /* @__PURE__ */ jsx2("span", { class: "tech-console-level", children: l.level }),
1385
1491
  /* @__PURE__ */ jsx2("span", { class: "tech-console-msg", children: l.message })
1386
1492
  ] })) })
@@ -1390,15 +1496,15 @@ function NetworkSection({
1390
1496
  rows,
1391
1497
  strings
1392
1498
  }) {
1393
- return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1499
+ return /* @__PURE__ */ jsxs3("div", { class: "tech-section", children: [
1394
1500
  /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.network"] }),
1395
1501
  /* @__PURE__ */ jsx2("ul", { class: "tech-network", children: rows.map((n) => {
1396
1502
  const failed = n.status >= 400 || n.status === 0;
1397
- return /* @__PURE__ */ jsxs2("li", { class: `tech-network-row${failed ? " tech-network-row--fail" : ""}`, children: [
1503
+ return /* @__PURE__ */ jsxs3("li", { class: `tech-network-row${failed ? " tech-network-row--fail" : ""}`, children: [
1398
1504
  /* @__PURE__ */ jsx2("span", { class: "tech-network-status", children: n.status || "\u2014" }),
1399
1505
  /* @__PURE__ */ jsx2("span", { class: "tech-network-method", children: n.method }),
1400
1506
  /* @__PURE__ */ jsx2("span", { class: "tech-network-url", title: n.url, children: truncateUrl(n.url, 56) }),
1401
- /* @__PURE__ */ jsxs2("span", { class: "tech-network-time", children: [
1507
+ /* @__PURE__ */ jsxs3("span", { class: "tech-network-time", children: [
1402
1508
  Math.round(n.durationMs),
1403
1509
  "ms"
1404
1510
  ] })
@@ -1412,15 +1518,15 @@ function truncateStack(stack) {
1412
1518
  return lines.slice(0, 12).join("\n") + "\n \u2026";
1413
1519
  }
1414
1520
  function StatusHistorySection({ rows, strings }) {
1415
- return /* @__PURE__ */ jsxs2("div", { class: "report-detail-history", children: [
1521
+ return /* @__PURE__ */ jsxs3("div", { class: "report-detail-history", children: [
1416
1522
  /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.history"] }),
1417
1523
  /* @__PURE__ */ jsx2("ol", { class: "status-history", children: rows.map((r) => {
1418
1524
  const from = strings[`status.${r.from_status}`] ?? r.from_status;
1419
1525
  const to = strings[`status.${r.to_status}`] ?? r.to_status;
1420
1526
  const sourceKey = r.changed_by_source === "mcp" ? "detail.author.mcp" : r.changed_by_source === "system" ? "detail.author.system" : "detail.author.staff";
1421
- return /* @__PURE__ */ jsxs2("li", { class: "status-history-row", children: [
1527
+ return /* @__PURE__ */ jsxs3("li", { class: "status-history-row", children: [
1422
1528
  /* @__PURE__ */ jsx2("span", { class: "status-history-time", children: formatSubmittedAt(r.created_at) }),
1423
- /* @__PURE__ */ jsxs2("span", { class: "status-history-transition", children: [
1529
+ /* @__PURE__ */ jsxs3("span", { class: "status-history-transition", children: [
1424
1530
  /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${r.from_status}`, children: from }),
1425
1531
  /* @__PURE__ */ jsx2("span", { class: "status-history-arrow", "aria-hidden": "true", children: "\u2192" }),
1426
1532
  /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${r.to_status}`, children: to })
@@ -1432,7 +1538,7 @@ function StatusHistorySection({ rows, strings }) {
1432
1538
  }
1433
1539
 
1434
1540
  // src/widget/BoardView.tsx
1435
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
1541
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs4 } from "preact/jsx-runtime";
1436
1542
  var POLL_MS2 = 3e4;
1437
1543
  var SORT_OPTIONS = ["recent", "oldest", "updated", "severity", "status"];
1438
1544
  var STATUSES = [
@@ -1470,11 +1576,10 @@ function BoardView({
1470
1576
  if (!thisPage) return;
1471
1577
  return onLocationChange(() => setReloadTick((t) => t + 1));
1472
1578
  }, [thisPage]);
1473
- const fetchFilters = useMemo(
1474
- () => thisPage ? { ...filters, pagePath: currentPagePath(getCurrentPage !== void 0 ? { getCurrentPage } : {}) } : filters,
1475
- // eslint-disable-next-line react-hooks/exhaustive-deps
1476
- [filtersHash(filters), thisPage, reloadTick]
1477
- );
1579
+ const fetchFilters = useMemo(() => {
1580
+ const searching = !!filters.q?.trim();
1581
+ return thisPage && !searching ? { ...filters, pagePath: currentPagePath(getCurrentPage !== void 0 ? { getCurrentPage } : {}) } : filters;
1582
+ }, [filtersHash(filters), thisPage, reloadTick]);
1478
1583
  useEffect2(() => {
1479
1584
  let cancelled = false;
1480
1585
  setLoading(true);
@@ -1491,7 +1596,7 @@ function BoardView({
1491
1596
  return true;
1492
1597
  } catch (e) {
1493
1598
  if (cancelled) return false;
1494
- setError(e instanceof Error ? e.message : String(e));
1599
+ setError(rateLimitMessage(e, strings) ?? strings["board.list.error"]);
1495
1600
  return false;
1496
1601
  } finally {
1497
1602
  if (!cancelled) setLoading(false);
@@ -1510,7 +1615,7 @@ function BoardView({
1510
1615
  setSelectedId(row.id);
1511
1616
  setDetailKey((k) => k + 1);
1512
1617
  };
1513
- return /* @__PURE__ */ jsxs3("div", { class: "board-view", children: [
1618
+ return /* @__PURE__ */ jsxs4("div", { class: "board-view", children: [
1514
1619
  /* @__PURE__ */ jsx3(BoardHeader, { strings, kpis, thisPage }),
1515
1620
  /* @__PURE__ */ jsx3(
1516
1621
  BoardFilters,
@@ -1523,10 +1628,10 @@ function BoardView({
1523
1628
  onToggleThisPage: () => setThisPage((v) => !v)
1524
1629
  }
1525
1630
  ),
1526
- /* @__PURE__ */ jsxs3("div", { class: "board-body", children: [
1527
- /* @__PURE__ */ jsxs3("div", { class: "board-list-wrap", "aria-busy": loading && !page, children: [
1528
- error && /* @__PURE__ */ jsxs3("div", { class: "board-error", role: "alert", children: [
1529
- /* @__PURE__ */ jsx3("span", { children: strings["board.list.error"] }),
1631
+ /* @__PURE__ */ jsxs4("div", { class: "board-body", children: [
1632
+ /* @__PURE__ */ jsxs4("div", { class: "board-list-wrap", "aria-busy": loading && !page, children: [
1633
+ error && /* @__PURE__ */ jsxs4("div", { class: "board-error", role: "alert", children: [
1634
+ /* @__PURE__ */ jsx3("span", { children: error }),
1530
1635
  /* @__PURE__ */ jsx3(
1531
1636
  "button",
1532
1637
  {
@@ -1571,10 +1676,17 @@ function BoardView({
1571
1676
  strings,
1572
1677
  onBack: () => setSelectedId(null),
1573
1678
  canModerate: selectedRow.can_moderate ?? selectedRow.is_mine,
1574
- variant: "board"
1679
+ variant: "board",
1680
+ onOpenSeq: (seq) => {
1681
+ void api.getReportBySeq(seq, externalId).then((r) => {
1682
+ setSelectedId(r.id);
1683
+ setDetailKey((k) => k + 1);
1684
+ }).catch(() => {
1685
+ });
1686
+ }
1575
1687
  },
1576
1688
  detailKey
1577
- ) : /* @__PURE__ */ jsxs3("div", { class: "board-detail-empty", children: [
1689
+ ) : /* @__PURE__ */ jsxs4("div", { class: "board-detail-empty", children: [
1578
1690
  /* @__PURE__ */ jsx3(DetailEmptyIllustration, {}),
1579
1691
  /* @__PURE__ */ jsx3("p", { children: strings["board.detail.empty"] })
1580
1692
  ] }) })
@@ -1587,10 +1699,10 @@ function BoardHeader({
1587
1699
  thisPage
1588
1700
  }) {
1589
1701
  const scopeLabel = thisPage ? strings["board.scope.onThisPage"] : kpis?.scope === "project" ? strings["board.scope.project"] : strings["board.scope.mine"];
1590
- return /* @__PURE__ */ jsxs3("header", { class: "board-header", children: [
1591
- /* @__PURE__ */ jsxs3("div", { class: "board-header-title", children: [
1702
+ return /* @__PURE__ */ jsxs4("header", { class: "board-header", children: [
1703
+ /* @__PURE__ */ jsxs4("div", { class: "board-header-title", children: [
1592
1704
  /* @__PURE__ */ jsx3("span", { class: "board-header-emoji", "aria-hidden": "true", children: "\u{1F4CB}" }),
1593
- /* @__PURE__ */ jsxs3("div", { children: [
1705
+ /* @__PURE__ */ jsxs4("div", { children: [
1594
1706
  /* @__PURE__ */ jsx3("h2", { class: "board-header-h", children: strings["tab.board"] }),
1595
1707
  /* @__PURE__ */ jsx3("p", { class: "board-header-sub", children: kpis ? `${kpis.total} \xB7 ${scopeLabel}` : scopeLabel })
1596
1708
  ] })
@@ -1628,7 +1740,7 @@ function BoardKpiStrip({
1628
1740
  tone: "tone-rate"
1629
1741
  }
1630
1742
  ];
1631
- return /* @__PURE__ */ jsx3("div", { class: `board-kpi-strip ${kpis ? "is-ready" : "is-loading"}`, "aria-live": "polite", children: cells.map((c) => /* @__PURE__ */ jsxs3("div", { class: `board-kpi-card ${c.tone}`, children: [
1743
+ return /* @__PURE__ */ jsx3("div", { class: `board-kpi-strip ${kpis ? "is-ready" : "is-loading"}`, "aria-live": "polite", children: cells.map((c) => /* @__PURE__ */ jsxs4("div", { class: `board-kpi-card ${c.tone}`, children: [
1632
1744
  /* @__PURE__ */ jsx3("div", { class: "board-kpi-value", children: c.value }),
1633
1745
  /* @__PURE__ */ jsx3("div", { class: "board-kpi-label", children: c.label })
1634
1746
  ] }, c.key)) });
@@ -1680,8 +1792,8 @@ function BoardFilters({
1680
1792
  }
1681
1793
  };
1682
1794
  const clear = () => onChange({});
1683
- return /* @__PURE__ */ jsxs3("div", { class: "board-filters", children: [
1684
- /* @__PURE__ */ jsxs3("div", { class: "board-scope", role: "group", "aria-label": strings["board.scope.thisPage"], children: [
1795
+ return /* @__PURE__ */ jsxs4("div", { class: "board-filters", children: [
1796
+ /* @__PURE__ */ jsxs4("div", { class: "board-scope", role: "group", "aria-label": strings["board.scope.thisPage"], children: [
1685
1797
  /* @__PURE__ */ jsx3(
1686
1798
  "button",
1687
1799
  {
@@ -1713,7 +1825,7 @@ function BoardFilters({
1713
1825
  children: SORT_OPTIONS.map((o) => /* @__PURE__ */ jsx3("option", { value: o, children: strings[`board.sort.${o}`] }, o))
1714
1826
  }
1715
1827
  ),
1716
- /* @__PURE__ */ jsxs3(
1828
+ /* @__PURE__ */ jsxs4(
1717
1829
  "select",
1718
1830
  {
1719
1831
  class: "board-filter-select",
@@ -1726,7 +1838,7 @@ function BoardFilters({
1726
1838
  ]
1727
1839
  }
1728
1840
  ),
1729
- /* @__PURE__ */ jsxs3(
1841
+ /* @__PURE__ */ jsxs4(
1730
1842
  "select",
1731
1843
  {
1732
1844
  class: "board-filter-select",
@@ -1739,7 +1851,7 @@ function BoardFilters({
1739
1851
  ]
1740
1852
  }
1741
1853
  ),
1742
- /* @__PURE__ */ jsxs3(
1854
+ /* @__PURE__ */ jsxs4(
1743
1855
  "select",
1744
1856
  {
1745
1857
  class: "board-filter-select",
@@ -1762,7 +1874,7 @@ function BoardFilters({
1762
1874
  onInput: (e) => setSearch(e.target.value)
1763
1875
  }
1764
1876
  ),
1765
- /* @__PURE__ */ jsxs3("label", { class: "board-filter-toggle", children: [
1877
+ /* @__PURE__ */ jsxs4("label", { class: "board-filter-toggle", children: [
1766
1878
  /* @__PURE__ */ jsx3(
1767
1879
  "input",
1768
1880
  {
@@ -1773,7 +1885,7 @@ function BoardFilters({
1773
1885
  ),
1774
1886
  /* @__PURE__ */ jsx3("span", { children: strings["board.filter.mine"] })
1775
1887
  ] }),
1776
- activeCount > 0 && /* @__PURE__ */ jsxs3("button", { type: "button", class: "board-filter-clear", onClick: clear, children: [
1888
+ activeCount > 0 && /* @__PURE__ */ jsxs4("button", { type: "button", class: "board-filter-clear", onClick: clear, children: [
1777
1889
  /* @__PURE__ */ jsx3(CloseIcon, {}),
1778
1890
  strings["board.filter.clear"]
1779
1891
  ] })
@@ -1787,7 +1899,7 @@ function BoardList({
1787
1899
  total,
1788
1900
  thisPage
1789
1901
  }) {
1790
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
1902
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1791
1903
  /* @__PURE__ */ jsx3("div", { class: "board-list-count", children: strings[thisPage ? "board.list.count.page" : "board.list.count"].replace("{n}", String(rows.length)).replace("{total}", String(total)) }),
1792
1904
  /* @__PURE__ */ jsx3("ul", { class: "board-list", role: "list", children: rows.map((row) => /* @__PURE__ */ jsx3(
1793
1905
  BoardRowCard,
@@ -1808,7 +1920,7 @@ function BoardRowCard({
1808
1920
  strings
1809
1921
  }) {
1810
1922
  const author = row.is_mine ? strings["board.list.you"] : row.author_label || "\u2014";
1811
- return /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs3(
1923
+ return /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs4(
1812
1924
  "button",
1813
1925
  {
1814
1926
  type: "button",
@@ -1816,14 +1928,14 @@ function BoardRowCard({
1816
1928
  onClick: () => onPick(row),
1817
1929
  "aria-pressed": selected,
1818
1930
  children: [
1819
- /* @__PURE__ */ jsxs3("div", { class: "board-row-badges", children: [
1931
+ /* @__PURE__ */ jsxs4("div", { class: "board-row-badges", children: [
1820
1932
  /* @__PURE__ */ jsx3("span", { class: `badge status-${row.status}`, children: strings[`status.${row.status}`] ?? row.status }),
1821
1933
  /* @__PURE__ */ jsx3("span", { class: `badge severity-${row.severity}`, children: strings[`severity.${row.severity}`] ?? row.severity })
1822
1934
  ] }),
1823
1935
  /* @__PURE__ */ jsx3("div", { class: "board-row-description", children: row.description }),
1824
- /* @__PURE__ */ jsxs3("div", { class: "board-row-meta", children: [
1936
+ /* @__PURE__ */ jsxs4("div", { class: "board-row-meta", children: [
1825
1937
  /* @__PURE__ */ jsx3("span", { class: "board-row-author", children: author }),
1826
- row.comment_count > 0 && /* @__PURE__ */ jsxs3("span", { class: "board-row-comments", children: [
1938
+ row.comment_count > 0 && /* @__PURE__ */ jsxs4("span", { class: "board-row-comments", children: [
1827
1939
  /* @__PURE__ */ jsx3(CommentIcon, {}),
1828
1940
  " ",
1829
1941
  row.comment_count
@@ -1839,7 +1951,7 @@ function BoardEmpty({
1839
1951
  thisPage,
1840
1952
  onAllPages
1841
1953
  }) {
1842
- return /* @__PURE__ */ jsxs3("div", { class: "board-empty", children: [
1954
+ return /* @__PURE__ */ jsxs4("div", { class: "board-empty", children: [
1843
1955
  /* @__PURE__ */ jsx3(EmptyIllustration, {}),
1844
1956
  /* @__PURE__ */ jsx3("h3", { children: strings[thisPage ? "board.list.empty.page.title" : "board.list.empty.title"] }),
1845
1957
  /* @__PURE__ */ jsx3("p", { children: strings[thisPage ? "board.list.empty.page.description" : "board.list.empty.description"] }),
@@ -1847,7 +1959,7 @@ function BoardEmpty({
1847
1959
  ] });
1848
1960
  }
1849
1961
  function BoardListSkeleton() {
1850
- return /* @__PURE__ */ jsx3("ul", { class: "board-list board-list-skeleton", "aria-hidden": "true", children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs3("div", { class: "board-row skeleton-row", children: [
1962
+ return /* @__PURE__ */ jsx3("ul", { class: "board-list board-list-skeleton", "aria-hidden": "true", children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs4("div", { class: "board-row skeleton-row", children: [
1851
1963
  /* @__PURE__ */ jsx3("div", { class: "skeleton-line skeleton-badges" }),
1852
1964
  /* @__PURE__ */ jsx3("div", { class: "skeleton-line skeleton-text" }),
1853
1965
  /* @__PURE__ */ jsx3("div", { class: "skeleton-line skeleton-text short" })
@@ -1857,19 +1969,19 @@ function CommentIcon() {
1857
1969
  return /* @__PURE__ */ jsx3("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) });
1858
1970
  }
1859
1971
  function CloseIcon() {
1860
- return /* @__PURE__ */ jsxs3("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
1972
+ return /* @__PURE__ */ jsxs4("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
1861
1973
  /* @__PURE__ */ jsx3("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1862
1974
  /* @__PURE__ */ jsx3("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1863
1975
  ] });
1864
1976
  }
1865
1977
  function EmptyIllustration() {
1866
- return /* @__PURE__ */ jsxs3("svg", { width: "64", height: "64", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [
1978
+ return /* @__PURE__ */ jsxs4("svg", { width: "64", height: "64", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [
1867
1979
  /* @__PURE__ */ jsx3("circle", { cx: "32", cy: "32", r: "28", stroke: "currentColor", "stroke-opacity": "0.18", "stroke-width": "2" }),
1868
1980
  /* @__PURE__ */ jsx3("path", { d: "M22 32h20M32 22v20", stroke: "currentColor", "stroke-opacity": "0.35", "stroke-width": "2", "stroke-linecap": "round" })
1869
1981
  ] });
1870
1982
  }
1871
1983
  function DetailEmptyIllustration() {
1872
- return /* @__PURE__ */ jsxs3("svg", { width: "80", height: "80", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [
1984
+ return /* @__PURE__ */ jsxs4("svg", { width: "80", height: "80", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [
1873
1985
  /* @__PURE__ */ jsx3("rect", { x: "10", y: "12", width: "44", height: "36", rx: "4", stroke: "currentColor", "stroke-opacity": "0.22", "stroke-width": "2" }),
1874
1986
  /* @__PURE__ */ jsx3("line", { x1: "18", y1: "22", x2: "46", y2: "22", stroke: "currentColor", "stroke-opacity": "0.22", "stroke-width": "2", "stroke-linecap": "round" }),
1875
1987
  /* @__PURE__ */ jsx3("line", { x1: "18", y1: "30", x2: "38", y2: "30", stroke: "currentColor", "stroke-opacity": "0.18", "stroke-width": "2", "stroke-linecap": "round" }),
@@ -1905,7 +2017,7 @@ function formatRelative(iso) {
1905
2017
  import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "preact/hooks";
1906
2018
 
1907
2019
  // src/widget/ReportRow.tsx
1908
- import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
2020
+ import { jsx as jsx4, jsxs as jsxs5 } from "preact/jsx-runtime";
1909
2021
  function statusClassName(status) {
1910
2022
  return `pill pill-status pill-status--${status}`;
1911
2023
  }
@@ -1933,16 +2045,16 @@ function repliesLabel(count, strings) {
1933
2045
  }
1934
2046
  function ReportRow({ row, strings, onClick }) {
1935
2047
  const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
1936
- return /* @__PURE__ */ jsxs4("button", { type: "button", class: "mine-row", onClick, children: [
1937
- /* @__PURE__ */ jsxs4("div", { class: "mine-row-pills", children: [
2048
+ return /* @__PURE__ */ jsxs5("button", { type: "button", class: "mine-row", onClick, children: [
2049
+ /* @__PURE__ */ jsxs5("div", { class: "mine-row-pills", children: [
1938
2050
  /* @__PURE__ */ jsx4("span", { class: statusClassName(row.status), children: strings[`status.${row.status}`] ?? row.status }),
1939
2051
  /* @__PURE__ */ jsx4("span", { class: typeClassName(), children: strings[`type.${row.feedback_type}`] }),
1940
2052
  /* @__PURE__ */ jsx4("span", { class: severityClassName(row.severity), children: strings[`severity.${row.severity}`] })
1941
2053
  ] }),
1942
2054
  /* @__PURE__ */ jsx4("div", { class: "mine-row-preview", children: preview }),
1943
- /* @__PURE__ */ jsxs4("div", { class: "mine-row-meta", children: [
2055
+ /* @__PURE__ */ jsxs5("div", { class: "mine-row-meta", children: [
1944
2056
  /* @__PURE__ */ jsx4("span", { children: formatRelative2(row.updated_at || row.created_at) }),
1945
- row.comment_count > 0 && /* @__PURE__ */ jsxs4("span", { children: [
2057
+ row.comment_count > 0 && /* @__PURE__ */ jsxs5("span", { children: [
1946
2058
  "\xB7 ",
1947
2059
  repliesLabel(row.comment_count, strings)
1948
2060
  ] })
@@ -1951,7 +2063,7 @@ function ReportRow({ row, strings, onClick }) {
1951
2063
  }
1952
2064
 
1953
2065
  // src/widget/ChangelogList.tsx
1954
- import { jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
2066
+ import { jsx as jsx5, jsxs as jsxs6 } from "preact/jsx-runtime";
1955
2067
  var POLL_MS3 = 3e4;
1956
2068
  function isoWeekKey(iso) {
1957
2069
  const d = new Date(iso);
@@ -2020,8 +2132,8 @@ function ChangelogList({ api, externalId, strings, onSelect }) {
2020
2132
  const groups = useMemo2(() => rows ? groupRowsByWeek(rows) : [], [rows]);
2021
2133
  const isLoading = rows === null && !error;
2022
2134
  const isEmpty = rows !== null && rows.length === 0;
2023
- return /* @__PURE__ */ jsxs5("div", { class: "mine-list", children: [
2024
- /* @__PURE__ */ jsxs5("div", { class: "mine-list-header", children: [
2135
+ return /* @__PURE__ */ jsxs6("div", { class: "mine-list", children: [
2136
+ /* @__PURE__ */ jsxs6("div", { class: "mine-list-header", children: [
2025
2137
  /* @__PURE__ */ jsx5("h2", { children: strings["tab.changelog"] }),
2026
2138
  /* @__PURE__ */ jsx5(
2027
2139
  "button",
@@ -2038,12 +2150,12 @@ function ChangelogList({ api, externalId, strings, onSelect }) {
2038
2150
  ] }),
2039
2151
  isLoading && /* @__PURE__ */ jsx5("div", { class: "mine-loading", children: strings["mine.loading"] }),
2040
2152
  error && /* @__PURE__ */ jsx5("div", { class: "error", children: error }),
2041
- isEmpty && /* @__PURE__ */ jsxs5("div", { class: "mine-empty", children: [
2153
+ isEmpty && /* @__PURE__ */ jsxs6("div", { class: "mine-empty", children: [
2042
2154
  /* @__PURE__ */ jsx5("strong", { children: strings["changelog.empty.title"] }),
2043
2155
  /* @__PURE__ */ jsx5("p", { children: strings["changelog.empty.body"] })
2044
2156
  ] }),
2045
- groups.length > 0 && /* @__PURE__ */ jsx5("div", { class: "changelog-groups", children: groups.map((g) => /* @__PURE__ */ jsxs5("section", { class: "changelog-group", children: [
2046
- /* @__PURE__ */ jsxs5("header", { class: "changelog-group-header", children: [
2157
+ groups.length > 0 && /* @__PURE__ */ jsx5("div", { class: "changelog-groups", children: groups.map((g) => /* @__PURE__ */ jsxs6("section", { class: "changelog-group", children: [
2158
+ /* @__PURE__ */ jsxs6("header", { class: "changelog-group-header", children: [
2047
2159
  /* @__PURE__ */ jsx5("span", { class: "changelog-group-marker", "aria-hidden": "true", children: "\u25CF" }),
2048
2160
  /* @__PURE__ */ jsx5("span", { class: "changelog-group-label", children: strings["changelog.week_of"].replace("{date}", g.label) }),
2049
2161
  /* @__PURE__ */ jsx5("span", { class: "changelog-group-rule", "aria-hidden": "true" }),
@@ -2055,9 +2167,9 @@ function ChangelogList({ api, externalId, strings, onSelect }) {
2055
2167
  }
2056
2168
 
2057
2169
  // src/widget/Fab.tsx
2058
- import { jsx as jsx6, jsxs as jsxs6 } from "preact/jsx-runtime";
2170
+ import { jsx as jsx6, jsxs as jsxs7 } from "preact/jsx-runtime";
2059
2171
  function BugIcon() {
2060
- return /* @__PURE__ */ jsxs6(
2172
+ return /* @__PURE__ */ jsxs7(
2061
2173
  "svg",
2062
2174
  {
2063
2175
  width: "20",
@@ -2087,7 +2199,7 @@ function BugIcon() {
2087
2199
  );
2088
2200
  }
2089
2201
  function Fab({ label, onClick, count }) {
2090
- return /* @__PURE__ */ jsxs6("button", { type: "button", class: "fab", "aria-label": label, title: label, onClick, children: [
2202
+ return /* @__PURE__ */ jsxs7("button", { type: "button", class: "fab", "aria-label": label, title: label, onClick, children: [
2091
2203
  /* @__PURE__ */ jsx6(BugIcon, {}),
2092
2204
  count !== void 0 && count > 0 && /* @__PURE__ */ jsx6("span", { class: "fab-badge", "aria-hidden": "true", children: count > 9 ? "9+" : String(count) })
2093
2205
  ] });
@@ -2098,7 +2210,7 @@ import { useEffect as useEffect5, useRef as useRef4, useState as useState5 } fro
2098
2210
 
2099
2211
  // src/widget/Annotator.tsx
2100
2212
  import { useEffect as useEffect4, useLayoutEffect, useRef as useRef3, useState as useState4 } from "preact/hooks";
2101
- import { jsx as jsx7, jsxs as jsxs7 } from "preact/jsx-runtime";
2213
+ import { jsx as jsx7, jsxs as jsxs8 } from "preact/jsx-runtime";
2102
2214
  var COLORS = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#ffffff"];
2103
2215
  var HIGHLIGHT_ALPHA = 0.35;
2104
2216
  function drawShape(ctx, shape, sourceImage) {
@@ -2224,11 +2336,11 @@ var Icon = {
2224
2336
  arrow: /* @__PURE__ */ jsx7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("path", { d: "M2 8h11M9 4l4 4-4 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
2225
2337
  pencil: /* @__PURE__ */ jsx7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("path", { d: "M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linejoin": "round" }) }),
2226
2338
  text: /* @__PURE__ */ jsx7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("path", { d: "M3 3h10M8 3v10M5 13h6", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) }),
2227
- highlight: /* @__PURE__ */ jsxs7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
2339
+ highlight: /* @__PURE__ */ jsxs8("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
2228
2340
  /* @__PURE__ */ jsx7("path", { d: "M3 13l3-3L11 5l-2-2-5 5-3 3v2h2zM10 4l2 2", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }),
2229
2341
  /* @__PURE__ */ jsx7("path", { d: "M2 14h12", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" })
2230
2342
  ] }),
2231
- blur: /* @__PURE__ */ jsxs7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
2343
+ blur: /* @__PURE__ */ jsxs8("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
2232
2344
  /* @__PURE__ */ jsx7("rect", { x: "2", y: "2", width: "4", height: "4", fill: "currentColor", opacity: "0.85" }),
2233
2345
  /* @__PURE__ */ jsx7("rect", { x: "7", y: "2", width: "3", height: "3", fill: "currentColor", opacity: "0.55" }),
2234
2346
  /* @__PURE__ */ jsx7("rect", { x: "11", y: "2", width: "3", height: "4", fill: "currentColor", opacity: "0.4" }),
@@ -2484,8 +2596,8 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2484
2596
  onClick: (e) => {
2485
2597
  if (e.target === e.currentTarget) onCancel();
2486
2598
  },
2487
- children: /* @__PURE__ */ jsxs7("div", { class: "annotator", role: "dialog", "aria-modal": "true", "aria-label": strings["annotator.title"], children: [
2488
- /* @__PURE__ */ jsxs7("div", { class: "annotator-header", children: [
2599
+ children: /* @__PURE__ */ jsxs8("div", { class: "annotator", role: "dialog", "aria-modal": "true", "aria-label": strings["annotator.title"], children: [
2600
+ /* @__PURE__ */ jsxs8("div", { class: "annotator-header", children: [
2489
2601
  /* @__PURE__ */ jsx7("span", { children: strings["annotator.title"] }),
2490
2602
  /* @__PURE__ */ jsx7(
2491
2603
  "button",
@@ -2498,7 +2610,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2498
2610
  }
2499
2611
  )
2500
2612
  ] }),
2501
- /* @__PURE__ */ jsxs7("div", { class: "annotator-toolbar", children: [
2613
+ /* @__PURE__ */ jsxs8("div", { class: "annotator-toolbar", children: [
2502
2614
  /* @__PURE__ */ jsx7("div", { class: "annotator-tools", children: tools.map((t) => /* @__PURE__ */ jsx7(
2503
2615
  "button",
2504
2616
  {
@@ -2513,7 +2625,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2513
2625
  t.id
2514
2626
  )) }),
2515
2627
  /* @__PURE__ */ jsx7("span", { class: "annotator-sep" }),
2516
- /* @__PURE__ */ jsxs7("div", { class: "annotator-colors", children: [
2628
+ /* @__PURE__ */ jsxs8("div", { class: "annotator-colors", children: [
2517
2629
  COLORS.map((c) => /* @__PURE__ */ jsx7(
2518
2630
  "button",
2519
2631
  {
@@ -2537,7 +2649,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2537
2649
  ) })
2538
2650
  ] }),
2539
2651
  /* @__PURE__ */ jsx7("span", { class: "annotator-sep" }),
2540
- /* @__PURE__ */ jsxs7(
2652
+ /* @__PURE__ */ jsxs8(
2541
2653
  "button",
2542
2654
  {
2543
2655
  type: "button",
@@ -2551,7 +2663,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2551
2663
  ]
2552
2664
  }
2553
2665
  ),
2554
- /* @__PURE__ */ jsxs7(
2666
+ /* @__PURE__ */ jsxs8(
2555
2667
  "button",
2556
2668
  {
2557
2669
  type: "button",
@@ -2565,7 +2677,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2565
2677
  ]
2566
2678
  }
2567
2679
  ),
2568
- /* @__PURE__ */ jsxs7(
2680
+ /* @__PURE__ */ jsxs8(
2569
2681
  "button",
2570
2682
  {
2571
2683
  type: "button",
@@ -2579,7 +2691,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2579
2691
  }
2580
2692
  ),
2581
2693
  /* @__PURE__ */ jsx7("span", { class: "annotator-spacer" }),
2582
- /* @__PURE__ */ jsxs7("span", { class: "annotator-count", children: [
2694
+ /* @__PURE__ */ jsxs8("span", { class: "annotator-count", children: [
2583
2695
  shapes.length,
2584
2696
  " ",
2585
2697
  strings["annotator.count_suffix"]
@@ -2596,7 +2708,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2596
2708
  class: "annotator-canvas"
2597
2709
  }
2598
2710
  ) }),
2599
- /* @__PURE__ */ jsxs7("div", { class: "annotator-footer", children: [
2711
+ /* @__PURE__ */ jsxs8("div", { class: "annotator-footer", children: [
2600
2712
  /* @__PURE__ */ jsx7("button", { type: "button", class: "btn", onClick: onCancel, children: strings["form.cancel"] }),
2601
2713
  /* @__PURE__ */ jsx7(
2602
2714
  "button",
@@ -2615,10 +2727,18 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2615
2727
  }
2616
2728
 
2617
2729
  // src/widget/Form.tsx
2618
- import { jsx as jsx8, jsxs as jsxs8 } from "preact/jsx-runtime";
2730
+ import { jsx as jsx8, jsxs as jsxs9 } from "preact/jsx-runtime";
2619
2731
  var TYPES2 = ["bug", "feature", "question", "praise", "typo"];
2620
2732
  var SEVERITIES2 = ["blocker", "high", "medium", "low"];
2621
- function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2733
+ function Form({
2734
+ strings,
2735
+ onSubmit,
2736
+ onCancel,
2737
+ status,
2738
+ errorMessage,
2739
+ submittedSeq,
2740
+ onDirtyChange
2741
+ }) {
2622
2742
  const [description, setDescription] = useState5("");
2623
2743
  const [feedbackType, setFeedbackType] = useState5("bug");
2624
2744
  const [severity, setSeverity] = useState5("medium");
@@ -2631,6 +2751,10 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2631
2751
  const submitting = status === "submitting";
2632
2752
  const submitLabel = submitting ? strings["form.submitting"] : strings["form.submit"];
2633
2753
  const pageUrl = typeof window !== "undefined" ? window.location.href : "";
2754
+ const isDirty = description.trim() !== "" || shots.length > 0;
2755
+ useEffect5(() => {
2756
+ onDirtyChange?.(isDirty);
2757
+ }, [isDirty]);
2634
2758
  const shotsRef = useRef4(shots);
2635
2759
  shotsRef.current = shots;
2636
2760
  useEffect5(() => {
@@ -2742,9 +2866,9 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2742
2866
  }
2743
2867
  onSubmit(values);
2744
2868
  };
2745
- return /* @__PURE__ */ jsxs8("form", { onSubmit: handleSubmit, children: [
2869
+ return /* @__PURE__ */ jsxs9("form", { onSubmit: handleSubmit, children: [
2746
2870
  /* @__PURE__ */ jsx8("h2", { children: strings["form.title"] }),
2747
- /* @__PURE__ */ jsxs8("div", { class: "field", children: [
2871
+ /* @__PURE__ */ jsxs9("div", { class: "field", children: [
2748
2872
  /* @__PURE__ */ jsx8("label", { for: "mfb-desc", children: strings["form.description.label"] }),
2749
2873
  /* @__PURE__ */ jsx8(
2750
2874
  "textarea",
@@ -2756,8 +2880,8 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2756
2880
  }
2757
2881
  )
2758
2882
  ] }),
2759
- /* @__PURE__ */ jsxs8("div", { class: "row", children: [
2760
- /* @__PURE__ */ jsxs8("div", { class: "field", children: [
2883
+ /* @__PURE__ */ jsxs9("div", { class: "row", children: [
2884
+ /* @__PURE__ */ jsxs9("div", { class: "field", children: [
2761
2885
  /* @__PURE__ */ jsx8("label", { for: "mfb-type", children: strings["form.type.label"] }),
2762
2886
  /* @__PURE__ */ jsx8(
2763
2887
  "select",
@@ -2769,7 +2893,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2769
2893
  }
2770
2894
  )
2771
2895
  ] }),
2772
- /* @__PURE__ */ jsxs8("div", { class: "field", children: [
2896
+ /* @__PURE__ */ jsxs9("div", { class: "field", children: [
2773
2897
  /* @__PURE__ */ jsx8("label", { for: "mfb-sev", children: strings["form.severity.label"] }),
2774
2898
  /* @__PURE__ */ jsx8(
2775
2899
  "select",
@@ -2782,7 +2906,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2782
2906
  )
2783
2907
  ] })
2784
2908
  ] }),
2785
- /* @__PURE__ */ jsxs8("div", { class: "field", children: [
2909
+ /* @__PURE__ */ jsxs9("div", { class: "field", children: [
2786
2910
  /* @__PURE__ */ jsx8("label", { children: strings["form.screenshot.label"] }),
2787
2911
  /* @__PURE__ */ jsx8(
2788
2912
  "input",
@@ -2797,17 +2921,17 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2797
2921
  tabIndex: -1
2798
2922
  }
2799
2923
  ),
2800
- shots.length > 0 && /* @__PURE__ */ jsx8("div", { class: "screenshot-strip", children: shots.map((shot, index) => /* @__PURE__ */ jsxs8("div", { class: "screenshot-preview", children: [
2924
+ shots.length > 0 && /* @__PURE__ */ jsx8("div", { class: "screenshot-strip", children: shots.map((shot, index) => /* @__PURE__ */ jsxs9("div", { class: "screenshot-preview", children: [
2801
2925
  /* @__PURE__ */ jsx8("img", { src: shot.preview, alt: "" }),
2802
- /* @__PURE__ */ jsxs8("div", { class: "screenshot-preview-actions", children: [
2803
- /* @__PURE__ */ jsxs8(
2926
+ /* @__PURE__ */ jsxs9("div", { class: "screenshot-preview-actions", children: [
2927
+ /* @__PURE__ */ jsxs9(
2804
2928
  "button",
2805
2929
  {
2806
2930
  type: "button",
2807
2931
  class: "btn btn--primary screenshot-annotate",
2808
2932
  onClick: () => setAnnotatingIndex(index),
2809
2933
  children: [
2810
- /* @__PURE__ */ jsxs8("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
2934
+ /* @__PURE__ */ jsxs9("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
2811
2935
  /* @__PURE__ */ jsx8("path", { d: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" }),
2812
2936
  /* @__PURE__ */ jsx8("path", { d: "M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" })
2813
2937
  ] }),
@@ -2815,13 +2939,13 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2815
2939
  ]
2816
2940
  }
2817
2941
  ),
2818
- /* @__PURE__ */ jsxs8("button", { type: "button", class: "btn", onClick: () => removeShot(index), children: [
2942
+ /* @__PURE__ */ jsxs9("button", { type: "button", class: "btn", onClick: () => removeShot(index), children: [
2819
2943
  /* @__PURE__ */ jsx8("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx8("path", { d: "M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" }) }),
2820
2944
  strings["form.screenshot.remove"]
2821
2945
  ] })
2822
2946
  ] })
2823
2947
  ] }, shot.preview)) }),
2824
- shots.length < MAX_SCREENSHOTS && /* @__PURE__ */ jsxs8(
2948
+ shots.length < MAX_SCREENSHOTS && /* @__PURE__ */ jsxs9(
2825
2949
  "div",
2826
2950
  {
2827
2951
  ref: dropZoneRef,
@@ -2840,12 +2964,12 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2840
2964
  onDragLeave: handleDragLeave,
2841
2965
  onDrop: handleDrop,
2842
2966
  children: [
2843
- /* @__PURE__ */ jsxs8("svg", { class: "screenshot-icon", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.6", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
2967
+ /* @__PURE__ */ jsxs9("svg", { class: "screenshot-icon", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.6", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
2844
2968
  /* @__PURE__ */ jsx8("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
2845
2969
  /* @__PURE__ */ jsx8("polyline", { points: "17 8 12 3 7 8" }),
2846
2970
  /* @__PURE__ */ jsx8("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
2847
2971
  ] }),
2848
- /* @__PURE__ */ jsxs8("div", { class: "screenshot-cta", children: [
2972
+ /* @__PURE__ */ jsxs9("div", { class: "screenshot-cta", children: [
2849
2973
  /* @__PURE__ */ jsx8("strong", { children: strings["form.screenshot.cta_click"] }),
2850
2974
  ", ",
2851
2975
  strings["form.screenshot.cta_rest"]
@@ -2855,14 +2979,15 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2855
2979
  }
2856
2980
  )
2857
2981
  ] }),
2858
- pageUrl && /* @__PURE__ */ jsxs8("div", { class: "page-context", title: pageUrl, children: [
2982
+ pageUrl && /* @__PURE__ */ jsxs9("div", { class: "page-context", title: pageUrl, children: [
2859
2983
  /* @__PURE__ */ jsx8("span", { class: "page-context-label", children: strings["form.context.label"] }),
2860
2984
  /* @__PURE__ */ jsx8("span", { class: "page-context-url", children: truncateUrl(pageUrl, 90) })
2861
2985
  ] }),
2986
+ /* @__PURE__ */ jsx8("div", { class: "capture-notice", children: strings["form.capture.notice"] }),
2862
2987
  localError && /* @__PURE__ */ jsx8("div", { class: "error", children: localError }),
2863
2988
  status === "error" && errorMessage && /* @__PURE__ */ jsx8("div", { class: "error", children: errorMessage }),
2864
- status === "success" && /* @__PURE__ */ jsx8("div", { class: "success", children: strings["form.success"] }),
2865
- /* @__PURE__ */ jsxs8("div", { class: "actions", children: [
2989
+ status === "success" && /* @__PURE__ */ jsx8("div", { class: "success", children: submittedSeq !== void 0 ? strings["form.success.seq"].replace("{seq}", String(submittedSeq)) : strings["form.success"] }),
2990
+ /* @__PURE__ */ jsxs9("div", { class: "actions", children: [
2866
2991
  /* @__PURE__ */ jsx8("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
2867
2992
  /* @__PURE__ */ jsx8("button", { type: "submit", class: "btn btn--primary", disabled: submitting, children: submitLabel })
2868
2993
  ] }),
@@ -2882,7 +3007,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2882
3007
  import { useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "preact/hooks";
2883
3008
 
2884
3009
  // src/widget/KpiStrip.tsx
2885
- import { jsx as jsx9, jsxs as jsxs9 } from "preact/jsx-runtime";
3010
+ import { jsx as jsx9, jsxs as jsxs10 } from "preact/jsx-runtime";
2886
3011
  function computeKpiCounts(rows) {
2887
3012
  const counts = {
2888
3013
  new: 0,
@@ -2952,7 +3077,7 @@ function KpiStrip({ rows, filter, onFilter, strings }) {
2952
3077
  return /* @__PURE__ */ jsx9("div", { class: "kpi-strip", role: "toolbar", children: cells.map((c) => {
2953
3078
  const active = filter === c.key;
2954
3079
  const toggleTo = active ? "all" : c.key;
2955
- return /* @__PURE__ */ jsxs9(
3080
+ return /* @__PURE__ */ jsxs10(
2956
3081
  "button",
2957
3082
  {
2958
3083
  type: "button",
@@ -2969,7 +3094,7 @@ function KpiStrip({ rows, filter, onFilter, strings }) {
2969
3094
  }
2970
3095
 
2971
3096
  // src/widget/MineList.tsx
2972
- import { jsx as jsx10, jsxs as jsxs10 } from "preact/jsx-runtime";
3097
+ import { jsx as jsx10, jsxs as jsxs11 } from "preact/jsx-runtime";
2973
3098
  var POLL_MS4 = 3e4;
2974
3099
  function MineList({ api, externalId, strings, onSelect }) {
2975
3100
  const [rows, setRows] = useState6(null);
@@ -2988,7 +3113,7 @@ function MineList({ api, externalId, strings, onSelect }) {
2988
3113
  } catch (err) {
2989
3114
  if (typeof console !== "undefined") console.warn("[mhosaic] listMine:", err);
2990
3115
  if (!mountedRef.current) return false;
2991
- setError(strings["mine.error"]);
3116
+ setError(rateLimitMessage(err, strings) ?? strings["mine.error"]);
2992
3117
  return false;
2993
3118
  } finally {
2994
3119
  if (mountedRef.current) setRefreshing(false);
@@ -3006,8 +3131,8 @@ function MineList({ api, externalId, strings, onSelect }) {
3006
3131
  const isLoading = rows === null && !error;
3007
3132
  const visibleRows = rows ? rowsMatchingFilter(rows, filter) : null;
3008
3133
  const visibleEmpty = !!rows && rows.length > 0 && (visibleRows?.length ?? 0) === 0;
3009
- return /* @__PURE__ */ jsxs10("div", { class: "mine-list", children: [
3010
- /* @__PURE__ */ jsxs10("div", { class: "mine-list-header", children: [
3134
+ return /* @__PURE__ */ jsxs11("div", { class: "mine-list", children: [
3135
+ /* @__PURE__ */ jsxs11("div", { class: "mine-list-header", children: [
3011
3136
  /* @__PURE__ */ jsx10("h2", { children: strings["tab.mine"] }),
3012
3137
  /* @__PURE__ */ jsx10(
3013
3138
  "button",
@@ -3025,7 +3150,7 @@ function MineList({ api, externalId, strings, onSelect }) {
3025
3150
  rows && rows.length > 0 && /* @__PURE__ */ jsx10(KpiStrip, { rows, filter, onFilter: setFilter, strings }),
3026
3151
  isLoading && /* @__PURE__ */ jsx10("div", { class: "mine-loading", children: strings["mine.loading"] }),
3027
3152
  error && /* @__PURE__ */ jsx10("div", { class: "error", children: error }),
3028
- isEmpty && /* @__PURE__ */ jsxs10("div", { class: "mine-empty", children: [
3153
+ isEmpty && /* @__PURE__ */ jsxs11("div", { class: "mine-empty", children: [
3029
3154
  /* @__PURE__ */ jsx10("strong", { children: strings["mine.empty.title"] }),
3030
3155
  /* @__PURE__ */ jsx10("p", { children: strings["mine.empty.body"] })
3031
3156
  ] }),
@@ -3036,7 +3161,7 @@ function MineList({ api, externalId, strings, onSelect }) {
3036
3161
 
3037
3162
  // src/widget/Modal.tsx
3038
3163
  import { useEffect as useEffect7, useRef as useRef6 } from "preact/hooks";
3039
- import { jsx as jsx11, jsxs as jsxs11 } from "preact/jsx-runtime";
3164
+ import { jsx as jsx11, jsxs as jsxs12 } from "preact/jsx-runtime";
3040
3165
  function Modal({ onDismiss, children, closeLabel = "Close", expanded = false }) {
3041
3166
  const modalRef = useRef6(null);
3042
3167
  const previouslyFocused = useRef6(null);
@@ -3049,7 +3174,7 @@ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false })
3049
3174
  return;
3050
3175
  }
3051
3176
  e.stopPropagation();
3052
- onDismiss();
3177
+ onDismiss("escape");
3053
3178
  };
3054
3179
  window.addEventListener("keydown", onKey);
3055
3180
  const first = modalRef.current?.querySelector(
@@ -3068,9 +3193,9 @@ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false })
3068
3193
  class: `backdrop ${expanded ? "is-expanded" : ""}`,
3069
3194
  role: "presentation",
3070
3195
  onClick: (e) => {
3071
- if (e.target === e.currentTarget) onDismiss();
3196
+ if (e.target === e.currentTarget) onDismiss("backdrop");
3072
3197
  },
3073
- children: /* @__PURE__ */ jsxs11(
3198
+ children: /* @__PURE__ */ jsxs12(
3074
3199
  "div",
3075
3200
  {
3076
3201
  ref: modalRef,
@@ -3084,7 +3209,7 @@ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false })
3084
3209
  type: "button",
3085
3210
  class: "modal-close",
3086
3211
  "aria-label": closeLabel,
3087
- onClick: onDismiss,
3212
+ onClick: () => onDismiss("button"),
3088
3213
  children: "\xD7"
3089
3214
  }
3090
3215
  ),
@@ -3097,7 +3222,7 @@ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false })
3097
3222
  }
3098
3223
 
3099
3224
  // src/widget/PageActivityStrip.tsx
3100
- import { jsx as jsx12, jsxs as jsxs12 } from "preact/jsx-runtime";
3225
+ import { jsx as jsx12, jsxs as jsxs13 } from "preact/jsx-runtime";
3101
3226
  function PageActivityStrip({
3102
3227
  open,
3103
3228
  strings,
@@ -3105,7 +3230,7 @@ function PageActivityStrip({
3105
3230
  }) {
3106
3231
  if (open === 0) return null;
3107
3232
  const text = open === 1 ? strings["page.strip.text.one"] : strings["page.strip.text"].replace("{n}", String(open));
3108
- return /* @__PURE__ */ jsxs12("div", { class: "page-strip", children: [
3233
+ return /* @__PURE__ */ jsxs13("div", { class: "page-strip", children: [
3109
3234
  /* @__PURE__ */ jsx12("span", { children: text }),
3110
3235
  /* @__PURE__ */ jsx12("button", { type: "button", class: "page-strip-view", onClick: onView, children: strings["page.strip.view"] })
3111
3236
  ] });
@@ -3113,7 +3238,7 @@ function PageActivityStrip({
3113
3238
 
3114
3239
  // src/widget/PagePeekPanel.tsx
3115
3240
  import { useEffect as useEffect8, useState as useState7 } from "preact/hooks";
3116
- import { jsx as jsx13, jsxs as jsxs13 } from "preact/jsx-runtime";
3241
+ import { jsx as jsx13, jsxs as jsxs14 } from "preact/jsx-runtime";
3117
3242
  function PagePeekPanel({
3118
3243
  api,
3119
3244
  externalId,
@@ -3141,14 +3266,14 @@ function PagePeekPanel({
3141
3266
  cancelled = true;
3142
3267
  };
3143
3268
  }, [api, externalId]);
3144
- return /* @__PURE__ */ jsxs13("div", { class: "page-peek", role: "region", "aria-label": strings["page.peek.title"], children: [
3269
+ return /* @__PURE__ */ jsxs14("div", { class: "page-peek", role: "region", "aria-label": strings["page.peek.title"], children: [
3145
3270
  /* @__PURE__ */ jsx13("div", { class: "page-peek-title", children: strings["page.peek.title"] }),
3146
- rows === null && !failed && /* @__PURE__ */ jsxs13("div", { class: "page-peek-skeleton", "aria-hidden": "true", children: [
3271
+ rows === null && !failed && /* @__PURE__ */ jsxs14("div", { class: "page-peek-skeleton", "aria-hidden": "true", children: [
3147
3272
  /* @__PURE__ */ jsx13("div", {}),
3148
3273
  " ",
3149
3274
  /* @__PURE__ */ jsx13("div", {})
3150
3275
  ] }),
3151
- rows !== null && rows.map((row) => /* @__PURE__ */ jsxs13(
3276
+ rows !== null && rows.map((row) => /* @__PURE__ */ jsxs14(
3152
3277
  "button",
3153
3278
  {
3154
3279
  type: "button",
@@ -3161,7 +3286,7 @@ function PagePeekPanel({
3161
3286
  },
3162
3287
  row.id
3163
3288
  )),
3164
- /* @__PURE__ */ jsxs13("div", { class: "page-peek-footer", children: [
3289
+ /* @__PURE__ */ jsxs14("div", { class: "page-peek-footer", children: [
3165
3290
  /* @__PURE__ */ jsx13("button", { type: "button", class: "page-peek-viewall", onClick: onViewAll, children: open === 1 ? strings["page.peek.viewAll.one"] : strings["page.peek.viewAll"].replace("{n}", String(open)) }),
3166
3291
  /* @__PURE__ */ jsx13("button", { type: "button", class: "page-peek-send", onClick: onSend, children: strings["fab.label"] })
3167
3292
  ] })
@@ -3621,6 +3746,46 @@ var WIDGET_STYLES = `
3621
3746
  .error { color: #dc2626; font-size: 13px; }
3622
3747
  .success { color: #059669; font-size: 13px; }
3623
3748
 
3749
+ /* REQ-A7: quiet transparency line under the form. */
3750
+ .capture-notice { color: var(--mfb-muted, #6b7280); font-size: 11px; line-height: 1.4; margin-top: 6px; }
3751
+
3752
+ /* #85: inline "#NN" report reference rendered as a link (a button for a11y). */
3753
+ .seq-link {
3754
+ display: inline;
3755
+ padding: 0;
3756
+ border: none;
3757
+ background: none;
3758
+ color: var(--mfb-accent);
3759
+ font: inherit;
3760
+ cursor: pointer;
3761
+ }
3762
+ .seq-link:hover { text-decoration: underline; }
3763
+
3764
+ /* ---- #89: discard-unsaved-changes confirmation over the form -------- */
3765
+ .discard-confirm {
3766
+ position: absolute;
3767
+ inset: 0;
3768
+ z-index: 5;
3769
+ display: flex;
3770
+ align-items: center;
3771
+ justify-content: center;
3772
+ padding: 16px;
3773
+ background: rgba(15, 23, 42, 0.45);
3774
+ border-radius: inherit;
3775
+ }
3776
+ .discard-confirm-card {
3777
+ max-width: 260px;
3778
+ text-align: center;
3779
+ background: var(--mfb-bg);
3780
+ color: var(--mfb-text);
3781
+ border-radius: var(--mfb-radius-lg);
3782
+ padding: 18px 16px;
3783
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
3784
+ }
3785
+ .discard-confirm-title { font-weight: 600; font-size: 15px; margin: 0 0 4px; }
3786
+ .discard-confirm-body { font-size: 13px; margin: 0 0 14px; opacity: 0.8; }
3787
+ .discard-confirm-actions { display: flex; gap: 8px; justify-content: center; }
3788
+
3624
3789
  /* ---- v0.6.0: manual screenshot upload + annotator -------------------- */
3625
3790
 
3626
3791
  .mfb-sr-only {
@@ -5104,7 +5269,7 @@ var WIDGET_STYLES = `
5104
5269
  `;
5105
5270
 
5106
5271
  // src/widget/mount.tsx
5107
- import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs14 } from "preact/jsx-runtime";
5272
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs15 } from "preact/jsx-runtime";
5108
5273
  function computeFabVisible(opts, externalId, visibilityAllowed) {
5109
5274
  if (!opts.showFAB) return false;
5110
5275
  if (opts.getExternalId !== void 0 && !externalId) return false;
@@ -5120,6 +5285,7 @@ function mountWidget(options) {
5120
5285
  shadow.appendChild(mountPoint);
5121
5286
  let currentState = { open: false, status: "idle", tab: "send" };
5122
5287
  let postSubmitTimer = null;
5288
+ let formDirty = false;
5123
5289
  function rerender(state) {
5124
5290
  currentState = state;
5125
5291
  render(h(Root, { state }), mountPoint);
@@ -5145,9 +5311,13 @@ function mountWidget(options) {
5145
5311
  const handleSubmit = useCallback2(async (values) => {
5146
5312
  rerender({ ...currentState, status: "submitting" });
5147
5313
  try {
5148
- await options.onSubmit(values);
5314
+ const result = await options.onSubmit(values);
5149
5315
  pageSignal.refresh();
5150
- rerender({ ...currentState, status: "success" });
5316
+ rerender({
5317
+ ...currentState,
5318
+ status: "success",
5319
+ ...result && result.seq !== void 0 && { submittedSeq: result.seq }
5320
+ });
5151
5321
  if (postSubmitTimer !== null) clearTimeout(postSubmitTimer);
5152
5322
  postSubmitTimer = setTimeout(() => {
5153
5323
  postSubmitTimer = null;
@@ -5214,8 +5384,8 @@ function mountWidget(options) {
5214
5384
  if (peekTimers.current.open) clearTimeout(peekTimers.current.open);
5215
5385
  peekTimers.current.close = setTimeout(() => setPeekOpen(false), 140);
5216
5386
  };
5217
- return /* @__PURE__ */ jsxs14(Fragment3, { children: [
5218
- fabVisible && /* @__PURE__ */ jsxs14(
5387
+ return /* @__PURE__ */ jsxs15(Fragment3, { children: [
5388
+ fabVisible && /* @__PURE__ */ jsxs15(
5219
5389
  "div",
5220
5390
  {
5221
5391
  class: "fab-area",
@@ -5263,14 +5433,62 @@ function mountWidget(options) {
5263
5433
  ]
5264
5434
  }
5265
5435
  ),
5266
- state.open && /* @__PURE__ */ jsxs14(
5436
+ state.open && /* @__PURE__ */ jsxs15(
5267
5437
  Modal,
5268
5438
  {
5269
- onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
5439
+ onDismiss: (reason) => {
5440
+ if (reason !== "button" && formDirty && currentState.tab === "send" && currentState.status !== "submitting") {
5441
+ rerender({ ...currentState, confirmingDiscard: true });
5442
+ return;
5443
+ }
5444
+ formDirty = false;
5445
+ rerender(
5446
+ clearSelected({
5447
+ ...currentState,
5448
+ open: false,
5449
+ status: "idle",
5450
+ confirmingDiscard: false
5451
+ })
5452
+ );
5453
+ },
5270
5454
  closeLabel: options.strings["form.close"],
5271
5455
  expanded: state.tab === "board",
5272
5456
  children: [
5273
- showMineTab && /* @__PURE__ */ jsxs14("div", { class: "tab-strip", role: "tablist", children: [
5457
+ state.confirmingDiscard && /* @__PURE__ */ jsx14("div", { class: "discard-confirm", role: "alertdialog", "aria-modal": "true", children: /* @__PURE__ */ jsxs15("div", { class: "discard-confirm-card", children: [
5458
+ /* @__PURE__ */ jsx14("p", { class: "discard-confirm-title", children: options.strings["form.discard.title"] }),
5459
+ /* @__PURE__ */ jsx14("p", { class: "discard-confirm-body", children: options.strings["form.discard.body"] }),
5460
+ /* @__PURE__ */ jsxs15("div", { class: "discard-confirm-actions", children: [
5461
+ /* @__PURE__ */ jsx14(
5462
+ "button",
5463
+ {
5464
+ type: "button",
5465
+ class: "btn",
5466
+ onClick: () => rerender({ ...currentState, confirmingDiscard: false }),
5467
+ children: options.strings["form.discard.keep"]
5468
+ }
5469
+ ),
5470
+ /* @__PURE__ */ jsx14(
5471
+ "button",
5472
+ {
5473
+ type: "button",
5474
+ class: "btn btn--primary",
5475
+ onClick: () => {
5476
+ formDirty = false;
5477
+ rerender(
5478
+ clearSelected({
5479
+ ...currentState,
5480
+ open: false,
5481
+ status: "idle",
5482
+ confirmingDiscard: false
5483
+ })
5484
+ );
5485
+ },
5486
+ children: options.strings["form.discard.confirm"]
5487
+ }
5488
+ )
5489
+ ] })
5490
+ ] }) }),
5491
+ showMineTab && /* @__PURE__ */ jsxs15("div", { class: "tab-strip", role: "tablist", children: [
5274
5492
  /* @__PURE__ */ jsx14(
5275
5493
  "button",
5276
5494
  {
@@ -5316,7 +5534,7 @@ function mountWidget(options) {
5316
5534
  }
5317
5535
  )
5318
5536
  ] }),
5319
- state.tab === "send" && /* @__PURE__ */ jsxs14(Fragment3, { children: [
5537
+ state.tab === "send" && /* @__PURE__ */ jsxs15(Fragment3, { children: [
5320
5538
  showMineTab && pageActivityEnabled && /* @__PURE__ */ jsx14(
5321
5539
  PageActivityStrip,
5322
5540
  {
@@ -5332,7 +5550,13 @@ function mountWidget(options) {
5332
5550
  onSubmit: handleSubmit,
5333
5551
  onCancel: () => rerender({ ...currentState, open: false, status: "idle" }),
5334
5552
  status: state.status,
5335
- ...state.error !== void 0 && { errorMessage: state.error }
5553
+ ...state.error !== void 0 && { errorMessage: state.error },
5554
+ ...state.submittedSeq !== void 0 && {
5555
+ submittedSeq: state.submittedSeq
5556
+ },
5557
+ onDirtyChange: (d) => {
5558
+ formDirty = d;
5559
+ }
5336
5560
  }
5337
5561
  )
5338
5562
  ] }),
@@ -5352,7 +5576,13 @@ function mountWidget(options) {
5352
5576
  externalId,
5353
5577
  reportId: state.selectedReportId,
5354
5578
  strings: options.strings,
5355
- onBack: () => rerender(clearSelected({ ...currentState }))
5579
+ onBack: () => rerender(clearSelected({ ...currentState })),
5580
+ onOpenSeq: (seq) => {
5581
+ void options.api?.getReportBySeq(seq, externalId).then(
5582
+ (r) => rerender({ ...currentState, selectedReportId: r.id })
5583
+ ).catch(() => {
5584
+ });
5585
+ }
5356
5586
  }
5357
5587
  ),
5358
5588
  state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx14(
@@ -5468,8 +5698,8 @@ function createFeedback(config) {
5468
5698
  capture_method: captureMethod,
5469
5699
  technical_context
5470
5700
  };
5471
- if ("0.27.1") {
5472
- payload.widget_version = "0.27.1";
5701
+ if ("0.28.0") {
5702
+ payload.widget_version = "0.28.0";
5473
5703
  }
5474
5704
  if (manualScreenshots?.length) {
5475
5705
  payload.screenshots = manualScreenshots;
@@ -5504,9 +5734,8 @@ function createFeedback(config) {
5504
5734
  host,
5505
5735
  strings,
5506
5736
  showFAB: config.showFAB ?? true,
5507
- onSubmit: async (values) => {
5508
- await buildAndSubmit(values);
5509
- },
5737
+ // Return the created report so the modal can acknowledge it by #seq (#82).
5738
+ onSubmit: (values) => buildAndSubmit(values),
5510
5739
  api,
5511
5740
  // Keep this a callback (not a snapshot) so the mount picks up identity
5512
5741
  // changes that happen after createFeedback() — `notifyIdentityChanged()`
@@ -5596,4 +5825,4 @@ function createFeedback(config) {
5596
5825
  export {
5597
5826
  createFeedback
5598
5827
  };
5599
- //# sourceMappingURL=chunk-ZFXGV5W5.mjs.map
5828
+ //# sourceMappingURL=chunk-FD3CJBMR.mjs.map