@mhosaic/feedback 0.7.3 → 0.9.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.
@@ -62,6 +62,18 @@ function createApiClient(options) {
62
62
  }
63
63
  return response.json();
64
64
  }
65
+ async function listChangelog(externalId) {
66
+ const response = await fetcher(
67
+ `${endpoint}/api/feedback/v1/reports/widget/changelog/`,
68
+ { method: "GET", headers: widgetHeaders(externalId) }
69
+ );
70
+ if (response.status === 404) return [];
71
+ if (!response.ok) {
72
+ const text = await response.text().catch(() => "");
73
+ throw new Error(`listChangelog failed: ${response.status} ${text}`);
74
+ }
75
+ return response.json();
76
+ }
65
77
  async function getReport(reportId, externalId) {
66
78
  const response = await fetcher(
67
79
  `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
@@ -112,7 +124,7 @@ function createApiClient(options) {
112
124
  }
113
125
  return response.json();
114
126
  }
115
- return { submitReport, listMine, getReport, addComment, closeAsResolved };
127
+ return { submitReport, listMine, listChangelog, getReport, addComment, closeAsResolved };
116
128
  }
117
129
 
118
130
  // src/capture/urlSanitizer.ts
@@ -473,6 +485,12 @@ var DEFAULT_STRINGS = {
473
485
  "annotator.applying": "Applying\u2026",
474
486
  "tab.send": "Send",
475
487
  "tab.mine": "My reports",
488
+ "tab.changelog": "This week",
489
+ "changelog.empty.title": "Nothing resolved yet",
490
+ "changelog.empty.body": "Once a report you sent is fixed it will appear here, grouped by week.",
491
+ "changelog.week_of": "Week of {date}",
492
+ "changelog.resolved_one": "{count} resolved",
493
+ "changelog.resolved_many": "{count} resolved",
476
494
  "mine.empty.title": "No reports yet",
477
495
  "mine.empty.body": "Once you send feedback you can follow the thread here.",
478
496
  "mine.refresh": "Refresh",
@@ -480,6 +498,11 @@ var DEFAULT_STRINGS = {
480
498
  "mine.error": "Could not load your reports.",
481
499
  "mine.replies_one": "1 reply",
482
500
  "mine.replies_many": "{count} replies",
501
+ "mine.filter.empty": "No reports match this filter.",
502
+ "kpi.new": "New",
503
+ "kpi.in_progress": "In progress",
504
+ "kpi.awaiting_validation": "Awaiting you",
505
+ "kpi.resolution_rate": "Resolution rate",
483
506
  "detail.back": "Back",
484
507
  "detail.thread": "Conversation",
485
508
  "detail.no_replies": "No replies yet \u2014 we\u2019ll let you know when an operator responds.",
@@ -498,6 +521,18 @@ var DEFAULT_STRINGS = {
498
521
  "detail.author.staff": "Operator",
499
522
  "detail.author.mcp": "Mhosaic Team",
500
523
  "detail.author.system": "System",
524
+ "detail.tech.title": "What we received",
525
+ "detail.tech.errors_one": "error",
526
+ "detail.tech.errors_many": "errors",
527
+ "detail.tech.device": "Device",
528
+ "detail.tech.device.viewport": "Viewport",
529
+ "detail.tech.device.platform": "Platform",
530
+ "detail.tech.device.language": "Language",
531
+ "detail.tech.device.timezone": "Timezone",
532
+ "detail.tech.device.connection": "Connection",
533
+ "detail.tech.errors": "Runtime errors",
534
+ "detail.tech.console": "Console (last 20)",
535
+ "detail.tech.network": "Network (last 15)",
501
536
  "status.new": "New",
502
537
  "status.in_progress": "In progress",
503
538
  "status.awaiting_validation": "Awaiting your validation",
@@ -552,6 +587,12 @@ var FRENCH_STRINGS = {
552
587
  "annotator.applying": "Application\u2026",
553
588
  "tab.send": "Envoyer",
554
589
  "tab.mine": "Mes rapports",
590
+ "tab.changelog": "Cette semaine",
591
+ "changelog.empty.title": "Rien de r\xE9solu pour l\u2019instant",
592
+ "changelog.empty.body": "Quand un rapport que vous avez envoy\xE9 est corrig\xE9, il appara\xEEtra ici, regroup\xE9 par semaine.",
593
+ "changelog.week_of": "Semaine du {date}",
594
+ "changelog.resolved_one": "{count} r\xE9solu",
595
+ "changelog.resolved_many": "{count} r\xE9solus",
555
596
  "mine.empty.title": "Aucun rapport",
556
597
  "mine.empty.body": "Apr\xE8s votre premier envoi vous pourrez suivre la conversation ici.",
557
598
  "mine.refresh": "Actualiser",
@@ -559,6 +600,11 @@ var FRENCH_STRINGS = {
559
600
  "mine.error": "Impossible de charger vos rapports.",
560
601
  "mine.replies_one": "1 r\xE9ponse",
561
602
  "mine.replies_many": "{count} r\xE9ponses",
603
+ "mine.filter.empty": "Aucun rapport ne correspond \xE0 ce filtre.",
604
+ "kpi.new": "Nouveau",
605
+ "kpi.in_progress": "En cours",
606
+ "kpi.awaiting_validation": "\xC0 valider",
607
+ "kpi.resolution_rate": "Taux de r\xE9solution",
562
608
  "detail.back": "Retour",
563
609
  "detail.thread": "Conversation",
564
610
  "detail.no_replies": "Pas encore de r\xE9ponse \u2014 vous serez notifi\xE9 d\xE8s qu\u2019un op\xE9rateur r\xE9pondra.",
@@ -577,6 +623,18 @@ var FRENCH_STRINGS = {
577
623
  "detail.author.staff": "Op\xE9rateur",
578
624
  "detail.author.mcp": "\xC9quipe Mhosaic",
579
625
  "detail.author.system": "Syst\xE8me",
626
+ "detail.tech.title": "Ce que nous avons re\xE7u",
627
+ "detail.tech.errors_one": "erreur",
628
+ "detail.tech.errors_many": "erreurs",
629
+ "detail.tech.device": "Appareil",
630
+ "detail.tech.device.viewport": "Fen\xEAtre",
631
+ "detail.tech.device.platform": "Plateforme",
632
+ "detail.tech.device.language": "Langue",
633
+ "detail.tech.device.timezone": "Fuseau horaire",
634
+ "detail.tech.device.connection": "Connexion",
635
+ "detail.tech.errors": "Erreurs d\u2019ex\xE9cution",
636
+ "detail.tech.console": "Console (20 derniers)",
637
+ "detail.tech.network": "R\xE9seau (15 derniers)",
580
638
  "status.new": "Nouveau",
581
639
  "status.in_progress": "En cours",
582
640
  "status.awaiting_validation": "En attente de validation",
@@ -606,10 +664,162 @@ function resolveStrings(overrides, options = {}) {
606
664
  import { h, render } from "preact";
607
665
  import { useCallback } from "preact/hooks";
608
666
 
667
+ // src/widget/ChangelogList.tsx
668
+ import { useEffect, useMemo, useRef, useState } from "preact/hooks";
669
+
670
+ // src/widget/ReportRow.tsx
671
+ import { jsx, jsxs } from "preact/jsx-runtime";
672
+ function statusClassName(status) {
673
+ return `pill pill-status pill-status--${status}`;
674
+ }
675
+ function severityClassName(severity) {
676
+ return `pill pill-severity pill-severity--${severity}`;
677
+ }
678
+ function typeClassName() {
679
+ return "pill pill-type";
680
+ }
681
+ function formatRelative(iso) {
682
+ const then = Date.parse(iso);
683
+ if (!Number.isFinite(then)) return "";
684
+ const seconds = Math.max(1, Math.round((Date.now() - then) / 1e3));
685
+ if (seconds < 60) return `${seconds}s`;
686
+ const minutes = Math.round(seconds / 60);
687
+ if (minutes < 60) return `${minutes}m`;
688
+ const hours = Math.round(minutes / 60);
689
+ if (hours < 48) return `${hours}h`;
690
+ const days = Math.round(hours / 24);
691
+ return `${days}d`;
692
+ }
693
+ function repliesLabel(count, strings) {
694
+ if (count === 1) return strings["mine.replies_one"];
695
+ return strings["mine.replies_many"].replace("{count}", String(count));
696
+ }
697
+ function ReportRow({ row, strings, onClick }) {
698
+ const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
699
+ return /* @__PURE__ */ jsxs("button", { type: "button", class: "mine-row", onClick, children: [
700
+ /* @__PURE__ */ jsxs("div", { class: "mine-row-pills", children: [
701
+ /* @__PURE__ */ jsx("span", { class: statusClassName(row.status), children: strings[`status.${row.status}`] ?? row.status }),
702
+ /* @__PURE__ */ jsx("span", { class: typeClassName(), children: strings[`type.${row.feedback_type}`] }),
703
+ /* @__PURE__ */ jsx("span", { class: severityClassName(row.severity), children: strings[`severity.${row.severity}`] })
704
+ ] }),
705
+ /* @__PURE__ */ jsx("div", { class: "mine-row-preview", children: preview }),
706
+ /* @__PURE__ */ jsxs("div", { class: "mine-row-meta", children: [
707
+ /* @__PURE__ */ jsx("span", { children: formatRelative(row.updated_at || row.created_at) }),
708
+ row.comment_count > 0 && /* @__PURE__ */ jsxs("span", { children: [
709
+ "\xB7 ",
710
+ repliesLabel(row.comment_count, strings)
711
+ ] })
712
+ ] })
713
+ ] });
714
+ }
715
+
716
+ // src/widget/ChangelogList.tsx
717
+ import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
718
+ var POLL_MS = 3e4;
719
+ function isoWeekKey(iso) {
720
+ const d = new Date(iso);
721
+ if (Number.isNaN(d.getTime())) return "";
722
+ const day = (d.getUTCDay() + 6) % 7;
723
+ const monday = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - day));
724
+ return monday.toISOString().slice(0, 10);
725
+ }
726
+ function groupRowsByWeek(rows) {
727
+ const buckets = /* @__PURE__ */ new Map();
728
+ for (const row of rows) {
729
+ const key = isoWeekKey(row.resolved_at);
730
+ if (!key) continue;
731
+ const existing = buckets.get(key);
732
+ if (existing) existing.push(row);
733
+ else buckets.set(key, [row]);
734
+ }
735
+ return Array.from(buckets.entries()).sort(([a], [b]) => a < b ? 1 : -1).map(([weekKey, weekRows]) => ({
736
+ weekKey,
737
+ label: formatWeekLabel(weekKey),
738
+ rows: weekRows
739
+ }));
740
+ }
741
+ function formatWeekLabel(weekKey) {
742
+ try {
743
+ return new Date(weekKey).toLocaleDateString(void 0, {
744
+ year: "numeric",
745
+ month: "short",
746
+ day: "numeric"
747
+ });
748
+ } catch {
749
+ return weekKey;
750
+ }
751
+ }
752
+ function ChangelogList({ api, externalId, strings, onSelect }) {
753
+ const [rows, setRows] = useState(null);
754
+ const [error, setError] = useState(null);
755
+ const [refreshing, setRefreshing] = useState(false);
756
+ const mountedRef = useRef(true);
757
+ const fetchRows = async () => {
758
+ setRefreshing(true);
759
+ setError(null);
760
+ try {
761
+ const next = await api.listChangelog(externalId);
762
+ if (!mountedRef.current) return;
763
+ setRows(next);
764
+ } catch (err) {
765
+ if (!mountedRef.current) return;
766
+ setError(err instanceof Error ? err.message : strings["mine.error"]);
767
+ } finally {
768
+ if (mountedRef.current) setRefreshing(false);
769
+ }
770
+ };
771
+ useEffect(() => {
772
+ mountedRef.current = true;
773
+ void fetchRows();
774
+ const timer = setInterval(() => {
775
+ void fetchRows();
776
+ }, POLL_MS);
777
+ return () => {
778
+ mountedRef.current = false;
779
+ clearInterval(timer);
780
+ };
781
+ }, [externalId]);
782
+ const groups = useMemo(() => rows ? groupRowsByWeek(rows) : [], [rows]);
783
+ const isLoading = rows === null && !error;
784
+ const isEmpty = rows !== null && rows.length === 0;
785
+ return /* @__PURE__ */ jsxs2("div", { class: "mine-list", children: [
786
+ /* @__PURE__ */ jsxs2("div", { class: "mine-list-header", children: [
787
+ /* @__PURE__ */ jsx2("h2", { children: strings["tab.changelog"] }),
788
+ /* @__PURE__ */ jsx2(
789
+ "button",
790
+ {
791
+ type: "button",
792
+ class: "btn",
793
+ onClick: () => {
794
+ void fetchRows();
795
+ },
796
+ disabled: refreshing,
797
+ children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
798
+ }
799
+ )
800
+ ] }),
801
+ isLoading && /* @__PURE__ */ jsx2("div", { class: "mine-loading", children: strings["mine.loading"] }),
802
+ error && /* @__PURE__ */ jsx2("div", { class: "error", children: error }),
803
+ isEmpty && /* @__PURE__ */ jsxs2("div", { class: "mine-empty", children: [
804
+ /* @__PURE__ */ jsx2("strong", { children: strings["changelog.empty.title"] }),
805
+ /* @__PURE__ */ jsx2("p", { children: strings["changelog.empty.body"] })
806
+ ] }),
807
+ groups.length > 0 && /* @__PURE__ */ jsx2("div", { class: "changelog-groups", children: groups.map((g) => /* @__PURE__ */ jsxs2("section", { class: "changelog-group", children: [
808
+ /* @__PURE__ */ jsxs2("header", { class: "changelog-group-header", children: [
809
+ /* @__PURE__ */ jsx2("span", { class: "changelog-group-marker", "aria-hidden": "true", children: "\u25CF" }),
810
+ /* @__PURE__ */ jsx2("span", { class: "changelog-group-label", children: strings["changelog.week_of"].replace("{date}", g.label) }),
811
+ /* @__PURE__ */ jsx2("span", { class: "changelog-group-rule", "aria-hidden": "true" }),
812
+ /* @__PURE__ */ jsx2("span", { class: "changelog-group-count", children: strings[g.rows.length === 1 ? "changelog.resolved_one" : "changelog.resolved_many"].replace("{count}", String(g.rows.length)) })
813
+ ] }),
814
+ /* @__PURE__ */ jsx2("ul", { class: "mine-rows", children: g.rows.map((row) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
815
+ ] }, g.weekKey)) })
816
+ ] });
817
+ }
818
+
609
819
  // src/widget/Fab.tsx
610
- import { jsx } from "preact/jsx-runtime";
820
+ import { jsx as jsx3 } from "preact/jsx-runtime";
611
821
  function ChatBubbleIcon() {
612
- return /* @__PURE__ */ jsx(
822
+ return /* @__PURE__ */ jsx3(
613
823
  "svg",
614
824
  {
615
825
  width: "24",
@@ -618,7 +828,7 @@ function ChatBubbleIcon() {
618
828
  fill: "none",
619
829
  "aria-hidden": "true",
620
830
  focusable: "false",
621
- children: /* @__PURE__ */ jsx(
831
+ children: /* @__PURE__ */ jsx3(
622
832
  "path",
623
833
  {
624
834
  d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z",
@@ -629,15 +839,15 @@ function ChatBubbleIcon() {
629
839
  );
630
840
  }
631
841
  function Fab({ label, onClick }) {
632
- return /* @__PURE__ */ jsx("button", { type: "button", class: "fab", "aria-label": label, title: label, onClick, children: /* @__PURE__ */ jsx(ChatBubbleIcon, {}) });
842
+ return /* @__PURE__ */ jsx3("button", { type: "button", class: "fab", "aria-label": label, title: label, onClick, children: /* @__PURE__ */ jsx3(ChatBubbleIcon, {}) });
633
843
  }
634
844
 
635
845
  // src/widget/Form.tsx
636
- import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "preact/hooks";
846
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "preact/hooks";
637
847
 
638
848
  // src/widget/Annotator.tsx
639
- import { useEffect, useRef, useState } from "preact/hooks";
640
- import { jsx as jsx2, jsxs } from "preact/jsx-runtime";
849
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "preact/hooks";
850
+ import { jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
641
851
  var COLORS = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#ffffff"];
642
852
  function drawShape(ctx, shape) {
643
853
  ctx.save();
@@ -696,27 +906,27 @@ function drawArrow(ctx, x1, y1, x2, y2) {
696
906
  ctx.fill();
697
907
  }
698
908
  var Icon = {
699
- rect: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("rect", { x: "2", y: "3", width: "12", height: "10", fill: "none", stroke: "currentColor", "stroke-width": "1.5" }) }),
700
- arrow: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("path", { d: "M2 8h11M9 4l4 4-4 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
701
- pencil: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("path", { d: "M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linejoin": "round" }) }),
702
- text: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("path", { d: "M3 3h10M8 3v10M5 13h6", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) }),
703
- undo: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("path", { d: "M4 7l3-3M4 7l3 3M4 7h6a3 3 0 0 1 0 6H7", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
704
- trash: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("path", { d: "M3 4h10M6 4V2.5h4V4M5 4l.5 9h5L11 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
705
- close: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("path", { d: "M4 4l8 8M12 4l-8 8", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) })
909
+ rect: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("rect", { x: "2", y: "3", width: "12", height: "10", fill: "none", stroke: "currentColor", "stroke-width": "1.5" }) }),
910
+ arrow: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M2 8h11M9 4l4 4-4 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
911
+ pencil: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linejoin": "round" }) }),
912
+ text: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M3 3h10M8 3v10M5 13h6", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) }),
913
+ undo: /* @__PURE__ */ jsx4("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M4 7l3-3M4 7l3 3M4 7h6a3 3 0 0 1 0 6H7", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
914
+ trash: /* @__PURE__ */ jsx4("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M3 4h10M6 4V2.5h4V4M5 4l.5 9h5L11 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
915
+ close: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M4 4l8 8M12 4l-8 8", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) })
706
916
  };
707
917
  function Annotator({ imageBlob, strings, onSave, onCancel }) {
708
- const canvasRef = useRef(null);
709
- const containerRef = useRef(null);
710
- const imageRef = useRef(null);
711
- const [tool, setTool] = useState("rectangle");
712
- const [color, setColor] = useState(COLORS[0]);
713
- const [shapes, setShapes] = useState([]);
714
- const isDrawingRef = useRef(false);
715
- const [draftShape, setDraftShape] = useState(null);
716
- const [imageLoaded, setImageLoaded] = useState(false);
717
- const [saving, setSaving] = useState(false);
718
- const scaleRef = useRef(1);
719
- useEffect(() => {
918
+ const canvasRef = useRef2(null);
919
+ const containerRef = useRef2(null);
920
+ const imageRef = useRef2(null);
921
+ const [tool, setTool] = useState2("rectangle");
922
+ const [color, setColor] = useState2(COLORS[0]);
923
+ const [shapes, setShapes] = useState2([]);
924
+ const isDrawingRef = useRef2(false);
925
+ const [draftShape, setDraftShape] = useState2(null);
926
+ const [imageLoaded, setImageLoaded] = useState2(false);
927
+ const [saving, setSaving] = useState2(false);
928
+ const scaleRef = useRef2(1);
929
+ useEffect2(() => {
720
930
  const onKey = (e) => {
721
931
  if (e.key === "Escape") {
722
932
  e.stopPropagation();
@@ -726,7 +936,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
726
936
  window.addEventListener("keydown", onKey);
727
937
  return () => window.removeEventListener("keydown", onKey);
728
938
  }, [onCancel]);
729
- useEffect(() => {
939
+ useEffect2(() => {
730
940
  const url = URL.createObjectURL(imageBlob);
731
941
  const img = new Image();
732
942
  img.onload = () => {
@@ -736,7 +946,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
736
946
  img.src = url;
737
947
  return () => URL.revokeObjectURL(url);
738
948
  }, [imageBlob]);
739
- useEffect(() => {
949
+ useEffect2(() => {
740
950
  if (!imageLoaded || !canvasRef.current || !imageRef.current || !containerRef.current) {
741
951
  return;
742
952
  }
@@ -753,7 +963,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
753
963
  canvas.style.height = `${img.height * fitScale}px`;
754
964
  redraw();
755
965
  }, [imageLoaded]);
756
- useEffect(() => {
966
+ useEffect2(() => {
757
967
  redraw();
758
968
  }, [shapes, draftShape]);
759
969
  function redraw() {
@@ -849,7 +1059,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
849
1059
  { id: "freehand", icon: Icon.pencil, label: strings["annotator.tool.freehand"] },
850
1060
  { id: "text", icon: Icon.text, label: strings["annotator.tool.text"] }
851
1061
  ];
852
- return /* @__PURE__ */ jsx2(
1062
+ return /* @__PURE__ */ jsx4(
853
1063
  "div",
854
1064
  {
855
1065
  class: "annotator-backdrop",
@@ -857,10 +1067,10 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
857
1067
  onClick: (e) => {
858
1068
  if (e.target === e.currentTarget) onCancel();
859
1069
  },
860
- children: /* @__PURE__ */ jsxs("div", { class: "annotator", role: "dialog", "aria-modal": "true", "aria-label": strings["annotator.title"], children: [
861
- /* @__PURE__ */ jsxs("div", { class: "annotator-header", children: [
862
- /* @__PURE__ */ jsx2("span", { children: strings["annotator.title"] }),
863
- /* @__PURE__ */ jsx2(
1070
+ children: /* @__PURE__ */ jsxs3("div", { class: "annotator", role: "dialog", "aria-modal": "true", "aria-label": strings["annotator.title"], children: [
1071
+ /* @__PURE__ */ jsxs3("div", { class: "annotator-header", children: [
1072
+ /* @__PURE__ */ jsx4("span", { children: strings["annotator.title"] }),
1073
+ /* @__PURE__ */ jsx4(
864
1074
  "button",
865
1075
  {
866
1076
  type: "button",
@@ -871,8 +1081,8 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
871
1081
  }
872
1082
  )
873
1083
  ] }),
874
- /* @__PURE__ */ jsxs("div", { class: "annotator-toolbar", children: [
875
- /* @__PURE__ */ jsx2("div", { class: "annotator-tools", children: tools.map((t) => /* @__PURE__ */ jsx2(
1084
+ /* @__PURE__ */ jsxs3("div", { class: "annotator-toolbar", children: [
1085
+ /* @__PURE__ */ jsx4("div", { class: "annotator-tools", children: tools.map((t) => /* @__PURE__ */ jsx4(
876
1086
  "button",
877
1087
  {
878
1088
  type: "button",
@@ -885,8 +1095,8 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
885
1095
  },
886
1096
  t.id
887
1097
  )) }),
888
- /* @__PURE__ */ jsx2("span", { class: "annotator-sep" }),
889
- /* @__PURE__ */ jsx2("div", { class: "annotator-colors", children: COLORS.map((c) => /* @__PURE__ */ jsx2(
1098
+ /* @__PURE__ */ jsx4("span", { class: "annotator-sep" }),
1099
+ /* @__PURE__ */ jsx4("div", { class: "annotator-colors", children: COLORS.map((c) => /* @__PURE__ */ jsx4(
890
1100
  "button",
891
1101
  {
892
1102
  type: "button",
@@ -898,8 +1108,8 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
898
1108
  },
899
1109
  c
900
1110
  )) }),
901
- /* @__PURE__ */ jsx2("span", { class: "annotator-sep" }),
902
- /* @__PURE__ */ jsxs(
1111
+ /* @__PURE__ */ jsx4("span", { class: "annotator-sep" }),
1112
+ /* @__PURE__ */ jsxs3(
903
1113
  "button",
904
1114
  {
905
1115
  type: "button",
@@ -908,11 +1118,11 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
908
1118
  disabled: shapes.length === 0,
909
1119
  children: [
910
1120
  Icon.undo,
911
- /* @__PURE__ */ jsx2("span", { children: strings["annotator.undo"] })
1121
+ /* @__PURE__ */ jsx4("span", { children: strings["annotator.undo"] })
912
1122
  ]
913
1123
  }
914
1124
  ),
915
- /* @__PURE__ */ jsxs(
1125
+ /* @__PURE__ */ jsxs3(
916
1126
  "button",
917
1127
  {
918
1128
  type: "button",
@@ -921,18 +1131,18 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
921
1131
  disabled: shapes.length === 0,
922
1132
  children: [
923
1133
  Icon.trash,
924
- /* @__PURE__ */ jsx2("span", { children: strings["annotator.clear"] })
1134
+ /* @__PURE__ */ jsx4("span", { children: strings["annotator.clear"] })
925
1135
  ]
926
1136
  }
927
1137
  ),
928
- /* @__PURE__ */ jsx2("span", { class: "annotator-spacer" }),
929
- /* @__PURE__ */ jsxs("span", { class: "annotator-count", children: [
1138
+ /* @__PURE__ */ jsx4("span", { class: "annotator-spacer" }),
1139
+ /* @__PURE__ */ jsxs3("span", { class: "annotator-count", children: [
930
1140
  shapes.length,
931
1141
  " ",
932
1142
  strings["annotator.count_suffix"]
933
1143
  ] })
934
1144
  ] }),
935
- /* @__PURE__ */ jsx2("div", { ref: containerRef, class: "annotator-canvas-wrap", children: !imageLoaded ? /* @__PURE__ */ jsx2("span", { class: "annotator-loading", children: strings["annotator.loading"] }) : /* @__PURE__ */ jsx2(
1145
+ /* @__PURE__ */ jsx4("div", { ref: containerRef, class: "annotator-canvas-wrap", children: !imageLoaded ? /* @__PURE__ */ jsx4("span", { class: "annotator-loading", children: strings["annotator.loading"] }) : /* @__PURE__ */ jsx4(
936
1146
  "canvas",
937
1147
  {
938
1148
  ref: canvasRef,
@@ -943,9 +1153,9 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
943
1153
  class: "annotator-canvas"
944
1154
  }
945
1155
  ) }),
946
- /* @__PURE__ */ jsxs("div", { class: "annotator-footer", children: [
947
- /* @__PURE__ */ jsx2("button", { type: "button", class: "btn", onClick: onCancel, children: strings["form.cancel"] }),
948
- /* @__PURE__ */ jsx2(
1156
+ /* @__PURE__ */ jsxs3("div", { class: "annotator-footer", children: [
1157
+ /* @__PURE__ */ jsx4("button", { type: "button", class: "btn", onClick: onCancel, children: strings["form.cancel"] }),
1158
+ /* @__PURE__ */ jsx4(
949
1159
  "button",
950
1160
  {
951
1161
  type: "button",
@@ -987,24 +1197,24 @@ function truncateUrl(url, maxLength = 80) {
987
1197
  }
988
1198
 
989
1199
  // src/widget/Form.tsx
990
- import { jsx as jsx3, jsxs as jsxs2 } from "preact/jsx-runtime";
1200
+ import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
991
1201
  var TYPES = ["bug", "feature", "question", "praise", "typo"];
992
1202
  var SEVERITIES = ["blocker", "high", "medium", "low"];
993
1203
  function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
994
- const [description, setDescription] = useState2("");
995
- const [feedbackType, setFeedbackType] = useState2("bug");
996
- const [severity, setSeverity] = useState2("medium");
997
- const [localError, setLocalError] = useState2("");
998
- const [screenshotBlob, setScreenshotBlob] = useState2(null);
999
- const [screenshotPreview, setScreenshotPreview] = useState2(null);
1000
- const [isDragOver, setIsDragOver] = useState2(false);
1001
- const [annotatorOpen, setAnnotatorOpen] = useState2(false);
1002
- const fileInputRef = useRef2(null);
1003
- const dropZoneRef = useRef2(null);
1204
+ const [description, setDescription] = useState3("");
1205
+ const [feedbackType, setFeedbackType] = useState3("bug");
1206
+ const [severity, setSeverity] = useState3("medium");
1207
+ const [localError, setLocalError] = useState3("");
1208
+ const [screenshotBlob, setScreenshotBlob] = useState3(null);
1209
+ const [screenshotPreview, setScreenshotPreview] = useState3(null);
1210
+ const [isDragOver, setIsDragOver] = useState3(false);
1211
+ const [annotatorOpen, setAnnotatorOpen] = useState3(false);
1212
+ const fileInputRef = useRef3(null);
1213
+ const dropZoneRef = useRef3(null);
1004
1214
  const submitting = status === "submitting";
1005
1215
  const submitLabel = submitting ? strings["form.submitting"] : strings["form.submit"];
1006
1216
  const pageUrl = typeof window !== "undefined" ? window.location.href : "";
1007
- useEffect2(() => {
1217
+ useEffect3(() => {
1008
1218
  return () => {
1009
1219
  if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
1010
1220
  };
@@ -1052,7 +1262,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1052
1262
  const file = e.dataTransfer?.files?.[0];
1053
1263
  if (file) acceptFile(file);
1054
1264
  };
1055
- useEffect2(() => {
1265
+ useEffect3(() => {
1056
1266
  const zone = dropZoneRef.current;
1057
1267
  if (!zone) return;
1058
1268
  const onPaste = (e) => {
@@ -1093,11 +1303,11 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1093
1303
  if (screenshotBlob) values.screenshot = screenshotBlob;
1094
1304
  onSubmit(values);
1095
1305
  };
1096
- return /* @__PURE__ */ jsxs2("form", { onSubmit: handleSubmit, children: [
1097
- /* @__PURE__ */ jsx3("h2", { children: strings["form.title"] }),
1098
- /* @__PURE__ */ jsxs2("div", { class: "field", children: [
1099
- /* @__PURE__ */ jsx3("label", { for: "mfb-desc", children: strings["form.description.label"] }),
1100
- /* @__PURE__ */ jsx3(
1306
+ return /* @__PURE__ */ jsxs4("form", { onSubmit: handleSubmit, children: [
1307
+ /* @__PURE__ */ jsx5("h2", { children: strings["form.title"] }),
1308
+ /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1309
+ /* @__PURE__ */ jsx5("label", { for: "mfb-desc", children: strings["form.description.label"] }),
1310
+ /* @__PURE__ */ jsx5(
1101
1311
  "textarea",
1102
1312
  {
1103
1313
  id: "mfb-desc",
@@ -1107,35 +1317,35 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1107
1317
  }
1108
1318
  )
1109
1319
  ] }),
1110
- /* @__PURE__ */ jsxs2("div", { class: "row", children: [
1111
- /* @__PURE__ */ jsxs2("div", { class: "field", children: [
1112
- /* @__PURE__ */ jsx3("label", { for: "mfb-type", children: strings["form.type.label"] }),
1113
- /* @__PURE__ */ jsx3(
1320
+ /* @__PURE__ */ jsxs4("div", { class: "row", children: [
1321
+ /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1322
+ /* @__PURE__ */ jsx5("label", { for: "mfb-type", children: strings["form.type.label"] }),
1323
+ /* @__PURE__ */ jsx5(
1114
1324
  "select",
1115
1325
  {
1116
1326
  id: "mfb-type",
1117
1327
  value: feedbackType,
1118
1328
  onChange: (e) => setFeedbackType(e.target.value),
1119
- children: TYPES.map((t) => /* @__PURE__ */ jsx3("option", { value: t, children: strings[`type.${t}`] }))
1329
+ children: TYPES.map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: strings[`type.${t}`] }))
1120
1330
  }
1121
1331
  )
1122
1332
  ] }),
1123
- /* @__PURE__ */ jsxs2("div", { class: "field", children: [
1124
- /* @__PURE__ */ jsx3("label", { for: "mfb-sev", children: strings["form.severity.label"] }),
1125
- /* @__PURE__ */ jsx3(
1333
+ /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1334
+ /* @__PURE__ */ jsx5("label", { for: "mfb-sev", children: strings["form.severity.label"] }),
1335
+ /* @__PURE__ */ jsx5(
1126
1336
  "select",
1127
1337
  {
1128
1338
  id: "mfb-sev",
1129
1339
  value: severity,
1130
1340
  onChange: (e) => setSeverity(e.target.value),
1131
- children: SEVERITIES.map((s) => /* @__PURE__ */ jsx3("option", { value: s, children: strings[`severity.${s}`] }))
1341
+ children: SEVERITIES.map((s) => /* @__PURE__ */ jsx5("option", { value: s, children: strings[`severity.${s}`] }))
1132
1342
  }
1133
1343
  )
1134
1344
  ] })
1135
1345
  ] }),
1136
- /* @__PURE__ */ jsxs2("div", { class: "field", children: [
1137
- /* @__PURE__ */ jsx3("label", { children: strings["form.screenshot.label"] }),
1138
- /* @__PURE__ */ jsx3(
1346
+ /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1347
+ /* @__PURE__ */ jsx5("label", { children: strings["form.screenshot.label"] }),
1348
+ /* @__PURE__ */ jsx5(
1139
1349
  "input",
1140
1350
  {
1141
1351
  ref: fileInputRef,
@@ -1147,9 +1357,9 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1147
1357
  tabIndex: -1
1148
1358
  }
1149
1359
  ),
1150
- screenshotPreview ? /* @__PURE__ */ jsxs2("div", { class: "screenshot-preview", children: [
1151
- /* @__PURE__ */ jsx3("img", { src: screenshotPreview, alt: "" }),
1152
- /* @__PURE__ */ jsx3(
1360
+ screenshotPreview ? /* @__PURE__ */ jsxs4("div", { class: "screenshot-preview", children: [
1361
+ /* @__PURE__ */ jsx5("img", { src: screenshotPreview, alt: "" }),
1362
+ /* @__PURE__ */ jsx5(
1153
1363
  "button",
1154
1364
  {
1155
1365
  type: "button",
@@ -1159,7 +1369,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1159
1369
  children: "\xD7"
1160
1370
  }
1161
1371
  ),
1162
- /* @__PURE__ */ jsx3(
1372
+ /* @__PURE__ */ jsx5(
1163
1373
  "button",
1164
1374
  {
1165
1375
  type: "button",
@@ -1168,7 +1378,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1168
1378
  children: strings["form.screenshot.annotate"]
1169
1379
  }
1170
1380
  )
1171
- ] }) : /* @__PURE__ */ jsxs2(
1381
+ ] }) : /* @__PURE__ */ jsxs4(
1172
1382
  "div",
1173
1383
  {
1174
1384
  ref: dropZoneRef,
@@ -1187,28 +1397,28 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1187
1397
  onDragLeave: handleDragLeave,
1188
1398
  onDrop: handleDrop,
1189
1399
  children: [
1190
- /* @__PURE__ */ jsxs2("div", { class: "screenshot-cta", children: [
1191
- /* @__PURE__ */ jsx3("strong", { children: strings["form.screenshot.cta_click"] }),
1400
+ /* @__PURE__ */ jsxs4("div", { class: "screenshot-cta", children: [
1401
+ /* @__PURE__ */ jsx5("strong", { children: strings["form.screenshot.cta_click"] }),
1192
1402
  ", ",
1193
1403
  strings["form.screenshot.cta_rest"]
1194
1404
  ] }),
1195
- /* @__PURE__ */ jsx3("div", { class: "screenshot-formats", children: strings["form.screenshot.formats"] })
1405
+ /* @__PURE__ */ jsx5("div", { class: "screenshot-formats", children: strings["form.screenshot.formats"] })
1196
1406
  ]
1197
1407
  }
1198
1408
  )
1199
1409
  ] }),
1200
- pageUrl && /* @__PURE__ */ jsxs2("div", { class: "page-context", title: pageUrl, children: [
1201
- /* @__PURE__ */ jsx3("span", { class: "page-context-label", children: strings["form.context.label"] }),
1202
- /* @__PURE__ */ jsx3("span", { class: "page-context-url", children: truncateUrl(pageUrl, 90) })
1410
+ pageUrl && /* @__PURE__ */ jsxs4("div", { class: "page-context", title: pageUrl, children: [
1411
+ /* @__PURE__ */ jsx5("span", { class: "page-context-label", children: strings["form.context.label"] }),
1412
+ /* @__PURE__ */ jsx5("span", { class: "page-context-url", children: truncateUrl(pageUrl, 90) })
1203
1413
  ] }),
1204
- localError && /* @__PURE__ */ jsx3("div", { class: "error", children: localError }),
1205
- status === "error" && errorMessage && /* @__PURE__ */ jsx3("div", { class: "error", children: errorMessage }),
1206
- status === "success" && /* @__PURE__ */ jsx3("div", { class: "success", children: strings["form.success"] }),
1207
- /* @__PURE__ */ jsxs2("div", { class: "actions", children: [
1208
- /* @__PURE__ */ jsx3("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
1209
- /* @__PURE__ */ jsx3("button", { type: "submit", class: "btn btn--primary", disabled: submitting, children: submitLabel })
1414
+ localError && /* @__PURE__ */ jsx5("div", { class: "error", children: localError }),
1415
+ status === "error" && errorMessage && /* @__PURE__ */ jsx5("div", { class: "error", children: errorMessage }),
1416
+ status === "success" && /* @__PURE__ */ jsx5("div", { class: "success", children: strings["form.success"] }),
1417
+ /* @__PURE__ */ jsxs4("div", { class: "actions", children: [
1418
+ /* @__PURE__ */ jsx5("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
1419
+ /* @__PURE__ */ jsx5("button", { type: "submit", class: "btn btn--primary", disabled: submitting, children: submitLabel })
1210
1420
  ] }),
1211
- annotatorOpen && screenshotBlob && /* @__PURE__ */ jsx3(
1421
+ annotatorOpen && screenshotBlob && /* @__PURE__ */ jsx5(
1212
1422
  Annotator,
1213
1423
  {
1214
1424
  imageBlob: screenshotBlob,
@@ -1221,62 +1431,104 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1221
1431
  }
1222
1432
 
1223
1433
  // src/widget/MineList.tsx
1224
- import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "preact/hooks";
1434
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "preact/hooks";
1225
1435
 
1226
- // src/widget/ReportRow.tsx
1227
- import { jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
1228
- function statusClassName(status) {
1229
- return `pill pill-status pill-status--${status}`;
1230
- }
1231
- function severityClassName(severity) {
1232
- return `pill pill-severity pill-severity--${severity}`;
1233
- }
1234
- function typeClassName() {
1235
- return "pill pill-type";
1236
- }
1237
- function formatRelative(iso) {
1238
- const then = Date.parse(iso);
1239
- if (!Number.isFinite(then)) return "";
1240
- const seconds = Math.max(1, Math.round((Date.now() - then) / 1e3));
1241
- if (seconds < 60) return `${seconds}s`;
1242
- const minutes = Math.round(seconds / 60);
1243
- if (minutes < 60) return `${minutes}m`;
1244
- const hours = Math.round(minutes / 60);
1245
- if (hours < 48) return `${hours}h`;
1246
- const days = Math.round(hours / 24);
1247
- return `${days}d`;
1436
+ // src/widget/KpiStrip.tsx
1437
+ import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
1438
+ function computeKpiCounts(rows) {
1439
+ const counts = {
1440
+ new: 0,
1441
+ in_progress: 0,
1442
+ awaiting_validation: 0,
1443
+ resolved: 0,
1444
+ total: 0
1445
+ };
1446
+ for (const row of rows) {
1447
+ counts.total += 1;
1448
+ switch (row.status) {
1449
+ case "new":
1450
+ counts.new += 1;
1451
+ break;
1452
+ case "in_progress":
1453
+ counts.in_progress += 1;
1454
+ break;
1455
+ case "awaiting_validation":
1456
+ counts.awaiting_validation += 1;
1457
+ break;
1458
+ case "closed":
1459
+ case "rejected":
1460
+ case "duplicate":
1461
+ case "wontfix":
1462
+ counts.resolved += 1;
1463
+ break;
1464
+ }
1465
+ }
1466
+ return counts;
1248
1467
  }
1249
- function repliesLabel(count, strings) {
1250
- if (count === 1) return strings["mine.replies_one"];
1251
- return strings["mine.replies_many"].replace("{count}", String(count));
1468
+ function computeResolutionRate(counts) {
1469
+ if (counts.total === 0) return null;
1470
+ return Math.round(
1471
+ (counts.awaiting_validation + counts.resolved) / counts.total * 100
1472
+ );
1252
1473
  }
1253
- function ReportRow({ row, strings, onClick }) {
1254
- const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
1255
- return /* @__PURE__ */ jsxs3("button", { type: "button", class: "mine-row", onClick, children: [
1256
- /* @__PURE__ */ jsxs3("div", { class: "mine-row-pills", children: [
1257
- /* @__PURE__ */ jsx4("span", { class: statusClassName(row.status), children: strings[`status.${row.status}`] ?? row.status }),
1258
- /* @__PURE__ */ jsx4("span", { class: typeClassName(), children: strings[`type.${row.feedback_type}`] }),
1259
- /* @__PURE__ */ jsx4("span", { class: severityClassName(row.severity), children: strings[`severity.${row.severity}`] })
1260
- ] }),
1261
- /* @__PURE__ */ jsx4("div", { class: "mine-row-preview", children: preview }),
1262
- /* @__PURE__ */ jsxs3("div", { class: "mine-row-meta", children: [
1263
- /* @__PURE__ */ jsx4("span", { children: formatRelative(row.updated_at || row.created_at) }),
1264
- row.comment_count > 0 && /* @__PURE__ */ jsxs3("span", { children: [
1265
- "\xB7 ",
1266
- repliesLabel(row.comment_count, strings)
1267
- ] })
1268
- ] })
1269
- ] });
1474
+ var FILTER_FOR_STATUS = {
1475
+ new: "new",
1476
+ in_progress: "in_progress",
1477
+ awaiting_validation: "awaiting_validation",
1478
+ closed: "resolved",
1479
+ rejected: "resolved",
1480
+ duplicate: "resolved",
1481
+ wontfix: "resolved"
1482
+ };
1483
+ function rowsMatchingFilter(rows, filter) {
1484
+ if (filter === "all") return rows;
1485
+ return rows.filter((r) => FILTER_FOR_STATUS[r.status] === filter);
1486
+ }
1487
+ function KpiStrip({ rows, filter, onFilter, strings }) {
1488
+ const counts = computeKpiCounts(rows);
1489
+ const resolutionRate = computeResolutionRate(counts);
1490
+ const cells = [
1491
+ { key: "new", label: strings["kpi.new"], value: String(counts.new) },
1492
+ { key: "in_progress", label: strings["kpi.in_progress"], value: String(counts.in_progress) },
1493
+ {
1494
+ key: "awaiting_validation",
1495
+ label: strings["kpi.awaiting_validation"],
1496
+ value: String(counts.awaiting_validation)
1497
+ },
1498
+ {
1499
+ key: "resolved",
1500
+ label: strings["kpi.resolution_rate"],
1501
+ value: resolutionRate === null ? "\u2014" : `${resolutionRate}%`
1502
+ }
1503
+ ];
1504
+ return /* @__PURE__ */ jsx6("div", { class: "kpi-strip", role: "toolbar", children: cells.map((c) => {
1505
+ const active = filter === c.key;
1506
+ const toggleTo = active ? "all" : c.key;
1507
+ return /* @__PURE__ */ jsxs5(
1508
+ "button",
1509
+ {
1510
+ type: "button",
1511
+ class: `kpi-cell kpi-cell--${c.key}${active ? " is-active" : ""}`,
1512
+ onClick: () => onFilter(toggleTo),
1513
+ "aria-pressed": active,
1514
+ children: [
1515
+ /* @__PURE__ */ jsx6("span", { class: "kpi-value", children: c.value }),
1516
+ /* @__PURE__ */ jsx6("span", { class: "kpi-label", children: c.label })
1517
+ ]
1518
+ }
1519
+ );
1520
+ }) });
1270
1521
  }
1271
1522
 
1272
1523
  // src/widget/MineList.tsx
1273
- import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
1274
- var POLL_MS = 3e4;
1524
+ import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
1525
+ var POLL_MS2 = 3e4;
1275
1526
  function MineList({ api, externalId, strings, onSelect }) {
1276
- const [rows, setRows] = useState3(null);
1277
- const [error, setError] = useState3(null);
1278
- const [refreshing, setRefreshing] = useState3(false);
1279
- const mountedRef = useRef3(true);
1527
+ const [rows, setRows] = useState4(null);
1528
+ const [error, setError] = useState4(null);
1529
+ const [refreshing, setRefreshing] = useState4(false);
1530
+ const [filter, setFilter] = useState4("all");
1531
+ const mountedRef = useRef4(true);
1280
1532
  const fetchRows = async () => {
1281
1533
  setRefreshing(true);
1282
1534
  setError(null);
@@ -1291,12 +1543,12 @@ function MineList({ api, externalId, strings, onSelect }) {
1291
1543
  if (mountedRef.current) setRefreshing(false);
1292
1544
  }
1293
1545
  };
1294
- useEffect3(() => {
1546
+ useEffect4(() => {
1295
1547
  mountedRef.current = true;
1296
1548
  void fetchRows();
1297
1549
  const timer = setInterval(() => {
1298
1550
  void fetchRows();
1299
- }, POLL_MS);
1551
+ }, POLL_MS2);
1300
1552
  return () => {
1301
1553
  mountedRef.current = false;
1302
1554
  clearInterval(timer);
@@ -1304,10 +1556,12 @@ function MineList({ api, externalId, strings, onSelect }) {
1304
1556
  }, [externalId]);
1305
1557
  const isEmpty = rows !== null && rows.length === 0;
1306
1558
  const isLoading = rows === null && !error;
1307
- return /* @__PURE__ */ jsxs4("div", { class: "mine-list", children: [
1308
- /* @__PURE__ */ jsxs4("div", { class: "mine-list-header", children: [
1309
- /* @__PURE__ */ jsx5("h2", { children: strings["tab.mine"] }),
1310
- /* @__PURE__ */ jsx5(
1559
+ const visibleRows = rows ? rowsMatchingFilter(rows, filter) : null;
1560
+ const visibleEmpty = !!rows && rows.length > 0 && (visibleRows?.length ?? 0) === 0;
1561
+ return /* @__PURE__ */ jsxs6("div", { class: "mine-list", children: [
1562
+ /* @__PURE__ */ jsxs6("div", { class: "mine-list-header", children: [
1563
+ /* @__PURE__ */ jsx7("h2", { children: strings["tab.mine"] }),
1564
+ /* @__PURE__ */ jsx7(
1311
1565
  "button",
1312
1566
  {
1313
1567
  type: "button",
@@ -1320,23 +1574,25 @@ function MineList({ api, externalId, strings, onSelect }) {
1320
1574
  }
1321
1575
  )
1322
1576
  ] }),
1323
- isLoading && /* @__PURE__ */ jsx5("div", { class: "mine-loading", children: strings["mine.loading"] }),
1324
- error && /* @__PURE__ */ jsx5("div", { class: "error", children: error }),
1325
- isEmpty && /* @__PURE__ */ jsxs4("div", { class: "mine-empty", children: [
1326
- /* @__PURE__ */ jsx5("strong", { children: strings["mine.empty.title"] }),
1327
- /* @__PURE__ */ jsx5("p", { children: strings["mine.empty.body"] })
1577
+ rows && rows.length > 0 && /* @__PURE__ */ jsx7(KpiStrip, { rows, filter, onFilter: setFilter, strings }),
1578
+ isLoading && /* @__PURE__ */ jsx7("div", { class: "mine-loading", children: strings["mine.loading"] }),
1579
+ error && /* @__PURE__ */ jsx7("div", { class: "error", children: error }),
1580
+ isEmpty && /* @__PURE__ */ jsxs6("div", { class: "mine-empty", children: [
1581
+ /* @__PURE__ */ jsx7("strong", { children: strings["mine.empty.title"] }),
1582
+ /* @__PURE__ */ jsx7("p", { children: strings["mine.empty.body"] })
1328
1583
  ] }),
1329
- rows && rows.length > 0 && /* @__PURE__ */ jsx5("ul", { class: "mine-rows", children: rows.map((row) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
1584
+ visibleEmpty && /* @__PURE__ */ jsx7("div", { class: "mine-empty", children: /* @__PURE__ */ jsx7("p", { children: strings["mine.filter.empty"] }) }),
1585
+ visibleRows && visibleRows.length > 0 && /* @__PURE__ */ jsx7("ul", { class: "mine-rows", children: visibleRows.map((row) => /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsx7(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
1330
1586
  ] });
1331
1587
  }
1332
1588
 
1333
1589
  // src/widget/Modal.tsx
1334
- import { useEffect as useEffect4, useRef as useRef4 } from "preact/hooks";
1335
- import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
1590
+ import { useEffect as useEffect5, useRef as useRef5 } from "preact/hooks";
1591
+ import { jsx as jsx8, jsxs as jsxs7 } from "preact/jsx-runtime";
1336
1592
  function Modal({ onDismiss, children, closeLabel = "Close" }) {
1337
- const modalRef = useRef4(null);
1338
- const previouslyFocused = useRef4(null);
1339
- useEffect4(() => {
1593
+ const modalRef = useRef5(null);
1594
+ const previouslyFocused = useRef5(null);
1595
+ useEffect5(() => {
1340
1596
  previouslyFocused.current = document.activeElement;
1341
1597
  const onKey = (e) => {
1342
1598
  if (e.key !== "Escape") return;
@@ -1358,7 +1614,7 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1358
1614
  if (prev && typeof prev.focus === "function") prev.focus();
1359
1615
  };
1360
1616
  }, [onDismiss]);
1361
- return /* @__PURE__ */ jsx6(
1617
+ return /* @__PURE__ */ jsx8(
1362
1618
  "div",
1363
1619
  {
1364
1620
  class: "backdrop",
@@ -1366,8 +1622,8 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1366
1622
  onClick: (e) => {
1367
1623
  if (e.target === e.currentTarget) onDismiss();
1368
1624
  },
1369
- children: /* @__PURE__ */ jsxs5("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
1370
- /* @__PURE__ */ jsx6(
1625
+ children: /* @__PURE__ */ jsxs7("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
1626
+ /* @__PURE__ */ jsx8(
1371
1627
  "button",
1372
1628
  {
1373
1629
  type: "button",
@@ -1384,10 +1640,10 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1384
1640
  }
1385
1641
 
1386
1642
  // src/widget/ReportDetailView.tsx
1387
- import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "preact/hooks";
1643
+ import { useEffect as useEffect6, useRef as useRef6, useState as useState5 } from "preact/hooks";
1388
1644
 
1389
1645
  // src/widget/CommentBubble.tsx
1390
- import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
1646
+ import { jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
1391
1647
  function CommentBubble({ comment, strings }) {
1392
1648
  const isMine = comment.is_mine;
1393
1649
  const isAgent = !isMine && comment.author_source === "mcp";
@@ -1395,10 +1651,10 @@ function CommentBubble({ comment, strings }) {
1395
1651
  const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
1396
1652
  const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
1397
1653
  const label = comment.author_label || strings[labelKey];
1398
- return /* @__PURE__ */ jsxs6("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1399
- !isMine && label && /* @__PURE__ */ jsx7("div", { class: `comment-author comment-author--${variant}`, children: label }),
1400
- /* @__PURE__ */ jsx7("div", { class: "comment-body", children: comment.body }),
1401
- /* @__PURE__ */ jsx7("div", { class: "comment-time", children: formatTime(comment.created_at) })
1654
+ return /* @__PURE__ */ jsxs8("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1655
+ !isMine && label && /* @__PURE__ */ jsx9("div", { class: `comment-author comment-author--${variant}`, children: label }),
1656
+ /* @__PURE__ */ jsx9("div", { class: "comment-body", children: comment.body }),
1657
+ /* @__PURE__ */ jsx9("div", { class: "comment-time", children: formatTime(comment.created_at) })
1402
1658
  ] });
1403
1659
  }
1404
1660
  function formatTime(iso) {
@@ -1413,8 +1669,8 @@ function formatTime(iso) {
1413
1669
  }
1414
1670
 
1415
1671
  // src/widget/ReportDetailView.tsx
1416
- import { jsx as jsx8, jsxs as jsxs7 } from "preact/jsx-runtime";
1417
- var POLL_MS2 = 3e4;
1672
+ import { Fragment, jsx as jsx10, jsxs as jsxs9 } from "preact/jsx-runtime";
1673
+ var POLL_MS3 = 3e4;
1418
1674
  function ReportDetailView({
1419
1675
  api,
1420
1676
  externalId,
@@ -1422,12 +1678,12 @@ function ReportDetailView({
1422
1678
  strings,
1423
1679
  onBack
1424
1680
  }) {
1425
- const [detail, setDetail] = useState4(null);
1426
- const [error, setError] = useState4(null);
1427
- const [composeBody, setComposeBody] = useState4("");
1428
- const [sending, setSending] = useState4(false);
1429
- const [closing, setClosing] = useState4(false);
1430
- const mountedRef = useRef5(true);
1681
+ const [detail, setDetail] = useState5(null);
1682
+ const [error, setError] = useState5(null);
1683
+ const [composeBody, setComposeBody] = useState5("");
1684
+ const [sending, setSending] = useState5(false);
1685
+ const [closing, setClosing] = useState5(false);
1686
+ const mountedRef = useRef6(true);
1431
1687
  const fetchDetail = async () => {
1432
1688
  try {
1433
1689
  const next = await api.getReport(reportId, externalId);
@@ -1439,12 +1695,12 @@ function ReportDetailView({
1439
1695
  setError(err instanceof Error ? err.message : "load_failed");
1440
1696
  }
1441
1697
  };
1442
- useEffect5(() => {
1698
+ useEffect6(() => {
1443
1699
  mountedRef.current = true;
1444
1700
  void fetchDetail();
1445
1701
  const timer = setInterval(() => {
1446
1702
  void fetchDetail();
1447
- }, POLL_MS2);
1703
+ }, POLL_MS3);
1448
1704
  return () => {
1449
1705
  mountedRef.current = false;
1450
1706
  clearInterval(timer);
@@ -1484,37 +1740,39 @@ function ReportDetailView({
1484
1740
  }
1485
1741
  };
1486
1742
  if (!detail && !error) {
1487
- return /* @__PURE__ */ jsx8("div", { class: "mine-loading", children: strings["mine.loading"] });
1743
+ return /* @__PURE__ */ jsx10("div", { class: "mine-loading", children: strings["mine.loading"] });
1488
1744
  }
1489
1745
  if (!detail) {
1490
- return /* @__PURE__ */ jsx8("div", { class: "error", role: "alert", children: error });
1746
+ return /* @__PURE__ */ jsx10("div", { class: "error", role: "alert", children: error });
1491
1747
  }
1492
1748
  const showCloseCta = detail.status === "awaiting_validation";
1493
- return /* @__PURE__ */ jsxs7("div", { class: "report-detail", children: [
1494
- /* @__PURE__ */ jsxs7("div", { class: "report-detail-header", children: [
1495
- /* @__PURE__ */ jsxs7("button", { type: "button", class: "btn", onClick: onBack, children: [
1749
+ return /* @__PURE__ */ jsxs9("div", { class: "report-detail", children: [
1750
+ /* @__PURE__ */ jsxs9("div", { class: "report-detail-header", children: [
1751
+ /* @__PURE__ */ jsxs9("button", { type: "button", class: "btn", onClick: onBack, children: [
1496
1752
  "\u2190 ",
1497
1753
  strings["detail.back"]
1498
1754
  ] }),
1499
- /* @__PURE__ */ jsx8("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1755
+ /* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1500
1756
  ] }),
1501
- /* @__PURE__ */ jsxs7("div", { class: "report-detail-body", children: [
1502
- /* @__PURE__ */ jsx8(ContextBlock, { detail, strings }),
1503
- /* @__PURE__ */ jsx8("p", { class: "report-detail-description", children: detail.description }),
1504
- detail.screenshot_url && /* @__PURE__ */ jsx8(
1757
+ /* @__PURE__ */ jsxs9("div", { class: "report-detail-body", children: [
1758
+ /* @__PURE__ */ jsx10(ContextBlock, { detail, strings }),
1759
+ /* @__PURE__ */ jsx10("p", { class: "report-detail-description", children: detail.description }),
1760
+ detail.screenshot_url && /* @__PURE__ */ jsx10(
1505
1761
  "a",
1506
1762
  {
1507
1763
  class: "report-detail-screenshot",
1508
1764
  href: detail.screenshot_url,
1509
1765
  target: "_blank",
1510
1766
  rel: "noopener noreferrer",
1511
- children: /* @__PURE__ */ jsx8("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
1767
+ children: /* @__PURE__ */ jsx10("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
1512
1768
  }
1513
1769
  ),
1514
- /* @__PURE__ */ jsx8("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1515
- detail.comments.length === 0 ? /* @__PURE__ */ jsx8("p", { class: "report-detail-empty", children: strings["detail.no_replies"] }) : /* @__PURE__ */ jsx8("ul", { class: "report-comments", children: detail.comments.map((c) => /* @__PURE__ */ jsx8("li", { children: /* @__PURE__ */ jsx8(CommentBubble, { comment: c, strings }) })) }),
1516
- /* @__PURE__ */ jsxs7("div", { class: "report-compose", children: [
1517
- /* @__PURE__ */ jsx8(
1770
+ /* @__PURE__ */ jsx10("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1771
+ detail.comments.length === 0 ? /* @__PURE__ */ jsx10("p", { class: "report-detail-empty", children: strings["detail.no_replies"] }) : /* @__PURE__ */ jsx10("ul", { class: "report-comments", children: detail.comments.map((c) => /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(CommentBubble, { comment: c, strings }) })) }),
1772
+ detail.status_history && detail.status_history.length > 0 && /* @__PURE__ */ jsx10(StatusHistorySection, { rows: detail.status_history, strings }),
1773
+ detail.technical_context && /* @__PURE__ */ jsx10(TechnicalContextDrawer, { ctx: detail.technical_context, strings }),
1774
+ /* @__PURE__ */ jsxs9("div", { class: "report-compose", children: [
1775
+ /* @__PURE__ */ jsx10(
1518
1776
  "textarea",
1519
1777
  {
1520
1778
  value: composeBody,
@@ -1523,8 +1781,8 @@ function ReportDetailView({
1523
1781
  disabled: sending
1524
1782
  }
1525
1783
  ),
1526
- /* @__PURE__ */ jsxs7("div", { class: "report-compose-actions", children: [
1527
- showCloseCta && /* @__PURE__ */ jsx8(
1784
+ /* @__PURE__ */ jsxs9("div", { class: "report-compose-actions", children: [
1785
+ showCloseCta && /* @__PURE__ */ jsx10(
1528
1786
  "button",
1529
1787
  {
1530
1788
  type: "button",
@@ -1534,7 +1792,7 @@ function ReportDetailView({
1534
1792
  children: closing ? strings["detail.close_busy"] : strings["detail.close_cta"]
1535
1793
  }
1536
1794
  ),
1537
- /* @__PURE__ */ jsx8(
1795
+ /* @__PURE__ */ jsx10(
1538
1796
  "button",
1539
1797
  {
1540
1798
  type: "button",
@@ -1546,7 +1804,7 @@ function ReportDetailView({
1546
1804
  )
1547
1805
  ] })
1548
1806
  ] }),
1549
- error && /* @__PURE__ */ jsx8("div", { class: "error", children: error })
1807
+ error && /* @__PURE__ */ jsx10("div", { class: "error", children: error })
1550
1808
  ] })
1551
1809
  ] });
1552
1810
  }
@@ -1557,19 +1815,19 @@ function appendComment(current, next) {
1557
1815
  function ContextBlock({ detail, strings }) {
1558
1816
  const captureKey = `detail.context.capture.${detail.capture_method}`;
1559
1817
  const captureLabel = strings[captureKey] ?? detail.capture_method;
1560
- return /* @__PURE__ */ jsxs7("div", { class: "report-detail-context", children: [
1561
- /* @__PURE__ */ jsxs7("div", { class: "report-detail-context-pills", children: [
1562
- /* @__PURE__ */ jsx8("span", { class: "pill pill-type", children: strings[`type.${detail.feedback_type}`] }),
1563
- /* @__PURE__ */ jsx8("span", { class: `pill pill-severity pill-severity--${detail.severity}`, children: strings[`severity.${detail.severity}`] }),
1564
- /* @__PURE__ */ jsx8("span", { class: "pill pill-capture", children: captureLabel })
1818
+ return /* @__PURE__ */ jsxs9("div", { class: "report-detail-context", children: [
1819
+ /* @__PURE__ */ jsxs9("div", { class: "report-detail-context-pills", children: [
1820
+ /* @__PURE__ */ jsx10("span", { class: "pill pill-type", children: strings[`type.${detail.feedback_type}`] }),
1821
+ /* @__PURE__ */ jsx10("span", { class: `pill pill-severity pill-severity--${detail.severity}`, children: strings[`severity.${detail.severity}`] }),
1822
+ /* @__PURE__ */ jsx10("span", { class: "pill pill-capture", children: captureLabel })
1565
1823
  ] }),
1566
- /* @__PURE__ */ jsxs7("div", { class: "report-detail-context-line", children: [
1567
- /* @__PURE__ */ jsx8("span", { class: "report-detail-context-label", children: strings["detail.context.submitted_at"] }),
1568
- /* @__PURE__ */ jsx8("span", { children: formatSubmittedAt(detail.created_at) })
1824
+ /* @__PURE__ */ jsxs9("div", { class: "report-detail-context-line", children: [
1825
+ /* @__PURE__ */ jsx10("span", { class: "report-detail-context-label", children: strings["detail.context.submitted_at"] }),
1826
+ /* @__PURE__ */ jsx10("span", { children: formatSubmittedAt(detail.created_at) })
1569
1827
  ] }),
1570
- detail.page_url && /* @__PURE__ */ jsxs7("div", { class: "report-detail-context-line", title: detail.page_url, children: [
1571
- /* @__PURE__ */ jsx8("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
1572
- /* @__PURE__ */ jsx8(
1828
+ detail.page_url && /* @__PURE__ */ jsxs9("div", { class: "report-detail-context-line", title: detail.page_url, children: [
1829
+ /* @__PURE__ */ jsx10("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
1830
+ /* @__PURE__ */ jsx10(
1573
1831
  "a",
1574
1832
  {
1575
1833
  class: "report-detail-context-url",
@@ -1592,6 +1850,120 @@ function formatSubmittedAt(iso) {
1592
1850
  return iso;
1593
1851
  }
1594
1852
  }
1853
+ var TECH_CONSOLE_LIMIT = 20;
1854
+ var TECH_NETWORK_LIMIT = 15;
1855
+ function TechnicalContextDrawer({ ctx, strings }) {
1856
+ const errors = ctx.errors ?? [];
1857
+ const consoleLogs = (ctx.consoleLogs ?? []).slice(-TECH_CONSOLE_LIMIT);
1858
+ const network = (ctx.networkRequests ?? []).slice(-TECH_NETWORK_LIMIT);
1859
+ const device = ctx.device;
1860
+ const hasAny = !!device || errors.length > 0 || consoleLogs.length > 0 || network.length > 0;
1861
+ if (!hasAny) return null;
1862
+ const errorCount = errors.length;
1863
+ 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"];
1864
+ return /* @__PURE__ */ jsxs9("details", { class: "report-detail-tech", children: [
1865
+ /* @__PURE__ */ jsx10("summary", { children: summary }),
1866
+ /* @__PURE__ */ jsxs9("div", { class: "tech-body", children: [
1867
+ device && /* @__PURE__ */ jsx10(DeviceSection, { device, strings }),
1868
+ errors.length > 0 && /* @__PURE__ */ jsx10(ErrorsSection, { errors, strings }),
1869
+ consoleLogs.length > 0 && /* @__PURE__ */ jsx10(ConsoleSection, { logs: consoleLogs, strings }),
1870
+ network.length > 0 && /* @__PURE__ */ jsx10(NetworkSection, { rows: network, strings })
1871
+ ] })
1872
+ ] });
1873
+ }
1874
+ function DeviceSection({
1875
+ device,
1876
+ strings
1877
+ }) {
1878
+ const parts = [];
1879
+ if (device?.viewport) {
1880
+ const dpr = device.viewport.dpr ? ` @${device.viewport.dpr}x` : "";
1881
+ parts.push({
1882
+ label: strings["detail.tech.device.viewport"],
1883
+ value: `${device.viewport.w}\xD7${device.viewport.h}${dpr}`
1884
+ });
1885
+ }
1886
+ if (device?.platform) parts.push({ label: strings["detail.tech.device.platform"], value: device.platform });
1887
+ if (device?.language) parts.push({ label: strings["detail.tech.device.language"], value: device.language });
1888
+ if (device?.timezone) parts.push({ label: strings["detail.tech.device.timezone"], value: device.timezone });
1889
+ if (device?.connection) parts.push({ label: strings["detail.tech.device.connection"], value: device.connection });
1890
+ if (parts.length === 0) return null;
1891
+ return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
1892
+ /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.device"] }),
1893
+ /* @__PURE__ */ jsx10("dl", { class: "tech-device", children: parts.map((p) => /* @__PURE__ */ jsxs9(Fragment, { children: [
1894
+ /* @__PURE__ */ jsx10("dt", { children: p.label }),
1895
+ /* @__PURE__ */ jsx10("dd", { children: p.value })
1896
+ ] })) })
1897
+ ] });
1898
+ }
1899
+ function ErrorsSection({
1900
+ errors,
1901
+ strings
1902
+ }) {
1903
+ return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
1904
+ /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.errors"] }),
1905
+ /* @__PURE__ */ jsx10("ul", { class: "tech-errors", children: errors.map((e) => /* @__PURE__ */ jsxs9("li", { children: [
1906
+ /* @__PURE__ */ jsx10("div", { class: "tech-errors-msg", children: e.message }),
1907
+ e.stack && /* @__PURE__ */ jsx10("pre", { class: "tech-errors-stack", children: truncateStack(e.stack) })
1908
+ ] })) })
1909
+ ] });
1910
+ }
1911
+ function ConsoleSection({
1912
+ logs,
1913
+ strings
1914
+ }) {
1915
+ return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
1916
+ /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.console"] }),
1917
+ /* @__PURE__ */ jsx10("ul", { class: "tech-console", children: logs.map((l) => /* @__PURE__ */ jsxs9("li", { class: `tech-console-row tech-console-row--${l.level}`, children: [
1918
+ /* @__PURE__ */ jsx10("span", { class: "tech-console-level", children: l.level }),
1919
+ /* @__PURE__ */ jsx10("span", { class: "tech-console-msg", children: l.message })
1920
+ ] })) })
1921
+ ] });
1922
+ }
1923
+ function NetworkSection({
1924
+ rows,
1925
+ strings
1926
+ }) {
1927
+ return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
1928
+ /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.network"] }),
1929
+ /* @__PURE__ */ jsx10("ul", { class: "tech-network", children: rows.map((n) => {
1930
+ const failed = n.status >= 400 || n.status === 0;
1931
+ return /* @__PURE__ */ jsxs9("li", { class: `tech-network-row${failed ? " tech-network-row--fail" : ""}`, children: [
1932
+ /* @__PURE__ */ jsx10("span", { class: "tech-network-status", children: n.status || "\u2014" }),
1933
+ /* @__PURE__ */ jsx10("span", { class: "tech-network-method", children: n.method }),
1934
+ /* @__PURE__ */ jsx10("span", { class: "tech-network-url", title: n.url, children: truncateUrl(n.url, 56) }),
1935
+ /* @__PURE__ */ jsxs9("span", { class: "tech-network-time", children: [
1936
+ Math.round(n.durationMs),
1937
+ "ms"
1938
+ ] })
1939
+ ] });
1940
+ }) })
1941
+ ] });
1942
+ }
1943
+ function truncateStack(stack) {
1944
+ const lines = stack.split("\n");
1945
+ if (lines.length <= 12) return stack;
1946
+ return lines.slice(0, 12).join("\n") + "\n \u2026";
1947
+ }
1948
+ function StatusHistorySection({ rows, strings }) {
1949
+ return /* @__PURE__ */ jsxs9("div", { class: "report-detail-history", children: [
1950
+ /* @__PURE__ */ jsx10("h3", { class: "report-detail-section", children: strings["detail.history"] }),
1951
+ /* @__PURE__ */ jsx10("ol", { class: "status-history", children: rows.map((r) => {
1952
+ const from = strings[`status.${r.from_status}`] ?? r.from_status;
1953
+ const to = strings[`status.${r.to_status}`] ?? r.to_status;
1954
+ const sourceKey = r.changed_by_source === "mcp" ? "detail.author.mcp" : r.changed_by_source === "system" ? "detail.author.system" : "detail.author.staff";
1955
+ return /* @__PURE__ */ jsxs9("li", { class: "status-history-row", children: [
1956
+ /* @__PURE__ */ jsx10("span", { class: "status-history-time", children: formatSubmittedAt(r.created_at) }),
1957
+ /* @__PURE__ */ jsxs9("span", { class: "status-history-transition", children: [
1958
+ /* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${r.from_status}`, children: from }),
1959
+ /* @__PURE__ */ jsx10("span", { class: "status-history-arrow", "aria-hidden": "true", children: "\u2192" }),
1960
+ /* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${r.to_status}`, children: to })
1961
+ ] }),
1962
+ /* @__PURE__ */ jsx10("span", { class: `status-history-source status-history-source--${r.changed_by_source}`, children: strings[sourceKey] })
1963
+ ] });
1964
+ }) })
1965
+ ] });
1966
+ }
1595
1967
 
1596
1968
  // src/widget/styles.ts
1597
1969
  var WIDGET_STYLES = `
@@ -2313,10 +2685,259 @@ var WIDGET_STYLES = `
2313
2685
  justify-content: flex-end;
2314
2686
  gap: 6px;
2315
2687
  }
2688
+
2689
+ /* Status history \u2014 read-only audit trail visible to the submitter.
2690
+ Timeline of who flipped the status and when (operator vs. agent vs.
2691
+ system). Bordered, neutral surface so it doesn't compete visually
2692
+ with the conversation thread above. */
2693
+ .report-detail-history {
2694
+ display: flex;
2695
+ flex-direction: column;
2696
+ gap: 6px;
2697
+ border: 1px solid var(--mfb-border);
2698
+ border-radius: var(--mfb-radius);
2699
+ background: var(--mfb-surface);
2700
+ padding: 8px 10px;
2701
+ }
2702
+ .report-detail-history .report-detail-section {
2703
+ margin: 0;
2704
+ font-size: 11px;
2705
+ text-transform: uppercase;
2706
+ letter-spacing: 0.04em;
2707
+ color: var(--mfb-text-muted);
2708
+ }
2709
+ .status-history {
2710
+ list-style: none;
2711
+ margin: 0;
2712
+ padding: 0;
2713
+ display: flex;
2714
+ flex-direction: column;
2715
+ gap: 4px;
2716
+ }
2717
+ .status-history-row {
2718
+ display: grid;
2719
+ grid-template-columns: auto 1fr auto;
2720
+ gap: 8px;
2721
+ align-items: center;
2722
+ font-size: 11px;
2723
+ }
2724
+ .status-history-time {
2725
+ color: var(--mfb-text-muted);
2726
+ font-variant-numeric: tabular-nums;
2727
+ white-space: nowrap;
2728
+ }
2729
+ .status-history-transition {
2730
+ display: inline-flex;
2731
+ gap: 4px;
2732
+ align-items: center;
2733
+ flex-wrap: wrap;
2734
+ }
2735
+ .status-history-arrow {
2736
+ color: var(--mfb-text-muted);
2737
+ font-size: 11px;
2738
+ }
2739
+ .status-history-source {
2740
+ font-size: 10px;
2741
+ text-transform: uppercase;
2742
+ letter-spacing: 0.04em;
2743
+ font-weight: 600;
2744
+ white-space: nowrap;
2745
+ }
2746
+ .status-history-source--mcp { color: #6b21a8; }
2747
+ .status-history-source--user { color: #1e40af; }
2748
+ .status-history-source--system { color: var(--mfb-text-muted); }
2749
+
2750
+ /* Transparency drawer \u2014 closed by default. The user can click open to
2751
+ verify what their browser sent: device, errors, console tail, network
2752
+ tail. Read-only; purely a trust-building gesture. */
2753
+ .report-detail-tech {
2754
+ border: 1px solid var(--mfb-border);
2755
+ border-radius: var(--mfb-radius);
2756
+ background: var(--mfb-surface);
2757
+ }
2758
+ .report-detail-tech > summary {
2759
+ cursor: pointer;
2760
+ padding: 8px 10px;
2761
+ font-size: 11px;
2762
+ text-transform: uppercase;
2763
+ letter-spacing: 0.04em;
2764
+ color: var(--mfb-text-muted);
2765
+ user-select: none;
2766
+ list-style: none;
2767
+ }
2768
+ .report-detail-tech > summary::-webkit-details-marker { display: none; }
2769
+ .report-detail-tech > summary::before {
2770
+ content: '\u25B8';
2771
+ display: inline-block;
2772
+ margin-right: 6px;
2773
+ transition: transform 120ms;
2774
+ }
2775
+ .report-detail-tech[open] > summary::before { transform: rotate(90deg); }
2776
+ .tech-body {
2777
+ padding: 0 10px 10px 10px;
2778
+ display: flex;
2779
+ flex-direction: column;
2780
+ gap: 10px;
2781
+ }
2782
+ .tech-section h4 {
2783
+ margin: 0 0 4px 0;
2784
+ font-size: 10px;
2785
+ text-transform: uppercase;
2786
+ letter-spacing: 0.04em;
2787
+ color: var(--mfb-text-muted);
2788
+ font-weight: 600;
2789
+ }
2790
+ .tech-device {
2791
+ display: grid;
2792
+ grid-template-columns: max-content 1fr;
2793
+ column-gap: 8px;
2794
+ row-gap: 2px;
2795
+ margin: 0;
2796
+ font-size: 11px;
2797
+ font-variant-numeric: tabular-nums;
2798
+ }
2799
+ .tech-device dt { color: var(--mfb-text-muted); }
2800
+ .tech-device dd { margin: 0; }
2801
+ .tech-errors, .tech-console, .tech-network {
2802
+ list-style: none;
2803
+ margin: 0;
2804
+ padding: 0;
2805
+ display: flex;
2806
+ flex-direction: column;
2807
+ gap: 2px;
2808
+ font-size: 11px;
2809
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
2810
+ max-height: 200px;
2811
+ overflow-y: auto;
2812
+ }
2813
+ .tech-errors li { padding: 4px 0; border-top: 1px solid var(--mfb-border); }
2814
+ .tech-errors li:first-child { border-top: 0; }
2815
+ .tech-errors-msg { color: #991b1b; font-weight: 600; }
2816
+ .tech-errors-stack {
2817
+ margin: 4px 0 0 0;
2818
+ padding: 4px 6px;
2819
+ background: var(--mfb-bg);
2820
+ border-radius: 4px;
2821
+ font-size: 10px;
2822
+ white-space: pre-wrap;
2823
+ color: var(--mfb-text-muted);
2824
+ max-height: 120px;
2825
+ overflow-y: auto;
2826
+ }
2827
+ .tech-console-row {
2828
+ display: grid;
2829
+ grid-template-columns: 48px 1fr;
2830
+ gap: 6px;
2831
+ padding: 1px 0;
2832
+ }
2833
+ .tech-console-level {
2834
+ text-transform: uppercase;
2835
+ font-size: 9px;
2836
+ font-weight: 600;
2837
+ align-self: center;
2838
+ color: var(--mfb-text-muted);
2839
+ }
2840
+ .tech-console-row--warn .tech-console-level { color: #92400e; }
2841
+ .tech-console-row--error .tech-console-level { color: #991b1b; }
2842
+ .tech-console-row--info .tech-console-level { color: #1e40af; }
2843
+ .tech-console-msg { word-break: break-word; }
2844
+ .tech-network-row {
2845
+ display: grid;
2846
+ grid-template-columns: 48px 56px 1fr 56px;
2847
+ gap: 6px;
2848
+ padding: 1px 0;
2849
+ }
2850
+ .tech-network-row--fail .tech-network-status { color: #991b1b; font-weight: 700; }
2851
+ .tech-network-status { font-variant-numeric: tabular-nums; }
2852
+ .tech-network-method { color: var(--mfb-text-muted); }
2853
+ .tech-network-url { word-break: break-all; }
2854
+ .tech-network-time { text-align: right; color: var(--mfb-text-muted); font-variant-numeric: tabular-nums; }
2855
+
2856
+ /* KPI strip \u2014 four pills above the My Reports list. Each is clickable
2857
+ to filter the rows below. Borrowed from thePnr's FeedbackKPICards. */
2858
+ .kpi-strip {
2859
+ display: grid;
2860
+ grid-template-columns: repeat(4, 1fr);
2861
+ gap: 6px;
2862
+ }
2863
+ .kpi-cell {
2864
+ appearance: none;
2865
+ background: var(--mfb-bg);
2866
+ border: 1px solid var(--mfb-border);
2867
+ border-radius: var(--mfb-radius);
2868
+ padding: 8px 6px;
2869
+ cursor: pointer;
2870
+ display: flex;
2871
+ flex-direction: column;
2872
+ align-items: flex-start;
2873
+ gap: 2px;
2874
+ font-family: inherit;
2875
+ color: inherit;
2876
+ transition: background 120ms, border-color 120ms;
2877
+ }
2878
+ .kpi-cell:hover { background: var(--mfb-surface); }
2879
+ .kpi-cell.is-active {
2880
+ border-color: var(--mfb-accent);
2881
+ background: rgba(59, 130, 246, 0.08);
2882
+ }
2883
+ .kpi-cell.is-active.kpi-cell--in_progress { border-color: #d97706; background: rgba(251, 191, 36, 0.10); }
2884
+ .kpi-cell.is-active.kpi-cell--awaiting_validation { border-color: #9333ea; background: rgba(168, 85, 247, 0.10); }
2885
+ .kpi-cell.is-active.kpi-cell--resolved { border-color: #059669; background: rgba(16, 185, 129, 0.10); }
2886
+ .kpi-value {
2887
+ font-size: 18px;
2888
+ font-weight: 700;
2889
+ font-variant-numeric: tabular-nums;
2890
+ line-height: 1;
2891
+ }
2892
+ .kpi-label {
2893
+ font-size: 10px;
2894
+ text-transform: uppercase;
2895
+ letter-spacing: 0.04em;
2896
+ color: var(--mfb-text-muted);
2897
+ }
2898
+
2899
+ /* Changelog ("This week") \u2014 week-grouped resolved-report timeline.
2900
+ Borrowed from thePnr's FeedbackChangelogView; the header per group
2901
+ reads "\u25CF Week of {date} \u2501\u2501\u2501 {n} resolved". */
2902
+ .changelog-groups {
2903
+ display: flex;
2904
+ flex-direction: column;
2905
+ gap: 12px;
2906
+ }
2907
+ .changelog-group {
2908
+ display: flex;
2909
+ flex-direction: column;
2910
+ gap: 6px;
2911
+ }
2912
+ .changelog-group-header {
2913
+ display: grid;
2914
+ grid-template-columns: auto auto 1fr auto;
2915
+ gap: 6px;
2916
+ align-items: center;
2917
+ font-size: 11px;
2918
+ text-transform: uppercase;
2919
+ letter-spacing: 0.04em;
2920
+ color: var(--mfb-text-muted);
2921
+ }
2922
+ .changelog-group-marker {
2923
+ color: var(--mfb-accent);
2924
+ font-size: 14px;
2925
+ line-height: 1;
2926
+ }
2927
+ .changelog-group-label { font-weight: 600; }
2928
+ .changelog-group-rule {
2929
+ height: 1px;
2930
+ background: var(--mfb-border);
2931
+ align-self: center;
2932
+ }
2933
+ .changelog-group-count {
2934
+ font-variant-numeric: tabular-nums;
2935
+ font-weight: 600;
2936
+ }
2316
2937
  `;
2317
2938
 
2318
2939
  // src/widget/mount.tsx
2319
- import { Fragment, jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
2940
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs10 } from "preact/jsx-runtime";
2320
2941
  function mountWidget(options) {
2321
2942
  const shadow = options.host.attachShadow({ mode: "open" });
2322
2943
  const style = document.createElement("style");
@@ -2363,22 +2984,22 @@ function mountWidget(options) {
2363
2984
  const externalId = options.getExternalId?.();
2364
2985
  const fabVisible = options.showFAB && (options.getExternalId === void 0 ? true : Boolean(externalId));
2365
2986
  const showMineTab = Boolean(options.api && externalId);
2366
- return /* @__PURE__ */ jsxs8(Fragment, { children: [
2367
- fabVisible && /* @__PURE__ */ jsx9(
2987
+ return /* @__PURE__ */ jsxs10(Fragment2, { children: [
2988
+ fabVisible && /* @__PURE__ */ jsx11(
2368
2989
  Fab,
2369
2990
  {
2370
2991
  label: options.strings["fab.label"],
2371
2992
  onClick: () => rerender({ ...currentState, open: true })
2372
2993
  }
2373
2994
  ),
2374
- state.open && /* @__PURE__ */ jsxs8(
2995
+ state.open && /* @__PURE__ */ jsxs10(
2375
2996
  Modal,
2376
2997
  {
2377
2998
  onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
2378
2999
  closeLabel: options.strings["form.close"],
2379
3000
  children: [
2380
- showMineTab && /* @__PURE__ */ jsxs8("div", { class: "tab-strip", role: "tablist", children: [
2381
- /* @__PURE__ */ jsx9(
3001
+ showMineTab && /* @__PURE__ */ jsxs10("div", { class: "tab-strip", role: "tablist", children: [
3002
+ /* @__PURE__ */ jsx11(
2382
3003
  "button",
2383
3004
  {
2384
3005
  type: "button",
@@ -2389,7 +3010,7 @@ function mountWidget(options) {
2389
3010
  children: options.strings["tab.send"]
2390
3011
  }
2391
3012
  ),
2392
- /* @__PURE__ */ jsx9(
3013
+ /* @__PURE__ */ jsx11(
2393
3014
  "button",
2394
3015
  {
2395
3016
  type: "button",
@@ -2399,9 +3020,20 @@ function mountWidget(options) {
2399
3020
  onClick: () => rerender(clearSelected({ ...currentState, tab: "mine" })),
2400
3021
  children: options.strings["tab.mine"]
2401
3022
  }
3023
+ ),
3024
+ /* @__PURE__ */ jsx11(
3025
+ "button",
3026
+ {
3027
+ type: "button",
3028
+ role: "tab",
3029
+ "aria-selected": state.tab === "changelog",
3030
+ class: `tab-button ${state.tab === "changelog" ? "is-active" : ""}`,
3031
+ onClick: () => rerender(clearSelected({ ...currentState, tab: "changelog" })),
3032
+ children: options.strings["tab.changelog"]
3033
+ }
2402
3034
  )
2403
3035
  ] }),
2404
- state.tab === "send" && /* @__PURE__ */ jsx9(
3036
+ state.tab === "send" && /* @__PURE__ */ jsx11(
2405
3037
  Form,
2406
3038
  {
2407
3039
  strings: options.strings,
@@ -2411,7 +3043,7 @@ function mountWidget(options) {
2411
3043
  ...state.error !== void 0 && { errorMessage: state.error }
2412
3044
  }
2413
3045
  ),
2414
- state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx9(
3046
+ state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx11(
2415
3047
  MineList,
2416
3048
  {
2417
3049
  api: options.api,
@@ -2420,7 +3052,7 @@ function mountWidget(options) {
2420
3052
  onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
2421
3053
  }
2422
3054
  ),
2423
- state.tab === "mine" && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx9(
3055
+ (state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx11(
2424
3056
  ReportDetailView,
2425
3057
  {
2426
3058
  api: options.api,
@@ -2429,6 +3061,15 @@ function mountWidget(options) {
2429
3061
  strings: options.strings,
2430
3062
  onBack: () => rerender(clearSelected({ ...currentState }))
2431
3063
  }
3064
+ ),
3065
+ state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx11(
3066
+ ChangelogList,
3067
+ {
3068
+ api: options.api,
3069
+ externalId,
3070
+ strings: options.strings,
3071
+ onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
3072
+ }
2432
3073
  )
2433
3074
  ]
2434
3075
  }
@@ -2501,8 +3142,8 @@ function createFeedback(config) {
2501
3142
  capture_method: screenshot ? manualScreenshot ? "manual" : "html2canvas" : "none",
2502
3143
  technical_context
2503
3144
  };
2504
- if ("0.7.3") {
2505
- payload.widget_version = "0.7.3";
3145
+ if ("0.9.0") {
3146
+ payload.widget_version = "0.9.0";
2506
3147
  }
2507
3148
  if (screenshot) payload.screenshot = screenshot;
2508
3149
  if (values.synthetic) payload.synthetic = true;
@@ -2588,4 +3229,4 @@ function createFeedback(config) {
2588
3229
  export {
2589
3230
  createFeedback
2590
3231
  };
2591
- //# sourceMappingURL=chunk-2VBGH64F.mjs.map
3232
+ //# sourceMappingURL=chunk-KO5NHJ7J.mjs.map