@mhosaic/feedback 0.34.0 → 0.36.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.
@@ -197,7 +197,12 @@ async function loadAndAttach(config, deferred) {
197
197
  // flag > on. The loader provider forwards its `errorTracking` prop here
198
198
  // rather than wrapping host-side, so there's exactly one arm point (the
199
199
  // real instance) — no deferred/real double-registration.
200
- errorTracking: config.errorTracking ?? manifest.config?.error_tracking ?? true
200
+ errorTracking: config.errorTracking ?? manifest.config?.error_tracking ?? true,
201
+ // Roadmap #17: operator-flipped behavior toggles ride the manifest;
202
+ // an explicit host value still wins.
203
+ ...config.openToCurrentPageFeedback === void 0 && typeof manifest.config?.behavior?.openToCurrentPageFeedback === "boolean" ? {
204
+ openToCurrentPageFeedback: manifest.config.behavior.openToCurrentPageFeedback
205
+ } : {}
201
206
  });
202
207
  deferred._attach(real);
203
208
  }
@@ -205,4 +210,4 @@ async function loadAndAttach(config, deferred) {
205
210
  export {
206
211
  createFeedback
207
212
  };
208
- //# sourceMappingURL=chunk-WROPP3IB.mjs.map
213
+ //# sourceMappingURL=chunk-557ACRBU.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/loader/bundleLoader.ts","../src/loader/manifest.ts","../src/loader/deferredApi.ts","../src/loader/index.ts"],"sourcesContent":["/**\n * Inject the widget bundle as an SRI-protected <script> and wait for its\n * window global to appear.\n *\n * Why a <script> tag and not `import()`:\n * `import()` can fetch cross-origin ESM but doesn't honor Subresource\n * Integrity on the dynamic-import call itself. A <script> tag with\n * `integrity=…` is the only path where the browser actually refuses\n * to execute a bundle whose bytes don't match the manifest's hash.\n *\n * The bundle is built with `tsup --format iife --globalName MhosaicFeedback`,\n * so once it parses it sets `window.MhosaicFeedback = { createFeedback }`\n * (per `src/widget.ts`'s exports). We wait for that to appear.\n *\n * Idempotency: if multiple FeedbackProviders mount on the same page (e.g.\n * during a React fast-refresh cycle), we reuse an already-injected bundle\n * instead of fetching a second copy.\n */\n\nimport type { FeedbackApi, FeedbackConfig } from '../types'\nimport type { ErrorTrackingOptions } from '../modules/error-tracking'\n\n/** What the loader may pass to the bundle's `createFeedback`: the public\n * config plus the bundle-only `errorTracking` knob the loader forwards\n * from the manifest. */\nexport type BundleConfig = FeedbackConfig & {\n errorTracking?: boolean | ErrorTrackingOptions\n}\n\ndeclare global {\n interface Window {\n MhosaicFeedback?: {\n createFeedback(config: BundleConfig): FeedbackApi\n }\n }\n}\n\nconst SCRIPT_DATA_ATTR = 'data-mhosaic-feedback-bundle'\n\nexport interface BundleLoadOptions {\n bundleUrl: string\n sriHash: string\n /** Default 30 s. Long enough for slow networks; short enough to surface\n * CDN outages quickly. */\n timeoutMs?: number\n}\n\nexport interface BundleHandle {\n createFeedback(config: BundleConfig): FeedbackApi\n}\n\nexport async function injectBundle(\n opts: BundleLoadOptions,\n): Promise<BundleHandle> {\n // Reuse a bundle already loaded this page.\n if (window.MhosaicFeedback?.createFeedback) {\n return window.MhosaicFeedback\n }\n\n const existing = document.querySelector<HTMLScriptElement>(\n `script[${SCRIPT_DATA_ATTR}]`,\n )\n if (existing) {\n // Another loader instance started the fetch; wait for the global.\n return waitForGlobal(opts.timeoutMs ?? 30_000)\n }\n\n return new Promise<BundleHandle>((resolve, reject) => {\n const script = document.createElement('script')\n script.src = opts.bundleUrl\n script.integrity = opts.sriHash\n script.crossOrigin = 'anonymous'\n script.async = true\n script.setAttribute(SCRIPT_DATA_ATTR, '1')\n script.onload = () => {\n if (window.MhosaicFeedback?.createFeedback) {\n resolve(window.MhosaicFeedback)\n } else {\n reject(\n new Error(\n 'mhosaic-feedback: bundle loaded but window.MhosaicFeedback.createFeedback is missing — bundle/loader version mismatch?',\n ),\n )\n }\n }\n script.onerror = () => {\n reject(\n new Error(\n `mhosaic-feedback: failed to load bundle from ${opts.bundleUrl} ` +\n '(network, CSP, or SRI mismatch)',\n ),\n )\n }\n document.head.appendChild(script)\n\n const timeoutMs = opts.timeoutMs ?? 30_000\n setTimeout(() => {\n reject(\n new Error(`mhosaic-feedback: bundle load timeout after ${timeoutMs}ms`),\n )\n }, timeoutMs)\n })\n}\n\nfunction waitForGlobal(timeoutMs: number): Promise<BundleHandle> {\n return new Promise((resolve, reject) => {\n const start = Date.now()\n const tick = () => {\n if (window.MhosaicFeedback?.createFeedback) {\n resolve(window.MhosaicFeedback)\n return\n }\n if (Date.now() - start > timeoutMs) {\n reject(\n new Error(\n 'mhosaic-feedback: timed out waiting for bundle global (another loader instance started the fetch but never finished)',\n ),\n )\n return\n }\n setTimeout(tick, 50)\n }\n tick()\n })\n}\n","/**\n * Fetch the widget-manifest from the Mhosaic backend.\n *\n * The loader calls this on every page load. The backend reads `Project.\n * pinned_version` (or current stable) + `Project.widget_enabled` and\n * returns the bundle URL + SRI hash for the version this tenant should\n * receive — or `{enabled: false}` if the widget is killed for the tenant.\n *\n * Network failures are surfaced to the caller. The loader degrades to\n * \"widget not present\" rather than crashing the host page.\n */\n\nexport interface Manifest {\n /** False when the widget is disabled for this project (kill switch) or\n * no stable release exists. Loader bails silently. */\n enabled: boolean\n /** Semver of the bundle being served. Present when enabled=true. */\n version?: string\n /** Fully-qualified URL to the version-pinned bundle (jsDelivr). */\n bundle_url?: string\n /** Subresource integrity hash for the bundle. The loader injects the\n * script tag with `integrity=…` so the browser refuses to run a\n * bundle whose bytes don't match. */\n sri_hash?: string\n /** Static config the manifest endpoint passes through to the widget. */\n config?: {\n endpoint: string\n project_slug: string\n share_reports_with_widget: boolean\n /** Phase 4: project gates the widget per end-user (default false). */\n requires_visibility_check?: boolean\n /** Per-project switch for the bundle's default error capture. When the\n * backend omits it, the bundle falls back to its own default (on). */\n error_tracking?: boolean\n behavior?: Record<string, boolean>\n }\n /** Human-readable detail when `enabled` is false. */\n detail?: string\n}\n\nexport async function fetchManifest(\n endpoint: string,\n apiKey: string,\n signal?: AbortSignal,\n): Promise<Manifest> {\n const base = endpoint.replace(/\\/$/, '')\n const url = `${base}/api/feedback/v1/widget-manifest/?pk=${encodeURIComponent(apiKey)}`\n // credentials: 'omit' — we authenticate via the public pk_proj_ query\n // param; sending cookies would just trigger CORS preflights for no\n // reason and is unnecessary since the manifest endpoint doesn't read\n // any session.\n const init: RequestInit = { credentials: 'omit' }\n if (signal) init.signal = signal\n const res = await fetch(url, init)\n if (!res.ok) {\n throw new Error(\n `mhosaic-feedback: manifest fetch failed (HTTP ${res.status})`,\n )\n }\n const body = (await res.json()) as Manifest\n return body\n}\n","/**\n * A FeedbackApi-shaped object that queues method calls until the real\n * widget bundle finishes loading, then drains the queue against the real\n * implementation.\n *\n * Why we need this: hosts that already have code like\n * const fb = createFeedback({...})\n * fb.identify({id, email, name})\n * useEffect(() => fb.open(...), [])\n * shouldn't have to refactor for async. The loader's createFeedback\n * returns sync — a deferred handle that records intent, then replays it\n * once the real bundle arrives. Idiomatic queue-then-flush pattern.\n *\n * If the bundle never loads (network failure, SRI mismatch, kill switch),\n * `_fail()` is called instead. Void methods (identify/setMetadata/open/...)\n * silently drop in that case — the widget just doesn't appear. The\n * `submit` Promise rejects so callers can surface real errors.\n */\n\nimport type {\n FeedbackApi,\n ReportPayload,\n SubmittedReport,\n UserIdentity,\n} from '../types'\n\ntype VoidMethod = 'show' | 'hide' | 'shutdown'\ntype ArgMethod = 'open' | 'identify' | 'setMetadata'\n\ntype Queued =\n | { kind: 'void'; method: VoidMethod }\n | { kind: 'arg'; method: 'open'; arg: Parameters<FeedbackApi['open']>[0] }\n | { kind: 'arg'; method: 'identify'; arg: UserIdentity }\n | { kind: 'arg'; method: 'setMetadata'; arg: Record<string, unknown> }\n | {\n kind: 'submit'\n payload: Partial<ReportPayload> & { description: string }\n resolve: (r: SubmittedReport) => void\n reject: (e: unknown) => void\n }\n\nexport interface DeferredHandle extends FeedbackApi {\n /** Called by the loader once the bundle is loaded and a real API exists. */\n _attach(real: FeedbackApi): void\n /** Called by the loader if the bundle fails to load. */\n _fail(err: Error): void\n /** Buffered transformer registry — forwarded to the real instance on\n * attach, so wrapper modules (withReplay) work over loader installs. */\n _registerTransformer(fn: unknown): void\n}\n\nexport function createDeferredApi(): DeferredHandle {\n let real: FeedbackApi | null = null\n let failure: Error | null = null\n const queue: Queued[] = []\n // Transformers (withReplay & friends) registered before the bundle\n // attaches. Forwarded to the real instance's registry on _attach so\n // host-side wrappers work over a loader install too (roadmap #7).\n const pendingTransformers: unknown[] = []\n\n function flush(item: Queued): void {\n if (real) {\n if (item.kind === 'void') {\n ;(real[item.method] as () => void)()\n } else if (item.kind === 'arg') {\n ;(real[item.method] as (a: unknown) => void)(item.arg)\n } else {\n // submit\n Promise.resolve(real.submit(item.payload)).then(\n item.resolve,\n item.reject,\n )\n }\n return\n }\n if (failure) {\n if (item.kind === 'submit') item.reject(failure)\n // Void / arg methods silently dropped — the widget never appeared.\n return\n }\n queue.push(item)\n }\n\n return {\n show() {\n flush({ kind: 'void', method: 'show' })\n },\n hide() {\n flush({ kind: 'void', method: 'hide' })\n },\n shutdown() {\n flush({ kind: 'void', method: 'shutdown' })\n },\n open(arg) {\n flush({ kind: 'arg', method: 'open', arg })\n },\n identify(arg) {\n flush({ kind: 'arg', method: 'identify', arg })\n },\n setMetadata(arg) {\n flush({ kind: 'arg', method: 'setMetadata', arg })\n },\n submit(payload) {\n return new Promise<SubmittedReport>((resolve, reject) => {\n flush({ kind: 'submit', payload, resolve, reject })\n })\n },\n _registerTransformer(fn: unknown) {\n const target = real as { _registerTransformer?: (f: unknown) => void } | null\n if (target && typeof target._registerTransformer === 'function') {\n target._registerTransformer(fn)\n return\n }\n if (failure) return // widget never appeared — nothing to transform\n pendingTransformers.push(fn)\n },\n _attach(impl) {\n real = impl\n const target = impl as { _registerTransformer?: (f: unknown) => void }\n if (typeof target._registerTransformer === 'function') {\n for (const fn of pendingTransformers) target._registerTransformer(fn)\n }\n pendingTransformers.length = 0\n while (queue.length > 0) flush(queue.shift()!)\n },\n _fail(err) {\n failure = err\n pendingTransformers.length = 0\n // Drain any pending submits with the failure; drop void/arg.\n for (const item of queue) {\n if (item.kind === 'submit') item.reject(err)\n }\n queue.length = 0\n },\n }\n}\n","/**\n * `@mhosaic/feedback/loader` — public entry for the loader architecture.\n *\n * Same shape as the direct-import path (`@mhosaic/feedback`), but the\n * widget bundle is fetched at runtime from a CDN URL the Mhosaic backend\n * dictates via the manifest endpoint. Letting hosts switch from the\n * direct-import path to the loader path gives them auto-updates without\n * any further code changes — Mhosaic ships a new Release row, every host\n * picks up the new bundle on next page load.\n *\n * Public surface MUST mirror the direct-import path exactly:\n * - `createFeedback(config)` returns a `FeedbackApi`-shaped handle.\n * - Method calls before the bundle finishes loading queue up and replay.\n * - `submit()` returns a Promise that resolves after the bundle is up\n * and the real submit completes.\n *\n * If the manifest endpoint reports the widget is disabled for this\n * project (`enabled: false` — kill switch or no stable release configured),\n * the deferred handle silently no-ops void calls and rejects `submit()`\n * calls with a clear error. The host page never crashes.\n */\n\nimport type { FeedbackApi, FeedbackConfig } from '../types'\nimport type { ErrorTrackingOptions } from '../modules/error-tracking'\nimport { injectBundle } from './bundleLoader'\nimport { fetchManifest } from './manifest'\nimport { createDeferredApi, type DeferredHandle } from './deferredApi'\n\nexport type { FeedbackApi, FeedbackConfig } from '../types'\nexport type { Manifest } from './manifest'\n\n/** Loader config = the public widget config plus the loader-only knobs the\n * host can set to override what the manifest dictates. */\nexport type LoaderConfig = FeedbackConfig & {\n /** Override the bundle's default error capture. Omit (or pass `undefined`)\n * to let the project's manifest flag decide (which itself defaults on). */\n errorTracking?: boolean | ErrorTrackingOptions | undefined\n}\n\nexport function createFeedback(config: LoaderConfig): FeedbackApi {\n const deferred = createDeferredApi()\n loadAndAttach(config, deferred).catch((err: Error) => {\n // Surface to console — host devtools is the canonical channel for\n // widget bootstrap errors. Production telemetry is the bundle's job\n // (it can't run if it never loaded), so console.warn is the floor.\n // eslint-disable-next-line no-console\n console.warn('[mhosaic-feedback] widget did not load:', err.message)\n deferred._fail(err)\n })\n return deferred\n}\n\nasync function loadAndAttach(\n config: LoaderConfig,\n deferred: DeferredHandle,\n): Promise<void> {\n const manifest = await fetchManifest(config.endpoint, config.apiKey)\n if (!manifest.enabled || !manifest.bundle_url || !manifest.sri_hash) {\n // Disabled by Mhosaic (kill switch) or no stable release. Surface a\n // clear failure so submit() callers see an error, but no host crash.\n deferred._fail(\n new Error(\n `mhosaic-feedback: widget disabled for this project${\n manifest.detail ? ` — ${manifest.detail}` : ''\n }`,\n ),\n )\n return\n }\n const bundle = await injectBundle({\n bundleUrl: manifest.bundle_url,\n sriHash: manifest.sri_hash,\n })\n const real: FeedbackApi = bundle.createFeedback({\n ...config,\n // Forward the project's per-end-user gating flag from the manifest unless\n // the host set it explicitly. The widget then runs the visibility check.\n requiresVisibilityCheck:\n config.requiresVisibilityCheck ??\n manifest.config?.requires_visibility_check ??\n false,\n // Error capture is armed INSIDE the bundle (the only auto-updating part\n // of a loader install), so already-deployed hosts pick it up without a\n // redeploy. Precedence: explicit host override > per-project manifest\n // flag > on. The loader provider forwards its `errorTracking` prop here\n // rather than wrapping host-side, so there's exactly one arm point (the\n // real instance) — no deferred/real double-registration.\n errorTracking:\n config.errorTracking ?? manifest.config?.error_tracking ?? true,\n // Roadmap #17: operator-flipped behavior toggles ride the manifest;\n // an explicit host value still wins.\n ...(config.openToCurrentPageFeedback === undefined &&\n typeof manifest.config?.behavior?.openToCurrentPageFeedback === 'boolean'\n ? {\n openToCurrentPageFeedback:\n manifest.config.behavior.openToCurrentPageFeedback,\n }\n : {}),\n })\n deferred._attach(real)\n}\n"],"mappings":"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","names":[]}
@@ -850,6 +850,10 @@ var DEFAULT_STRINGS = {
850
850
  "detail.tech.errors": "Runtime errors",
851
851
  "detail.tech.console": "Console (last 20)",
852
852
  "detail.tech.network": "Network (last 15)",
853
+ "detail.verified.badge": "Agent-verified",
854
+ "detail.verified.title": "Proof of fix",
855
+ "detail.verified.functional": "Functional check",
856
+ "detail.verified.ui": "UI check",
853
857
  "status.new": "New",
854
858
  "status.in_progress": "In progress",
855
859
  "status.awaiting_validation": "Awaiting your validation",
@@ -1050,6 +1054,10 @@ var FRENCH_STRINGS = {
1050
1054
  "detail.tech.errors": "Erreurs d\u2019ex\xE9cution",
1051
1055
  "detail.tech.console": "Console (20 derniers)",
1052
1056
  "detail.tech.network": "R\xE9seau (15 derniers)",
1057
+ "detail.verified.badge": "V\xE9rifi\xE9 par l\u2019agent",
1058
+ "detail.verified.title": "Preuve de correction",
1059
+ "detail.verified.functional": "Fonctionnel v\xE9rifi\xE9",
1060
+ "detail.verified.ui": "Interface v\xE9rifi\xE9e",
1053
1061
  "status.new": "Nouveau",
1054
1062
  "status.in_progress": "En cours",
1055
1063
  "status.awaiting_validation": "En attente de validation",
@@ -1621,7 +1629,10 @@ function ReportDetailView({
1621
1629
  "\u2190 ",
1622
1630
  strings["detail.back"]
1623
1631
  ] }),
1624
- /* @__PURE__ */ jsx3("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1632
+ /* @__PURE__ */ jsxs3("span", { class: "report-detail-header-pills", children: [
1633
+ /* @__PURE__ */ jsx3("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status }),
1634
+ detail.verification && /* @__PURE__ */ jsx3("span", { class: "pill pill-verified", children: strings["detail.verified.badge"] })
1635
+ ] })
1625
1636
  ] }),
1626
1637
  variant === "board" && /* @__PURE__ */ jsxs3("div", { class: "report-detail-header report-detail-header--board", children: [
1627
1638
  /* @__PURE__ */ jsxs3(
@@ -1637,7 +1648,10 @@ function ReportDetailView({
1637
1648
  ]
1638
1649
  }
1639
1650
  ),
1640
- /* @__PURE__ */ jsx3("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1651
+ /* @__PURE__ */ jsxs3("span", { class: "report-detail-header-pills", children: [
1652
+ /* @__PURE__ */ jsx3("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status }),
1653
+ detail.verification && /* @__PURE__ */ jsx3("span", { class: "pill pill-verified", children: strings["detail.verified.badge"] })
1654
+ ] })
1641
1655
  ] }),
1642
1656
  /* @__PURE__ */ jsxs3("div", { class: "report-detail-body", children: [
1643
1657
  /* @__PURE__ */ jsx3(ContextBlock, { detail, strings }),
@@ -1711,6 +1725,15 @@ function ReportDetailView({
1711
1725
  }
1712
1726
  ) : /* @__PURE__ */ jsx3("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx3("img", { src: url, alt: "", loading: "lazy", onError: () => unpinUrl(pinKey) }) });
1713
1727
  }),
1728
+ detail.verification && /* @__PURE__ */ jsx3(
1729
+ VerificationPanel,
1730
+ {
1731
+ verification: detail.verification,
1732
+ strings,
1733
+ pinUrl,
1734
+ unpinUrl
1735
+ }
1736
+ ),
1714
1737
  /* @__PURE__ */ jsx3("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1715
1738
  detail.comments.length === 0 ? /* @__PURE__ */ jsx3("p", { class: "report-detail-empty", children: strings["detail.no_replies"] }) : /* @__PURE__ */ jsx3("ul", { class: "report-comments", children: detail.comments.map((c) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsx3(
1716
1739
  CommentBubble,
@@ -1879,6 +1902,68 @@ function TechnicalContextDrawer({ ctx, strings }) {
1879
1902
  ] })
1880
1903
  ] });
1881
1904
  }
1905
+ function VerificationPanel({
1906
+ verification,
1907
+ strings,
1908
+ pinUrl,
1909
+ unpinUrl
1910
+ }) {
1911
+ return /* @__PURE__ */ jsxs3("details", { class: "report-detail-verification", children: [
1912
+ /* @__PURE__ */ jsx3("summary", { children: strings["detail.verified.title"] }),
1913
+ /* @__PURE__ */ jsxs3("div", { class: "verification-body", children: [
1914
+ /* @__PURE__ */ jsx3("p", { class: "verification-evidence", children: verification.evidence }),
1915
+ /* @__PURE__ */ jsxs3("div", { class: "verification-meta", children: [
1916
+ /* @__PURE__ */ jsxs3("span", { children: [
1917
+ verification.functional_ok ? "\u2713" : "\u2717",
1918
+ " ",
1919
+ strings["detail.verified.functional"]
1920
+ ] }),
1921
+ /* @__PURE__ */ jsxs3("span", { children: [
1922
+ verification.ui_ok ? "\u2713" : "\u2717",
1923
+ " ",
1924
+ strings["detail.verified.ui"]
1925
+ ] }),
1926
+ /* @__PURE__ */ jsxs3("span", { children: [
1927
+ verification.environment,
1928
+ verification.app_version ? ` \xB7 ${verification.app_version}` : ""
1929
+ ] })
1930
+ ] }),
1931
+ verification.screenshots.length > 0 && /* @__PURE__ */ jsx3("div", { class: "verification-shots", children: verification.screenshots.map((s, i) => {
1932
+ const pinKey = `verif:${i}`;
1933
+ const url = pinUrl(pinKey, s.url);
1934
+ if (!url) return null;
1935
+ const safeHref = safeExternalHref(url);
1936
+ return safeHref ? /* @__PURE__ */ jsx3(
1937
+ "a",
1938
+ {
1939
+ class: "report-detail-screenshot",
1940
+ href: safeHref,
1941
+ target: "_blank",
1942
+ rel: "noopener noreferrer",
1943
+ children: /* @__PURE__ */ jsx3(
1944
+ "img",
1945
+ {
1946
+ src: url,
1947
+ alt: s.caption ?? "",
1948
+ loading: "lazy",
1949
+ onError: () => unpinUrl(pinKey)
1950
+ }
1951
+ )
1952
+ },
1953
+ i
1954
+ ) : /* @__PURE__ */ jsx3("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx3(
1955
+ "img",
1956
+ {
1957
+ src: url,
1958
+ alt: s.caption ?? "",
1959
+ loading: "lazy",
1960
+ onError: () => unpinUrl(pinKey)
1961
+ }
1962
+ ) }, i);
1963
+ }) })
1964
+ ] })
1965
+ ] });
1966
+ }
1882
1967
  function DeviceSection({
1883
1968
  device,
1884
1969
  strings
@@ -3381,26 +3466,20 @@ function Form({
3381
3466
  const files = Array.from(e.dataTransfer?.files ?? []);
3382
3467
  if (files.length) acceptFiles(files);
3383
3468
  };
3384
- useEffect5(() => {
3385
- const zone = dropZoneRef.current;
3386
- if (!zone) return;
3387
- const onPaste = (e) => {
3388
- const items = e.clipboardData?.items;
3389
- if (!items) return;
3390
- for (const item of Array.from(items)) {
3391
- if (item.kind === "file" && item.type.startsWith("image/")) {
3392
- const file = item.getAsFile();
3393
- if (file) {
3394
- e.preventDefault();
3395
- acceptFiles([file]);
3396
- return;
3397
- }
3398
- }
3469
+ const handlePaste = (e) => {
3470
+ const items = e.clipboardData?.items;
3471
+ if (!items) return;
3472
+ const files = [];
3473
+ for (const item of Array.from(items)) {
3474
+ if (item.kind === "file" && item.type.startsWith("image/")) {
3475
+ const file = item.getAsFile();
3476
+ if (file) files.push(file);
3399
3477
  }
3400
- };
3401
- zone.addEventListener("paste", onPaste);
3402
- return () => zone.removeEventListener("paste", onPaste);
3403
- }, [shots.length]);
3478
+ }
3479
+ if (!files.length) return;
3480
+ e.preventDefault();
3481
+ acceptFiles(files);
3482
+ };
3404
3483
  const handleAnnotated = (annotated) => {
3405
3484
  const index = annotatingIndex;
3406
3485
  if (index === null || !shots[index]) {
@@ -3430,7 +3509,7 @@ function Form({
3430
3509
  }
3431
3510
  onSubmit(values);
3432
3511
  };
3433
- return /* @__PURE__ */ jsxs9("form", { onSubmit: handleSubmit, children: [
3512
+ return /* @__PURE__ */ jsxs9("form", { onSubmit: handleSubmit, onPaste: handlePaste, children: [
3434
3513
  /* @__PURE__ */ jsx9("h2", { children: strings["form.title"] }),
3435
3514
  /* @__PURE__ */ jsxs9("div", { class: "field", children: [
3436
3515
  /* @__PURE__ */ jsx9("label", { for: "mfb-desc", children: strings["form.description.label"] }),
@@ -3495,16 +3574,44 @@ function Form({
3495
3574
  class: "btn btn--primary screenshot-annotate",
3496
3575
  onClick: () => setAnnotatingIndex(index),
3497
3576
  children: [
3498
- /* @__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: [
3499
- /* @__PURE__ */ jsx9("path", { d: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" }),
3500
- /* @__PURE__ */ jsx9("path", { d: "M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" })
3501
- ] }),
3577
+ /* @__PURE__ */ jsxs9(
3578
+ "svg",
3579
+ {
3580
+ width: "14",
3581
+ height: "14",
3582
+ viewBox: "0 0 24 24",
3583
+ fill: "none",
3584
+ stroke: "currentColor",
3585
+ "stroke-width": "2",
3586
+ "stroke-linecap": "round",
3587
+ "stroke-linejoin": "round",
3588
+ "aria-hidden": "true",
3589
+ children: [
3590
+ /* @__PURE__ */ jsx9("path", { d: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" }),
3591
+ /* @__PURE__ */ jsx9("path", { d: "M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" })
3592
+ ]
3593
+ }
3594
+ ),
3502
3595
  strings["form.screenshot.annotate"]
3503
3596
  ]
3504
3597
  }
3505
3598
  ),
3506
3599
  /* @__PURE__ */ jsxs9("button", { type: "button", class: "btn", onClick: () => removeShot(index), children: [
3507
- /* @__PURE__ */ jsx9("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__ */ jsx9("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" }) }),
3600
+ /* @__PURE__ */ jsx9(
3601
+ "svg",
3602
+ {
3603
+ width: "14",
3604
+ height: "14",
3605
+ viewBox: "0 0 24 24",
3606
+ fill: "none",
3607
+ stroke: "currentColor",
3608
+ "stroke-width": "2",
3609
+ "stroke-linecap": "round",
3610
+ "stroke-linejoin": "round",
3611
+ "aria-hidden": "true",
3612
+ children: /* @__PURE__ */ jsx9("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" })
3613
+ }
3614
+ ),
3508
3615
  strings["form.screenshot.remove"]
3509
3616
  ] })
3510
3617
  ] })
@@ -3528,11 +3635,26 @@ function Form({
3528
3635
  onDragLeave: handleDragLeave,
3529
3636
  onDrop: handleDrop,
3530
3637
  children: [
3531
- /* @__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: [
3532
- /* @__PURE__ */ jsx9("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
3533
- /* @__PURE__ */ jsx9("polyline", { points: "17 8 12 3 7 8" }),
3534
- /* @__PURE__ */ jsx9("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
3535
- ] }),
3638
+ /* @__PURE__ */ jsxs9(
3639
+ "svg",
3640
+ {
3641
+ class: "screenshot-icon",
3642
+ width: "28",
3643
+ height: "28",
3644
+ viewBox: "0 0 24 24",
3645
+ fill: "none",
3646
+ stroke: "currentColor",
3647
+ "stroke-width": "1.6",
3648
+ "stroke-linecap": "round",
3649
+ "stroke-linejoin": "round",
3650
+ "aria-hidden": "true",
3651
+ children: [
3652
+ /* @__PURE__ */ jsx9("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
3653
+ /* @__PURE__ */ jsx9("polyline", { points: "17 8 12 3 7 8" }),
3654
+ /* @__PURE__ */ jsx9("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
3655
+ ]
3656
+ }
3657
+ ),
3536
3658
  /* @__PURE__ */ jsxs9("div", { class: "screenshot-cta", children: [
3537
3659
  /* @__PURE__ */ jsx9("strong", { children: strings["form.screenshot.cta_click"] }),
3538
3660
  ", ",
@@ -4928,6 +5050,10 @@ var WIDGET_STYLES = `
4928
5050
  .pill-status--awaiting_validation { background: #faf5ff; color: #6b21a8; border-color: #e9d5ff; }
4929
5051
  .pill-status--closed { background: #ecfdf5; color: #065f46; border-color: #a7f3d0; }
4930
5052
  .pill-status--rejected, .pill-status--wontfix, .pill-status--duplicate { background: #f3f4f6; color: #374151; border-color: #e5e7eb; }
5053
+ /* Agent-verified fix badge (Phase B) \u2014 emerald, same family as
5054
+ .pill-status--closed but its own modifier since it layers alongside
5055
+ the status pill rather than replacing it. */
5056
+ .pill-verified { background: #ecfdf5; color: #047857; border-color: #a7f3d0; }
4931
5057
  .pill-type { background: var(--mfb-surface); color: var(--mfb-text-muted); border-color: var(--mfb-border); }
4932
5058
  .pill-severity { background: var(--mfb-surface); color: var(--mfb-text-muted); border-color: var(--mfb-border); }
4933
5059
  .pill-severity--blocker { background: #fef2f2; color: #991b1b; border-color: #fecaca; }
@@ -4942,6 +5068,7 @@ var WIDGET_STYLES = `
4942
5068
  justify-content: space-between;
4943
5069
  gap: 8px;
4944
5070
  }
5071
+ .report-detail-header-pills { display: inline-flex; align-items: center; gap: 6px; }
4945
5072
  .report-detail-body { display: flex; flex-direction: column; gap: 10px; }
4946
5073
  .report-detail-description {
4947
5074
  font-size: 14px;
@@ -5295,12 +5422,14 @@ button.report-detail-edit {
5295
5422
  /* Transparency drawer \u2014 closed by default. The user can click open to
5296
5423
  verify what their browser sent: device, errors, console tail, network
5297
5424
  tail. Read-only; purely a trust-building gesture. */
5298
- .report-detail-tech {
5425
+ .report-detail-tech,
5426
+ .report-detail-verification {
5299
5427
  border: 1px solid var(--mfb-border);
5300
5428
  border-radius: var(--mfb-radius);
5301
5429
  background: var(--mfb-surface);
5302
5430
  }
5303
- .report-detail-tech > summary {
5431
+ .report-detail-tech > summary,
5432
+ .report-detail-verification > summary {
5304
5433
  cursor: pointer;
5305
5434
  padding: 8px 10px;
5306
5435
  font-size: 11px;
@@ -5310,15 +5439,19 @@ button.report-detail-edit {
5310
5439
  user-select: none;
5311
5440
  list-style: none;
5312
5441
  }
5313
- .report-detail-tech > summary::-webkit-details-marker { display: none; }
5314
- .report-detail-tech > summary::before {
5442
+ .report-detail-tech > summary::-webkit-details-marker,
5443
+ .report-detail-verification > summary::-webkit-details-marker { display: none; }
5444
+ .report-detail-tech > summary::before,
5445
+ .report-detail-verification > summary::before {
5315
5446
  content: '\u25B8';
5316
5447
  display: inline-block;
5317
5448
  margin-right: 6px;
5318
5449
  transition: transform 120ms;
5319
5450
  }
5320
- .report-detail-tech[open] > summary::before { transform: rotate(90deg); }
5321
- .tech-body {
5451
+ .report-detail-tech[open] > summary::before,
5452
+ .report-detail-verification[open] > summary::before { transform: rotate(90deg); }
5453
+ .tech-body,
5454
+ .verification-body {
5322
5455
  padding: 0 10px 10px 10px;
5323
5456
  display: flex;
5324
5457
  flex-direction: column;
@@ -5398,6 +5531,31 @@ button.report-detail-edit {
5398
5531
  .tech-network-url { word-break: break-all; }
5399
5532
  .tech-network-time { text-align: right; color: var(--mfb-text-muted); font-variant-numeric: tabular-nums; }
5400
5533
 
5534
+ /* Proof-of-fix drawer (Phase B) \u2014 shares the collapsible shell rules
5535
+ with .report-detail-tech above (grouped selectors): closed by
5536
+ default, rotating \u25B8 marker. Surfaces an agent's verification pass
5537
+ so the submitter can see the evidence behind the "V\xE9rifi\xE9 par
5538
+ l'agent" badge. */
5539
+ .verification-evidence {
5540
+ margin: 0;
5541
+ font-size: 12px;
5542
+ white-space: pre-wrap;
5543
+ }
5544
+ .verification-meta {
5545
+ display: flex;
5546
+ gap: 12px;
5547
+ flex-wrap: wrap;
5548
+ font-size: 11px;
5549
+ color: var(--mfb-text-muted);
5550
+ }
5551
+ .verification-shots {
5552
+ display: flex;
5553
+ gap: 8px;
5554
+ flex-wrap: wrap;
5555
+ }
5556
+ .verification-shots .report-detail-screenshot { width: 96px; flex: 0 0 auto; }
5557
+ .verification-shots .report-detail-screenshot img { max-height: 96px; }
5558
+
5401
5559
  /* KPI strip \u2014 four pills above the My Reports list. Each is clickable
5402
5560
  to filter the rows below. Borrowed from thePnr's FeedbackKPICards. */
5403
5561
  .kpi-strip {
@@ -6564,8 +6722,8 @@ function createFeedback(config) {
6564
6722
  capture_method: captureMethod,
6565
6723
  technical_context
6566
6724
  };
6567
- if ("0.34.0") {
6568
- payload.widget_version = "0.34.0";
6725
+ if ("0.36.0") {
6726
+ payload.widget_version = "0.36.0";
6569
6727
  }
6570
6728
  if (manualScreenshots?.length) {
6571
6729
  payload.screenshots = manualScreenshots;
@@ -6596,7 +6754,7 @@ function createFeedback(config) {
6596
6754
  throw error;
6597
6755
  }
6598
6756
  }
6599
- const handle = mountWidget({
6757
+ const mountOptions = {
6600
6758
  host,
6601
6759
  strings,
6602
6760
  showFAB: config.showFAB ?? true,
@@ -6624,7 +6782,25 @@ function createFeedback(config) {
6624
6782
  ...config.showPageActivity !== void 0 && {
6625
6783
  showPageActivity: config.showPageActivity
6626
6784
  }
6627
- });
6785
+ };
6786
+ const handle = mountWidget(mountOptions);
6787
+ if (config.openToCurrentPageFeedback === void 0 && config.showFAB !== false) {
6788
+ const behaviorFetch = config.fetchImpl ?? globalThis.fetch;
6789
+ void (async () => {
6790
+ try {
6791
+ const r = await behaviorFetch(
6792
+ `${config.endpoint}/api/feedback/v1/widget-manifest/?pk=${encodeURIComponent(config.apiKey)}`
6793
+ );
6794
+ if (!r.ok) return;
6795
+ const m = await r.json();
6796
+ const v = m?.config?.behavior?.openToCurrentPageFeedback;
6797
+ if (typeof v === "boolean" && mountOptions.openToCurrentPageFeedback === void 0) {
6798
+ mountOptions.openToCurrentPageFeedback = v;
6799
+ }
6800
+ } catch {
6801
+ }
6802
+ })();
6803
+ }
6628
6804
  let qaHandle;
6629
6805
  let qaDisposed = false;
6630
6806
  if (config.qaMeter?.source) {
@@ -6691,4 +6867,4 @@ function createFeedback(config) {
6691
6867
  export {
6692
6868
  createFeedback
6693
6869
  };
6694
- //# sourceMappingURL=chunk-J45ACCSP.mjs.map
6870
+ //# sourceMappingURL=chunk-T36VC7TZ.mjs.map