PageSourceSearch

https://flowscrape2.onrender.com/app/mvpApp.js

js flowscrape2.onrender.com collected 2026-10-03 11:40:26 UTC 314,568 bytes, 6,413 lines download raw bytes

1(() => {
2  const appInitStartedAt = performance.now();
3  let appInitCompletedMs = null;
4  const E = (id) => document.getElementById(id);
5  const explorerMode = document.body?.dataset.mvpMode === "explorer";
6  const routerMode = document.body?.dataset.mvpMode === "router";
7  const t = (key, variables, fallback = key) => window.FlowScrapeI18n?.t(key, variables) || fallback;
8  const log = (event, payload = {}) => {
9    console.info(`[webconsole][app-mvp] ${event}`, payload);
10  };
11  const warn = (event, payload = {}) => {
12    if (typeof console.warn === "function") {
13      console.warn(`[webconsole][app-mvp] ${event}`, payload);
14      return;
15    }
16    log(event, payload);
17  };
18
19  const htmlInput = E("mvpHtmlInput");
20  const fallbackPanel = E("mvpFallbackPanel");
21  const loadingPanel = E("mvpLoadingPanel");
22  const successPanel = E("mvpSuccessPanel");
23  const previewDetails = E("mvpPreviewDetails");
24  const discoveryPreviewSlot = E("mvpDiscoveryPreviewSlot");
25  const reviewPreviewSlot = E("mvpReviewPreviewSlot");
26  const previewWrap = E("mvpPreviewWrap");
27  const previewSummary = E("mvpPreviewSummary");
28  const previewMore = E("mvpPreviewMore");
29  const previewAccountGate = E("mvpPreviewAccountGate");
30  const previewSearch = E("mvpPreviewSearch");
31  const previewEmpty = E("mvpPreviewEmpty");
32  const previewTable = E("mvpPreviewTable");
33  const previewBody = E("mvpPreviewBody");
34  const previewProductSelect = E("mvpPreviewProductSelect");
35  const detailsLog = E("mvpDetailsLog");
36  const stateTitle = E("mvpResultsTitle");
37  const resultSource = E("mvpResultSource");
38  const resultStatus = E("mvpResultStatus");
39  const resultBanner = E("mvpResultBanner");
40  const stateMessage = E("mvpStateMessage");
41  const stateHint = E("mvpStateHint");
42  const discoveryCoveragePanel = E("mvpDiscoveryCoverage");
43  const continueScanButton = E("mvpContinueScanBtn");
44  const stopDiscoveryButton = E("mvpStopDiscoveryBtn");
45  const continueScanStatus = E("mvpContinueScanStatus");
46  const continueScanLoading = E("mvpContinueScanLoading");
47  const continueScanProgress = E("mvpContinueScanProgress");
48  const continueScanLoadingStatus = E("mvpContinueScanLoadingStatus");
49  const datasetActions = E("mvpDatasetActions");
50  const datasetRecommendation = E("mvpDatasetRecommendation");
51  const overviewEnrichment = E("mvpOverviewEnrichment");
52  const productReviewSection = E("mvpProductReviewSection");
53  const technicalSection = E("mvpResultDetails");
54  const technicalContent = E("mvpTechnicalContent");
55  const currentDatasetState = E("mvpCurrentDatasetState");
56  const downloadsPanel = E("mvpDownloadsPanel");
57  const freeFilesPanel = E("mvpFreeFiles");
58  const continuationGate = E("mvpContinuationGate");
59  const downloadsTitle = E("mvpDownloadsTitle");
60  const currentDraftExport = E("mvpCurrentDraftExport");
61  const cleanupPanel = E("mvpCleanupPanel");
62  const recommendedAction = E("mvpNextActions");
63  const completedSignupCta = E("mvpCompletedSignupCta");
64  const reviewBlockingIssues = E("mvpReviewBlockingIssues");
65  const enrichProductsButton = E("mvpEnrichProductsBtn");
66  const enrichmentAction = E("mvpEnrichmentAction");
67  const enrichmentSummary = E("mvpEnrichmentSummary");
68  const enrichmentStatusPanel = E("mvpEnrichmentStatusPanel");
69  const enrichmentStatusState = E("mvpEnrichmentStatusState");
70  const enrichmentProgress = E("mvpEnrichmentProgress");
71  const enrichmentStatus = E("mvpEnrichmentStatus");
72  const marketingContentStep = E("mvpMarketingContentStep");
73  const openMarketingContentButton = E("mvpOpenMarketingContent");
74  const marketingContentAuthSupport = E("mvpMarketingContentAuthSupport");
75  const marketingContentWorkspace = E("mvpMarketingContentWorkspace");
76  const marketingProductSelect = E("mvpMarketingProductSelect");
77  const marketingContextFields = E("mvpMarketingContextFields");
78  const marketingImages = E("mvpMarketingImages");
79  const marketingName = E("mvpMarketingName");
80  const marketingPrice = E("mvpMarketingPrice");
81  const marketingBrand = E("mvpMarketingBrand");
82  const marketingSku = E("mvpMarketingSku");
83  const marketingDescription = E("mvpMarketingDescription");
84  const marketingHeadline = E("mvpMarketingHeadline");
85  const marketingCta = E("mvpMarketingCta");
86  const prepareMarketingRequestButton = E("mvpPrepareMarketingRequest");
87  const marketingContentStatus = E("mvpMarketingContentStatus");
88  const marketingTemplatePreview = E("mvpMarketingTemplatePreview");
89  const marketingDownload = E("mvpMarketingDownload");
90  const marketingCustomize = E("mvpMarketingCustomize");
91  const loadingTitle = E("mvpLoadingTitle");
92  const loadingMessage = E("mvpLoadingMessage");
93  const loadingReassurance = E("mvpLoadingReassurance");
94  const loadingBar = E("mvpLoadingBar");
95  const startAnotherScanButton = E("mvpStartAnotherScan");
96  const scanReassurance = E("mvpScanReassurance");
97  const cleanupRequestLink = E("mvpCleanupRequestLink");
98  const cleanupTitle = E("mvpCleanupTitle");
99  const cleanupDescription = E("mvpCleanupDescription");
100  const cleanupStatus = E("mvpCleanupStatus");
101  const cleanupDownload = E("mvpCleanupDownload");
102  const cleanupPreview = E("mvpCleanupPreview");
103
104  // Keep diagnostic and advanced controls behind the single customer-facing
105  // Technical details disclosure without changing their existing behavior.
106  // Keep completed-result sections in their authored hierarchy. The Shopify
107  // preparation control belongs with the next steps, not technical details.
108  if (recommendedAction && cleanupPanel) recommendedAction.appendChild(cleanupPanel);
109  if (productReviewSection) productReviewSection.hidden = true;
110  const catalogPotentialPanel = E("mvpCatalogPotential");
111  if (successPanel && catalogPotentialPanel) successPanel.appendChild(catalogPotentialPanel);
112
113  const issueSummaryTitle = E("mvpIssueSummaryTitle");
114  const noReportMessage = E("mvpNoReportMessage");
115  const freeRunsStatus = E("mvpFreeRunsStatus");
116  const signedInFreeRuns = E("mvpSignedInFreeRuns");
117  const paidRunsStatus = E("mvpPaidRunsStatus");
118  const runButton = E("mvpRunBtn");
119  const explorerTabs = E("mvpExplorerTabs");
120  const explorerPages = E("mvpExplorerPages");
121  const accountHeading = E("mvpAccountHeading");
122  const accountPanel = E("mvpAccountPanel");
123  const accountCloseButton = E("mvpAccountClose");
124  const navSignedOut = E("mvpNavSignedOut");
125  const navSignedIn = E("mvpNavSignedIn");
126  const navSignInButton = E("mvpNavSignIn");
127  const navCreateAccountButton = E("mvpNavCreateAccount");
128  const navAccountButton = E("mvpNavAccount");
129  const navAccountLabel = E("mvpNavAccountLabel");
130  const navSignOutButton = E("mvpNavSignOut");
131  const authState = E("mvpAuthState");
132  const authMessage = E("mvpAuthMessage");
133  const checkoutMessage = E("mvpCheckoutMessage");
134  const authEmail = E("mvpAuthEmail");
135  const authEmailField = E("mvpAuthEmailField");
136  const authPassword = E("mvpAuthPassword");
137  const authPasswordField = E("mvpAuthPasswordField");
138  const authConfirmPassword = E("mvpAuthConfirmPassword");
139  const authConfirmPasswordField = E("mvpAuthConfirmPasswordField");
140  const authPasswordHint = E("mvpAuthPasswordHint");
141  const signInButton = E("mvpSignInBtn");
142  const signUpButton = E("mvpSignUpBtn");
143  const forgotPasswordButton = E("mvpForgotPasswordBtn");
144  const signOutButton = E("mvpSignOutBtn");
145  const checkoutButton = E("mvpCheckoutBtn");
146  const checkoutButtons = Array.from(document.querySelectorAll("[data-mvp-checkout-package]"));
147  const checkoutRetentionMessage = E("mvpCheckoutRetentionMessage");
148  const lowBalanceMessage = E("mvpLowBalanceMessage");
149  const addRunsTrigger = E("mvpAddRunsTrigger");
150  const addRunsLabel = E("mvpAddRunsLabel");
151  const addRunsContent = E("mvpAddRunsContent");
152  const paymentSuccess = E("mvpPaymentSuccess");
153  const authForm = E("mvpAuthForm");
154  const accountSummary = E("mvpAccountSummary");
155  const myRunsButton = E("mvpMyRunsBtn");
156  const runHistory = E("mvpRunHistory");
157  const runHistoryClose = E("mvpRunHistoryClose");
158  const runHistoryStatus = E("mvpRunHistoryStatus");
159  const runHistoryList = E("mvpRunHistoryList");
160  const uploadTab = E("mvpUploadTab");
161  const urlTab = E("mvpUrlTab");
162  const uploadPanel = E("mvpUploadPanel");
163  const urlPanel = E("mvpUrlPanel");
164  const urlActionRow = E("mvpUrlActionRow");
165  const csvFileInput = E("mvpCsvFileInput");
166  const urlInput = E("mvpUrlInput");
167  const intentInputs = Array.from(document.querySelectorAll('input[name="mvpIntent"]'));
168  const inspectFileButton = E("mvpInspectFileBtn");
169  const discoveryNotify = E("mvpDiscoveryNotify");
170  const discoveryEmailField = E("mvpDiscoveryEmailField");
171  const discoveryEmail = E("mvpDiscoveryEmail");
172  const discoveryNotificationStatus = E("mvpDiscoveryNotificationStatus");
173  const AUTHENTICATED_EXHAUSTED_MESSAGE = "You’re out of credits. Add credits to run this action.";
174  const CHECKOUT_CANCELLED_MESSAGE = "Checkout cancelled. No credits were added.";
175  const CHECKOUT_SIGN_IN_MESSAGE = "Sign in to finish adding your credits.";
176  const SIGNUPS_DISABLED_MESSAGE = "Signups are not enabled yet. Please try signing in or contact support.";
177  const RECOVERY_INVALID_MESSAGE = "This password reset link is invalid or has expired. Request a new one.";
178  const RESET_REQUESTED_MESSAGE = "If an account exists for that email, we sent a password reset link.";
179  const MIN_PASSWORD_LENGTH = 8;
180  const ADD_RUNS_PREFERENCE_KEY = "fsMvpAddRunsPreference";
181  const ACTIVE_DISCOVERY_STORAGE_KEY = "fsMvpActiveDiscovery";
182  const ANONYMOUS_COMPLETED_RUN_STORAGE_KEY = "fsMvpAnonymousCompletedRun";
183  const RECOVERY_POLL_MAX_MS = 2 * 60 * 1000;
184  const recoveryPanel = E("mvpRecoveryPanel");
185  const recoveryTitle = E("mvpRecoveryTitle");
186  const recoveryProgress = E("mvpRecoveryProgress");
187  const recoveryStatus = E("mvpRecoveryStatus");
188  const recoveryContinue = E("mvpRecoveryContinue");
189  const recoveryStartNew = E("mvpRecoveryStartNew");
190  let paymentSuccessTimer = null;
191  let completedSignupCtaEligible = false;
192  let accountPanelOpener = null;
193  const AUTH_BUTTON_LABELS = {
194    signin: "Sign in",
195    signup: "Create account",
196    signout: "Sign out",
197  };
198  const supabaseState = {
199    client: null,
200    session: null,
201    configured: false,
202    authBusy: null,
203    checkoutBusy: false,
204    checkoutPackageId: "",
205    pendingCheckoutSessionId: "",
206    checkoutCompleteStarted: false,
207  };
208  let activeInitialScanJobId = "";
209  let activePreScanLoading = null;
210  let activeDiscoveryJobId = "";
211  let activeRunRecoveryPending = true;
212  let resolveActiveRunRecovery;
213  const activeRunRecoveryReady = new Promise((resolve) => { resolveActiveRunRecovery = resolve; });
214  const activeRunObservers = new Map();
215  const activeSubmissionUrls = new Set();
216  let recoveryCandidate = null;
217  let cachedRunHistory = [];
218  let runHistoryLoaded = false;
219  const DOWNLOAD_LINKS = [
220    ["mvpShopifyDownload", "shopifyCsv", "mvpShopifyDownloadItem"],
221    ["mvpBigcommerceDownload", "bigcommerceCsv", "mvpBigcommerceDownloadItem"],
222    ["mvpCsvDownload", "exportCsv", "mvpCsvDownloadItem"],
223    ["mvpCatalogMapDownload", "catalogMapCsv", "mvpCatalogMapDownloadItem"],
224    ["mvpImportReadinessReportPdfDownload", "importReadinessReportPdf", "mvpImportReadinessReportPdfDownloadItem"],
225    ["mvpImportReadinessReportDownload", "importReadinessReport", "mvpImportReadinessReportDownloadItem"],
226    ["mvpIssuesDownload", "issuesCsv", "mvpIssuesDownloadItem"],
227    ["mvpSummaryDownload", "summaryJson", "mvpSummaryDownloadItem"],
228    ["mvpLlmDownload", "llmReadyJson", "mvpLlmDownloadItem"],
229    ["mvpEnrichmentDownload", "enrichment", "mvpEnrichmentDownloadItem"],
230  ];
231  const ANONYMOUS_PRODUCT_LIMIT = 10;
232
233  const STATE_COPY = {
234    success: {
235      title: "Product dataset ready for review",
236      message: "",
237      banner: "0 products collected. 0 are structurally importable. 0 are available for review or enrichment.",
238      hint: "",
239    },
240    warning: {
241      title: "Product dataset ready for review",
242      message: "",
243      banner: "0 products collected. 0 are structurally importable. 0 are available for review or enrichment.",
244      hint: "",
245    },
246    failure: {
247      title: "No usable products found",
248      message: "Try a category or collection page, wait until products load, then upload the saved page.",
249      banner: "No usable import file created.",
250      hint: "",
251    },
252    capture: {
253      title: "Capture page after it loads",
254      message: "Automatic extraction needs the saved page to continue.",
255      banner: "No usable import file created.",
256      hint: "Open the product listing page, wait until products are visible, then save and upload the complete page.",
257    },
258  };
259  const VISIBLE_LISTING_DESCRIPTION_NOTE = "Descriptions are limited because this run used listing/category page data.";
260  let inputMode = "url";
261  let authMode = "signin";
262  let claimInProgress = false;
263  let claimedToken = "";
264  let previewedToken = "";
265  let anonymousCompletedRunId = "";
266  let anonymousClaimPromise = null;
267
268  const claimTokenFromLocation = () => {
269    try {
270      return String(new URL(window.location.href).searchParams.get("claim") || "").trim();
271    } catch {
272      return "";
273    }
274  };
275  let lastUsage = null;
276  let lastMvpResult = null;
277  let resultWorkflowState = null;
278  let enrichmentInProgress = false;
279  let lastDiscoveryModel = null;
280  let lastMvpResultViewModel = null;
281  let selectedMvpIntent = "extraction";
282  let continuationScanActive = false;
283
284  const explorerPopulatedFieldCount = (product) => {
285    const context = normalizeMarketingProductContext(product);
286    return [context.name, context.sku, context.price, context.productUrl, context.description, context.brand, context.category, ...context.images].filter(Boolean).length;
287  };
288
289  if (routerMode) {
290    const input = E("mvpRouterUrl");
291    const check = E("mvpRouterCheck");
292    const loading = E("mvpRouterLoading");
293    const result = E("mvpRouterResult");
294    const escapeHtml = (value) => String(value || "").replace(/[&<>'"]/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;" })[char]);
295    const renderInspection = (inspection, submittedUrl) => {
296      const pageLabel = ({ product: "Product page", category: "Category page", homepage: "Homepage" })[inspection.pageType] || "Page type not confirmed";
297      const routeLabel = ({ "direct-html": "✓ Direct HTML", browser: "◐ Browser", "protected-browser": "◐ Enhanced retrieval", "fallback-input": "Fallback input" })[inspection.route.strategy];
298      const strategy = inspection.route.strategy;
299      const tasks = strategy === "fallback-input" ? [] : inspection.pageType === "product"
300        ? [["instagram-story", "Instagram Story"], ["product-pdf", "Product PDF"], ["ai-json", "AI-ready JSON"]]
301        : inspection.pageType === "category" ? [["shopify", "Shopify CSV"], ["ai-json", "AI-ready JSON"]]
302        : ["browser", "protected-browser"].includes(strategy) ? [["ai-json", "AI-ready JSON"]] : [];
303      result.innerHTML = `<section class="mvp-router-site"><h2>${pageLabel}</h2><p>${routeLabel}</p></section>
304        ${tasks.length ? `<fieldset><legend>Create</legend>${tasks.map(([id, label]) => `<a class="button" data-task="${id}" href="/app-mvp2?${escapeHtml(new URLSearchParams({ url: submittedUrl || "", strategy, task: id }).toString())}"${["instagram-story", "product-pdf", "ai-json"].includes(id) ? ' target="_blank" rel="noopener noreferrer"' : ""}>${label}</a>`).join(" ")}</fieldset>` : strategy === "fallback-input" ? `<a class="button button--primary" href="/app-mvp-legacy?url=${encodeURIComponent(submittedUrl || "")}">Open legacy workspace</a>` : ""}
305        <details class="mvp-router-technical"><summary>Technical details</summary><pre>${escapeHtml(JSON.stringify(inspection, null, 2))}</pre></details>`;
306    };
307    check?.addEventListener("click", async () => {
308      const url = String(input?.value || "").trim();
309      if (!url) { result.hidden = false; result.textContent = "Paste a website URL to check."; return; }
310      loading.hidden = false; result.hidden = true; check.disabled = true;
311      try {
312        const response = await fetch("/api/mvp/inspect-site", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }) });
313        const data = await response.json();
314        if (!response.ok || !data?.ok) throw new Error(data?.error || "Website check failed.");
315        if (String(input?.value || "").trim() !== url) return;
316        renderInspection(data.inspection, url); result.hidden = false;
317      } catch (error) { result.textContent = error?.message || "Website check failed."; result.hidden = false; }
318      finally { loading.hidden = true; check.disabled = false; }
319    });
320    input?.addEventListener("input", () => { result.hidden = true; result.replaceChildren(); });
321    input?.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); check?.click(); } });
322    return;
323  }
324  const renderExplorerPages = (discovery) => {
325    if (!explorerMode || !explorerPages) return;
326    const body = explorerPages.querySelector("tbody");
327    if (!body) return;
328    body.replaceChildren();
329    const pages = Array.isArray(discovery?.pages) ? discovery.pages : [];
330    for (const page of pages) {
331      const row = document.createElement("tr");
332      const url = document.createElement("td");
333      const link = document.createElement("a");
334      link.href = String(page?.url || ""); link.target = "_blank"; link.rel = "noopener noreferrer"; link.textContent = String(page?.url || "—");
335      url.append(link); row.append(url);
336      [discoveryLabel(page?.pageType || "unknown"), String(finiteCount(page?.productsFound)), discoveryLabel(page?.status || "unknown")].forEach((value) => { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); });
337      body.append(row);
338    }
339  };
340  const setExplorerTab = (tab) => {
341    if (!explorerMode) return;
342    const preview = E("mvpReviewPreviewSlot");
343    const structure = E("mvpDiscoveryCoverage");
344    if (preview) preview.hidden = tab !== "products";
345    if (structure) structure.hidden = tab !== "structure";
346    if (explorerPages) explorerPages.hidden = tab !== "pages" || !explorerPages.querySelector("tbody")?.children.length;
347    explorerTabs?.querySelectorAll("[data-mvp-explorer-tab]").forEach((button) => button.classList.toggle("is-active", button.dataset.mvpExplorerTab === tab));
348  };
349  let foregroundGeneration = 0;
350  const isForeground = (meta) => meta?.foregroundGeneration == null || meta.foregroundGeneration === foregroundGeneration;
351  const assertForeground = (meta) => {
352    if (!isForeground(meta)) throw new Error("Scan view detached");
353  };
354  let scanActive = false;
355  const blockAuthDuringScan = () => {
356    if (!scanActive || isSignedIn()) return false;
357    const message = E("mvpScanAuthMessage");
358    if (message) {
359      message.textContent = t("mvp.auth.scanRunning", null, "Your scan is still running. Wait for it to finish before signing in so you can keep these results.");
360      message.hidden = false;
361    }
362    return true;
363  };
364  const setScanActive = (active, { keepIntentHidden = active } = {}) => {
365    scanActive = active;
366    if (active) {
367      completedSignupCtaEligible = false;
368      if (completedSignupCta) completedSignupCta.hidden = true;
369    }
370    setAuthBusy(supabaseState.authBusy);
371    const intentSelector = E("mvpIntentSelector");
372    if (intentSelector) intentSelector.hidden = keepIntentHidden;
373    if (startAnotherScanButton) startAnotherScanButton.hidden = !active;
374  };
375  const claimPreviewOnlyFromLocation = () => {
376    try {
377      return new URL(window.location.href).searchParams.get("preview") === "1";
378    } catch {
379      return false;
380    }
381  };
382  const prefillUrlFromLocation = () => {
383    try {
384      const value = String(new URL(window.location.href).searchParams.get("url") || "").trim();
385      if (value && urlInput) urlInput.value = value;
386    } catch {}
387  };
388
389  startAnotherScanButton?.addEventListener("click", () => {
390    const url = new URL(window.location.href);
391    url.pathname = "/app-mvp";
392    for (const param of ["claim", "preview", "url", "run", "resume", "expires"]) {
393      url.searchParams.delete(param);
394    }
395    window.location.replace(url.href);
396  });
397
398  const discoveryNotificationPayload = () => {
399    if (!discoveryNotify?.checked) return { notifyByEmail: false };
400    const accountEmail = String(lastUsage?.email || supabaseState.session?.user?.email || "").trim();
401    return { notifyByEmail: true, notificationEmail: accountEmail || String(discoveryEmail?.value || "").trim() };
402  };
403  discoveryNotify?.addEventListener("change", () => {
404    const hasAccountEmail = Boolean(String(lastUsage?.email || supabaseState.session?.user?.email || "").trim());
405    if (discoveryEmailField) discoveryEmailField.hidden = !discoveryNotify.checked || hasAccountEmail;
406  });
407  let lastCompletedPackage = null;
408  const marketingContentState = {
409    sourceData: null,
410    products: [],
411    productsComplete: false,
412    selectedIndex: 0,
413    creativeOverrides: null,
414    selectedImage: "",
415    preparedRequest: null,
416    primary: false,
417    productsLoadPromise: null,
418  };
419
420  const entitlementState = (usage = lastUsage) => {
421    const authenticated = usage?.authenticated === true || isSignedIn();
422    return {
423      canStartUrlExtraction: true,
424      canContinueScan: authenticated,
425    };
426  };
427
428  const productDatasetContent = E("mvpProductDatasetContent");
429  const recommendedEnrichmentSlot = E("mvpRecommendedEnrichmentSlot");
430  const renderFreeRunStatus = () => {
431    if (!freeRunsStatus) return;
432    const authenticated = lastUsage?.authenticated === true || isSignedIn();
433    freeRunsStatus.textContent = inputMode === "upload" ? "Scanning and viewing results are free." : "";
434    freeRunsStatus.hidden = authenticated || inputMode !== "upload";
435    if (signedInFreeRuns) {
436      signedInFreeRuns.textContent = "Scanning and viewing results are free.";
437      signedInFreeRuns.hidden = true;
438    }
439  };
440
441  const setInputMode = (mode) => {
442    const uploadActive = mode === "upload";
443    inputMode = uploadActive ? "upload" : "url";
444    uploadTab?.classList.toggle("is-active", uploadActive);
445    urlTab?.classList.toggle("is-active", !uploadActive);
446    uploadTab?.setAttribute("aria-selected", uploadActive ? "true" : "false");
447    urlTab?.setAttribute("aria-selected", uploadActive ? "false" : "true");
448    if (uploadPanel) uploadPanel.hidden = !uploadActive;
449    if (urlPanel) urlPanel.hidden = uploadActive;
450    if (fallbackPanel) fallbackPanel.hidden = uploadActive;
451    if (urlActionRow) urlActionRow.hidden = uploadActive;
452    if (uploadActive) renderDiscoveryCoverage(null);
453    renderFreeRunStatus();
454    renderCheckoutRetentionMessage();
455  };
456
457  const hasCurrentScan = () => Boolean(
458    String(urlInput?.value || "").trim()
459    || csvFileInput?.files?.[0]
460    || lastCompletedPackage
461  );
462
463  function renderCheckoutRetentionMessage() {
464    if (checkoutRetentionMessage) checkoutRetentionMessage.hidden = !hasCurrentScan();
465  }
466
467  const setResultSourceLabel = (requestMeta = null) => {
468    if (!requestMeta) renderCompletedScanSource(null);
469    if (!resultSource) return;
470    if (!requestMeta) {
471      resultSource.hidden = true;
472      return;
473    }
474    resultSource.textContent = requestMeta?.sourceType === "upload"
475      ? "Product dataset ready from uploaded CSV"
476      : requestMeta?.url
477        ? `Scan results for: ${requestMeta.url}`
478        : "Product dataset ready for review";
479    resultSource.hidden = false;
480  };
481
482  const FRIENDLY_REASON = {
483    invalid_url: "Use a full product listing URL that starts with http or https.",
484    fetch_failed: "We could not open this page automatically.",
485    fetch_timeout: "The page took too long to open automatically.",
486    http_error: "The page did not allow automatic access.",
487    non_html_response: "The URL did not return a product listing page.",
488    empty_html: "The page returned no usable content.",
489    too_few_products: "Automatic extraction found too few products.",
490    low_usable_row_rate: "Automatic extraction found product rows, but too many are missing links or images.",
491  };
492
493  const setDetails = (value) => {
494    if (detailsLog) detailsLog.textContent = value;
495  };
496
497  const LOADING_COPY = {
498    url: {
499      title: explorerMode ? "Starting" : t("mvp.loading.urlTitle", null, "Starting scan…"),
500      message: explorerMode ? "Exploring website" : t("mvp.loading.urlMessage", null, "Preparing the scan request…"),
501      label: "Building supplier product dataset",
502    },
503    upload: {
504      title: "Building product dataset from supplier CSV...",
505      message: "Reading supplier rows, organizing available product data, and preparing reviewable outputs...",
506      label: "Building product dataset from supplier CSV",
507    },
508  };
509
510  const setPackageLoading = (isLoading, mode = "url") => {
511    if (!loadingPanel) return;
512    const copy = LOADING_COPY[mode] || LOADING_COPY.url;
513    if (loadingTitle) loadingTitle.textContent = copy.title;
514    if (loadingMessage) loadingMessage.textContent = copy.message;
515    if (loadingBar) loadingBar.setAttribute("aria-label", copy.label);
516    if (loadingBar) loadingBar.hidden = mode === "url" || !isLoading;
517    if (loadingReassurance) {
518      loadingReassurance.textContent = "";
519      loadingReassurance.hidden = true;
520    }
521    loadingPanel.hidden = !isLoading;
522    loadingPanel.setAttribute("aria-busy", isLoading ? "true" : "false");
523  };
524
525  const setScanProgress = (pagesScanned = 0, pagesDiscovered = 0) => {
526    if (!continueScanProgress) return;
527    const progress = Math.max(0, Math.min(100, pagesDiscovered > 0 ? (pagesScanned / pagesDiscovered) * 100 : 0));
528    continueScanProgress.setAttribute("aria-valuemin", "0");
529    continueScanProgress.setAttribute("aria-valuemax", "100");
530    continueScanProgress.setAttribute("aria-valuenow", String(Math.round(progress)));
531    const progressBar = continueScanProgress.querySelector("span");
532    if (progressBar) progressBar.style.width = `${progress}%`;
533  };
534
535  const setUrlGenerationLoading = (isLoading) => {
536    setPackageLoading(false, "url");
537    if (successPanel && isLoading) successPanel.style.display = "grid";
538    if (continueScanLoading) continueScanLoading.hidden = !isLoading;
539    if (continueScanProgress) continueScanProgress.hidden = !isLoading;
540    if (continueScanButton && isLoading) continueScanButton.hidden = true;
541    if (datasetActions && isLoading) datasetActions.hidden = true;
542    if (isLoading) {
543      setScanProgress();
544      if (continueScanLoadingStatus) continueScanLoadingStatus.textContent = "Preparing the scan request…";
545    } else {
546      setScanProgress(1, 1);
547      if (continueScanLoading) continueScanLoading.hidden = true;
548      if (continueScanProgress) continueScanProgress.hidden = true;
549      if (scanReassurance) scanReassurance.hidden = true;
550    }
551  };
552
553  const startInitialScanLoading = (startedAt) => {
554    const configuredSlowMs = Number(globalThis.__MVP_SLOW_START_DELAY_MS__);
555    const configuredLaterMs = Number(globalThis.__MVP_LONG_START_DELAY_MS__);
556    const slowMs = Number.isFinite(configuredSlowMs) && configuredSlowMs >= 0 ? configuredSlowMs : 10_000;
557    const laterMs = Number.isFinite(configuredLaterMs) && configuredLaterMs >
557= 0 ? configuredLaterMs : 30_000;
558    const configuredStageMs = Number(globalThis.__MVP_PRE_SCAN_STAGE_DELAY_MS__);
559    const stageMs = Number.isFinite(configuredStageMs) && configuredStageMs >= 0 ? configuredStageMs : 2_000;
560    const stages = [
561      { progress: 2, message: explorerMode ? "Starting" : "Starting catalog scan…" },
562      { progress: 6, message: explorerMode ? "Exploring website" : "Preparing website scan…" },
563      { progress: 10, message: explorerMode ? "Exploring website" : "Understanding website structure…" },
564      { progress: 14, message: explorerMode ? "Finding product pages" : "Building catalog map…" },
565      { progress: 18, message: explorerMode ? "Finding product pages" : "Looking for product pages…" },
566      { progress: 20, message: explorerMode ? "Collecting product data" : "Scanning catalog pages…" },
567    ];
568    let jobStatus = "queued";
569    let stageIndex = -1;
570    let displayedProgress = 0;
571    let usingRealProgress = false;
572    let timer = null;
573
574    const showProgress = (progress) => {
575      displayedProgress = Math.max(displayedProgress, Math.max(0, Math.min(100, progress)));
576      setScanProgress(displayedProgress, 100);
577    };
578
579    const render = () => {
580      const running = jobStatus === "running";
581      const complete = ["completed", "succeeded"].includes(jobStatus);
582      if (loadingTitle) loadingTitle.textContent = complete ? (explorerMode ? "Ready" : "Catalog discovery complete") : running ? (explorerMode ? "Collecting product data" : "Scanning catalog") : (explorerMode ? "Starting" : "Catalog scan queued");
583      if (loadingMessage) loadingMessage.textContent = complete
584        ? "Loading the completed catalog results."
585        : running
586          ? "Scanning catalog pages and collecting products."
587          : "Waiting for a scanner to become available.";
588      if (continueScanLoadingStatus && !usingRealProgress) {
589        continueScanLoadingStatus.textContent = complete
590          ? "Loading the completed catalog results."
591          : stages[stageIndex].message;
592      }
593      if (!loadingReassurance) return;
594      const elapsedMs = Math.max(0, Date.now() - startedAt);
595      const reassurance = elapsedMs >= Math.min(slowMs, laterMs) ? "You can safely leave this page." : "";
596      loadingReassurance.textContent = reassurance;
597      loadingReassurance.hidden = true;
598      if (scanReassurance) {
599        scanReassurance.textContent = reassurance;
600        scanReassurance.hidden = !reassurance;
601      }
602    };
603
604    const advancePreparation = () => {
605      if (usingRealProgress) return;
606      if (stageIndex < stages.length - 1) stageIndex += 1;
607      showProgress(stages[stageIndex].progress);
608      render();
609    };
610    advancePreparation();
611    timer = setInterval(advancePreparation, stageMs);
612    return {
613      updateStatus(status) {
614        jobStatus = String(status || "queued").toLowerCase();
615        render();
616      },
617      updateDiscovery(job = {}, payload = {}) {
618        const sources = [job, job?.progress, job?.discoveryJob, payload, payload?.progress, payload?.discoveryJob];
619        const firstCount = (keys) => {
620          for (const source of sources) {
621            for (const key of keys) {
622              const value = Number(source?.[key]);
623              if (Number.isFinite(value) && value >= 0) return value;
624            }
625          }
626          return null;
627        };
628        const productsFound = firstCount(["productsFound", "products_found"]);
629        const pagesAttempted = firstCount(["pagesAttempted", "pages_attempted", "pagesScanned", "pages_scanned"]);
630        const pagesDiscovered = firstCount(["pagesDiscovered", "pages_discovered", "candidatePagesFound", "candidate_pages_found"]);
631        if (!(productsFound > 0 || pagesAttempted !== null || pagesDiscovered !== null)) return;
632        usingRealProgress = true;
633        if (timer !== null) clearInterval(timer);
634        timer = null;
635        if (productsFound !== null) {
636          const metricProducts = E("mvpMetricProducts");
637          if (metricProducts) metricProducts.textContent = String(productsFound);
638          setText("mvpOverviewProducts", productsFound);
639          const progressSummary = E("mvpCatalogProgressSummary");
640          if (progressSummary) progressSummary.textContent = `${productsFound} ${productsFound === 1 ? "product" : "products"}`;
641        }
642        const realProgress = pagesDiscovered > 0 && pagesAttempted !== null
643          ? (pagesAttempted / pagesDiscovered) * 100
644          : 0;
645        showProgress(realProgress);
646        if (continueScanLoadingStatus) continueScanLoadingStatus.textContent = "Scanning catalog pages…";
647      },
648      stop() {
649        if (timer !== null) clearInterval(timer);
650        timer = null;
651      },
652    };
653  };
654
655  const setAuthMessage = (message, isError = false) => {
656    if (!authMessage) return;
657    authMessage.textContent = message;
658    authMessage.hidden = !message;
659    authMessage.classList.toggle("mvp-auth-message--error", Boolean(isError));
660  };
661
662  const setCheckoutMessage = (message, isError = false) => {
663    if (!checkoutMessage) return;
664    checkoutMessage.textContent = message;
665    checkoutMessage.hidden = !message;
666    checkoutMessage.classList.toggle("mvp-checkout-message--error", Boolean(isError));
667  };
668
669  const accessToken = () => String(supabaseState.session?.access_token || "").trim();
670
671  const mergeAuthHeaders = (headers = {}, token = "") => {
672    const merged = new Headers(headers || {});
673    if (token) merged.set("Authorization", `Bearer ${token}`);
674    return merged;
675  };
676
677  const readCurrentSession = async () => {
678    if (!supabaseState.client) return supabaseState.session;
679    try {
680      const { data, error } = await supabaseState.client.auth.getSession();
681      if (!error) supabaseState.session = data?.session || null;
682    } catch {}
683    return supabaseState.session;
684  };
685
686  const refreshCurrentSession = async () => {
687    if (!supabaseState.client) return readCurrentSession();
688    try {
689      const refreshed = await supabaseState.client.auth.refreshSession();
690      if (!refreshed?.error) supabaseState.session = refreshed?.data?.session || null;
691    } catch {}
692    return supabaseState.session;
693  };
694
695  const authenticatedFetch = async (url, options = {}, metadata = {}) => {
696    const requestOptions = { ...options };
697    const makeRequest = async (refresh = false) => {
698      const session = refresh ? await refreshCurrentSession() : await readCurrentSession();
699      const token = String(session?.access_token || "").trim();
700      const headers = mergeAuthHeaders(options.headers, token);
701      return fetch(url, { ...requestOptions, headers });
702    };
703
704    let response = await makeRequest(false);
705    if (response.status === 401) response = await makeRequest(true);
706    return response;
707  };
708
709  const authHeaders = (headers = {}) => mergeAuthHeaders(headers, accessToken());
710
711  const parseResponsePayload = async (response) => {
712    const text = await response.text().catch(() => "");
713    if (!text) return null;
714    try {
715      return JSON.parse(text);
716    } catch {
717      return { error: text };
718    }
719  };
720
721  const responseError = (response, payload, fallback, metadata = {}) => {
722    const authenticationFailure = response?.status === 401;
723    const message = authenticationFailure
724      ? (metadata.authenticatedRequest
725          ? "Your session expired. Sign in again to load this import package."
726          : "This import package is not available in this browser.")
727      : String(payload?.error || payload?.message || fallback || `Request failed (${response?.status || 0}).`);
728    const error = new Error(message);
729    error.code = payload?.code || metadata.reasonCode || (authenticationFailure
730      ? (metadata.authenticatedRequest ? "session_expired" : "result_access_denied")
731      : "");
732    error.reasonCode = payload?.reasonCode || metadata.reasonCode || "";
733    error.httpStatus = response?.status || 0;
734    error.requestId = payload?.requestId || metadata.requestId || null;
735    error.runId = payload?.runId || metadata.runId || null;
736    error.jobId = payload?.jobId || metadata.jobId || null;
737    error.endpointType = metadata.endpointType || "";
738    error.authenticatedRequest = metadata.authenticatedRequest === true;
739    if (authenticationFailure && metadata.authenticatedRequest) setAuthMessage(message, true);
740    return error;
741  };
742
743  const signedInEmail = () => String(supabaseState.session?.user?.email || "").trim();
744
745  const isSignedIn = () => Boolean(signedInEmail() || supabaseState.session?.user?.id);
746
747  const setAccountPanelOpen = (open, { restoreFocus = false } = {}) => {
748    if (!accountPanel || (open && scanActive && !isSignedIn())) return;
749    accountPanel.hidden = !open;
750    [navSignInButton, navCreateAccountButton, navAccountButton].forEach((button) => {
751      button?.setAttribute("aria-expanded", open ? "true" : "false");
752    });
753    if (open) {
754      if (!accountPanelOpener) accountPanelOpener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
755      return;
756    }
757    if (runHistory && !runHistory.hidden) closeRunHistory({ restoreFocus: false });
758    if (restoreFocus) accountPanelOpener?.focus?.();
759    accountPanelOpener = null;
760  };
761
762  const openAccountPanel = (opener) => {
763    if (blockAuthDuringScan()) return;
764    if (opener) accountPanelOpener = opener;
765    setAccountPanelOpen(true);
766    (isSignedIn() && authMode !== "recovery" ? accountCloseButton : (authMode === "recovery" ? authPassword : authEmail))?.focus?.();
767  };
768
769  const isSignupsDisabledError = (message) => {
770    const normalized = String(message || "").trim().toLowerCase();
771    return (
772      normalized.includes("signups not allowed")
773      || normalized.includes("signups are not allowed")
774      || normalized.includes("signup is disabled")
775      || normalized.includes("signups disabled")
776      || normalized.includes("signups are disabled")
777      || normalized.includes("not allowed for this instance")
778    );
779  };
780
781  const authErrorMessageFor = (mode, error) => {
782    const rawMessage = String(error?.message || "").trim();
783    if (mode === "signup" && isSignupsDisabledError(rawMessage)) {
784      warn("mvp_signup_disabled", {
785        provider: "supabase",
786        reasonCode: "signups_disabled",
787        supabaseMessage: rawMessage,
788        operatorAction:
789          "Enable Supabase Email provider and public signups. For MVP testing, Confirm email can be OFF.",
790      });
791      return SIGNUPS_DISABLED_MESSAGE;
792    }
793    const normalized = rawMessage.toLowerCase();
794    if (normalized.includes("invalid login credentials") || normalized.includes("email not confirmed")) {
795      return "The email or password is incorrect, or the email has not been confirmed yet.";
796    }
797    if (normalized.includes("password should be at least") || normalized.includes("weak password")) {
798      return `Use at least ${MIN_PASSWORD_LENGTH} characters.`;
799    }
800    if (normalized.includes("rate limit") || normalized.includes("too many requests")) {
801      return "Too many attempts. Please wait a moment and try again.";
802    }
803    if (normalized.includes("same password") || normalized.includes("different from the old password")) {
804      return "Choose a password you have not used for this account.";
805    }
806    const fallbacks = {
807      signup: "Unable to create your account. Please try again.",
808      reset: "Unable to send a reset link. Please try again.",
809      recovery: "Unable to update your password. Please try again.",
810      signin: "Unable to sign in. Please try again.",
811    };
812    return fallbacks[mode] || "Unable to complete that request. Please try again.";
813  };
814
815  const setAuthBusy = (mode = null) => {
816    supabaseState.authBusy = mode;
817    const busy = Boolean(mode) || (scanActive && !isSignedIn());
818    const signedIn = isSignedIn();
819    const entryBusy = Boolean(mode);
820    const scanNote = E("mvpScanAuthNote");
821    if (scanNote) {
822      scanNote.hidden = !scanActive || signedIn;
823      scanNote.textContent = t("mvp.auth.scanNote", null, "Scan in progress — keep this page open. You can sign in when the scan finishes to save and continue with your results.");
824    }
825    if (!scanActive || signedIn) {
826      const message = E("mvpScanAuthMessage");
827      if (message) message.hidden = true;
828    }
829    if (authEmail) authEmail.disabled = busy || !supabaseState.configured;
830    if (authPassword) authPassword.disabled = busy || !supabaseState.configured;
831    if (authConfirmPassword) authConfirmPassword.disabled = busy || !supabaseState.configured;
832    if (signInButton) {
833      signInButton.disabled = busy || !supabaseState.configured || (signedIn && authMode !== "recovery");
834      signInButton.textContent = mode === "signin"
835        ? "Signing in..."
836        : mode === "signup"
837          ? "Creating account..."
838          : mode === "reset"
839            ? "Sending reset link..."
840            : mode === "recovery"
841              ? "Updating password..."
842              : authMode === "reset"
843                ? "Send reset link"
844                : authMode === "recovery"
845                  ? "Set new password"
846                  : AUTH_BUTTON_LABELS[authMode];
847      signInButton.classList.toggle("button--primary", true);
848      signInButton.classList.toggle("secondary", false);
849    }
850    if (signUpButton) {
851      signUpButton.disabled = busy || !supabaseState.configured || (signedIn && authMode !== "recovery");
852      signUpButton.textContent = authMode === "signin"
853        ? AUTH_BUTTON_LABELS.signup
854        : authMode === "signup"
855          ? "Already have an account? Sign in"
856          : "Back to sign in";
857      signUpButton.classList.toggle("button--primary", false);
858      signUpButton.classList.toggle("secondary", true);
859    }
860    if (forgotPasswordButton) forgotPasswordButton.disabled = busy || !supabaseState.configured;
861    if (signOutButton) {
862      signOutButton.disabled = busy || !supabaseState.configured || !signedIn;
863      signOutButton.textContent = mode === "signout" ? "Signing out..." : AUTH_BUTTON_LABELS.signout;
864    }
865    if (navSignInButton) navSignInButton.disabled = entryBusy || !supabaseState.configured || signedIn;
866    if (navCreateAccountButton) navCreateAccountButton.disabled = entryBusy || !supabaseState.configured || signedIn;
867    for (const id of ["mvpPreviewCreateAccount", "mvpPreviewSignIn", "mvpCompletedSignupCreate", "mvpCompletedSignupSignIn", "mvpCreateAccountEnrich", "mvpSignInEnrich"]) {
868      const button = E(id);
869      if (button) button.disabled = entryBusy || !supabaseState.configured;
870    }
871    if (navAccountButton) navAccountButton.disabled = busy || !supabaseState.configured || !signedIn;
872    if (navSignOutButton) {
873      navSignOutButton.disabled = busy || !supabaseState.configured || !signedIn;
874      navSignOutButton.textContent = mode === "signout" ? "Signing out..." : AUTH_BUTTON_LABELS.signout;
875    }
876    for (const button of checkoutButtons) {
877      button.disabled = supabaseState.checkoutBusy || !supabaseState.configured || !signedIn;
878      const isStarting = supabaseState.checkoutBusy
879        && button.dataset.mvpCheckoutPackage === supabaseState.checkoutPackageId;
880      button.textContent = isStarting ? "Starting checkout..." : button.dataset.mvpCheckoutLabel;
881    }
882  };
883
884  const renderAuthState = () => {
885    const email = signedInEmail();
886    const signedIn = isSignedIn();
887    if (accountHeading) {
888      accountHeading.textContent = authMode === "signup" ? "Create your account" : "Your account";
889      accountHeading.hidden = signedIn && authMode !== "recovery";
890    }
891    renderFreeRunStatus();
892    if (authState) {
893      authState.textContent = signedIn ? (email || "Account") : "Not signed in";
894    }
895    if (navSignedOut) navSignedOut.hidden = signedIn;
896    if (navSignedIn) navSignedIn.hidden = !signedIn;
897    if (navAccountLabel) navAccountLabel.textContent = signedIn ? (email || t("mvp.auth.account", null, "Account")) : t("mvp.auth.account", null, "Account");
898    const recovering = authMode === "recovery";
899    if (authForm) authForm.hidden = signedIn && !recovering;
900    if (accountSummary) accountSummary.hidden = !signedIn || recovering;
901    if (!signedIn && runHistory) runHistory.hidden = true;
902    if (myRunsButton && !signedIn) myRunsButton.setAttribute("aria-expanded", "false");
903    if (authEmailField) authEmailField.hidden = recovering;
904    if (authPasswordField) authPasswordField.hidden = authMode === "reset";
905    if (authConfirmPasswordField) authConfirmPasswordField.hidden = authMode !== "signup" && !recovering;
906    if (authPasswordHint) authPasswordHint.hidden = authMode !== "signup" && !recovering;
907    if (authEmail) authEmail.required = !recovering;
908    if (authPassword) {
909      authPassword.required = authMode !== "reset";
910      authPassword.setAttribute("autocomplete", authMode === "signin" ? "current-password" : "new-password");
911    }
912    if (authConfirmPassword) authConfirmPassword.required = authMode === "signup" || recovering;
913    if (forgotPasswordButton) forgotPasswordButton.hidden = authMode !== "signin";
914    setAuthBusy(supabaseState.authBusy);
915    renderMarketingContentAuthState();
916    if (lastCompletedPackage) renderResultAccessState(isSignedIn());
917  };
918
919  const readAddRunsPreference = () => {
920    try {
921      const preference = window.sessionStorage?.getItem(ADD_RUNS_PREFERENCE_KEY);
922      return preference === "expanded" || preference === "collapsed" ? preference : null;
923    } catch {
924      return null;
925    }
926  };
927
928  const shouldExpandAddRuns = ({ paidRunsRemaining, sessionPreference }) => {
929    if (paidRunsRemaining <= 0) return true;
930    return sessionPreference === "expanded";
931  };
932
933  const setAddRunsExpanded = (expanded, { remember = false } = {}) => {
934    if (addRunsTrigger) addRunsTrigger.setAttribute("aria-expanded", String(expanded));
935    if (addRunsContent) addRunsContent.hidden = !expanded;
936    if (!remember) return;
937    try {
938      window.sessionStorage?.setItem(ADD_RUNS_PREFERENCE_KEY, expanded ? "expanded" : "collapsed");
939    } catch {}
940  };
941
942  const renderAddRunsState = (paidRunsRemaining) => {
943    const paidRuns = Math.max(0, Number(paidRunsRemaining || 0));
944    const label = paidRuns === 0
945      ? "Add credits · No credits remaining"
946      : paidRuns <= 5
947        ? `Add credits · ${paidRuns} remaining`
948        : "Add credits";
949    if (addRunsLabel) addRunsLabel.textContent = label;
950    setAddRunsExpanded(shouldExpandAddRuns({
951      paidRunsRemaining: paidRuns,
952      sessionPreference: readAddRunsPreference(),
953    }));
954  };
955
956  if (addRunsTrigger) {
957    addRunsTrigger.addEventListener("click", () => {
958      const expanded = addRunsTrigger.getAttribute("aria-expanded") === "true";
959      setAddRunsExpanded(!expanded, { remember: true });
960    });
961  }
962
963  const renderUsage = (usage) => {
964    if (!usage) return;
965    lastUsage = usage;
966    const paidCredits = Math.max(0, Number(usage?.paidCreditsRemaining ?? 0));
967    const authenticated = usage?.authenticated === true || isSignedIn();
968    const { canStartUrlExtraction } = entitlementState(usage);
969    renderFreeRunStatus();
970    if (paidRunsStatus) {
971      paidRunsStatus.textContent = `${paidCredits} ${paidCredits === 1 ? "credit" : "credits"} remaining`;
972      paidRunsStatus.hidden = !authenticated;
973    }
974    if (checkoutButton) {
975      checkoutButton.hidden = !authenticated;
976    }
977    if (lowBalanceMessage) lowBalanceMessage.hidden = !authenticated || paidCredits > 5;
978    if (authenticated) renderAddRunsState(paidCredits);
979    if (runButton) {
980      runButton.disabled = !canStartUrlExtraction;
981    }
982    if (authenticated && !canStartUrlExtraction) {
983      const hasPendingCheckout = Boolean(supabaseState.pendingCheckoutSessionId);
984      const statusEl = E("mvpStatus");
985      if (statusEl) {
986        statusEl.textContent = "";
987        statusEl.hidden = true;
988      }
989      if (!hasPendingCheckout) {
990        setCheckoutMessage(AUTHENTICATED_EXHAUSTED_MESSAGE);
991      }
992    } else if (
993      !supabaseState.pendingCheckoutSessionId
994      && /out of credits/i.test(String(checkoutMessage?.textContent || ""))
995    ) {
996      setCheckoutMessage("");
997    }
998    setAuthBusy(supabaseState.authBusy);
999    renderCheckoutRetentionMessage();
1000    if (lastMvpResultViewModel && lastMvpResult) {
1001      renderMvpResultViewModel(lastMvpResultViewModel, lastMvpResult);
1002    }
1003    renderActionAccessGates();
1004  };
1005
1006  const loadUsage = async () => {
1007    try {
1008      const res = isSignedIn()
1009        ? await authenticatedFetch("/api/mvp/usage", {}, { endpointType: "usage" })
1010        : await fetch("/api/mvp/usage");
1011      const usage = await parseResponsePayload(res);
1012      if (res.ok) {
1013        renderUsage(usage);
1014        return usage;
1015      }
1016      if (res.status === 401) setAuthMessage("Session expired. Sign in again.", true);
1017    } catch (err) {
1018      log("mvp_usage_load_failed", { error: err?.message || String(err) });
1019    }
1020    return null;
1021  };
1022
1023  const clearAuthFragment = () => {
1024    try {
1025      const url = new URL(window.location.href);
1026      const fragment = new URLSearchParams(url.hash.replace(/^#/, ""));
1027      if (!fragment.has("access_token") && !fragment.has("refresh_token")) return;
1028      [
1029        "access_token",
1030        "refresh_token",
1031        "expires_at",
1032        "expires_in",
1033        "provider_token",
1034        "provider_refresh_token",
1035        "token_type",
1036        "type",
1037      ].forEach((key) => fragment.delete(key));
1038      const remainingFragment = fragment.toString();
1039      window.history.replaceState(
1040        {},
1041        document.title,
1042        `${url.pathname}${url.search}${remainingFragment ? `#${remainingFragment}` : ""}`
1043      );
1044    } catch {}
1045  };
1046
1047  const initAuth = async () => {
1048    const supabaseUrl = window.__SUPABASE_URL__;
1049    const supabaseKey = window.__SUPABASE_PUBLISHABLE_KEY__;
1050    const createClient = window.supabase?.createClient;
1051    supabaseState.configured = Boolean(supabaseUrl && supabaseKey && typeof createClient === "function");
1052    if (!supabaseState.configured) {
1053      renderAuthState();
1054      setAuthMessage("Sign-in is not configured.", true);
1055      return;
1056    }
1057    supabaseState.client = createClient(supabaseUrl, supabaseKey, {
1058      auth: {
1059        persistSession: true,
1060        autoRefreshToken: true,
1061        detectSessionInUrl: true,
1062      },
1063    });
1064    const { data } = await supabaseState.client.auth.getSession();
1065    supabaseState.session = data?.session || null;
1066    if (supabaseState.session) clearAuthFragment();
1067    const authParam = new URL(window.location.href).searchParams.get("auth");
1068    let preserveAuthMessage = false;
1069    if (authParam === "recovery") {
1070      preserveAuthMessage = true;
1071      if (supabaseState.session) {
1072        authMode = "recovery";
1073        setAuthMessage("Choose a new password for your account.");
1074      } else {
1075        authMode = "reset";
1076        setAuthMessage(RECOVERY_INVALID_MESSAGE, true);
1077      }
1078    } else {
1079      try {
1080        const callbackMessage = window.sessionStorage?.getItem("fsMvpAuthMessage");
1081        if (callbackMessage) {
1082          preserveAuthMessage = true;
1083          window.sessionStorage.removeItem("fsMvpAuthMessage");
1084          setAuthMessage(callbackMessage, true);
1085        }
1086      } catch {}
1087    }
1088    renderAuthState();
1089    if (!preserveAuthMessage) {
1090      setAuthMessage(claimTokenFromLocation() && !isSignedIn()
1091        ? "A supplier dataset is ready. Sign in to open it."
1092        : isSignedIn() ? "" : "Sign in to save your checks and use credits.");
1093    }
1094    if (authParam === "recovery" || (claimTokenFromLocation() && !isSignedIn())) openAccountPanel();
1095    supabaseState.client.auth.onAuthStateChange((event, session) => {
1096      supabaseState.session = session || null;
1097      if (supabaseState.session) clearAuthFragment();
1098      if (event === "PASSWORD_RECOVERY") authMode = "recovery";
1099      renderAuthState();
1100      if (event === "PASSWORD_RECOVERY") {
1101        setAuthMessage("Choose a new password for your account.");
1102      } else if (isSignedIn() && authMode !== "recovery") {
1103        setAuthMessage("");
1104      } else if (event === "SIGNED_OUT") {
1105        setAuthMessage("Signed out.");
1106      }
1107      void handleAuthenticatedTransition();
1108    });
1109  };
1110
1111  const clearCheckoutParams = () => {
1112    try {
1113      const url = new URL(window.location.href);
1114      url.searchParams.delete("checkout");
1115      url.searchParams.delete("session_id");
1116      window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`);
1117    } catch {}
1118  };
1119
1120  const redirectToCheckout = (url) => {
1121    if (typeof window.__MVP_NAVIGATE__ === "function") {
1122      window.__MVP_NAVIGATE__(url);
1123      return;
1124    }
1125    window.location.assign(url);
1126  };
1127
1128  const completePendingCheckout = async () => {
1129    const sessionId = String(supabaseState.pendingCheckoutSessionId || "").trim();
1130    if (!sessionId || supabaseState.checkoutCompleteStarted) return;
1131    if (!accessToken()) {
1132      setCheckoutMessage(CHECKOUT_SIGN_IN_MESSAGE, true);
1133      setAuthMessage(CHECKOUT_SIGN_IN_MESSAGE, true);
1134      openAccountPanel();
1135      return;
1136    }
1137    supabaseState.checkoutCompleteStarted = true;
1138    setCheckoutMessage("Finishing checkout...");
1139    try {
1140      const res = await authenticatedFetch(`/api/mvp/checkout/complete?session_id=${encodeURIComponent(sessionId)}`, {}, { endpointType: "checkout_complete" });
1141      const data = await parseResponsePayload(res);
1142      if (!res.ok || data?.ok === false) {
1143        throw new Error(data?.error || "Unable to finish checkout.");
1144      }
1145      const creditsPurchased = Math.max(0, Number(data?.creditsPurchased ?? data?.creditsGranted ?? 0));
1146      supabaseState.pendingCheckoutSessionId = "";
1147      clearCheckoutParams();
1148      const refreshedUsage = await loadUsage();
1149      const refreshedBalance = Math.max(0, Number(refreshedUsage?.paidCreditsRemaining ?? data?.paidCreditsRemaining ?? 0));
1150      setAddRunsExpanded(false, { remember: true });
1151      setCheckoutMessage("");
1152      if (paymentSuccess) {
1153        paymentSuccess.textContent = `${creditsPurchased} credits added. New balance: ${refreshedBalance}.`;
1154        paymentSuccess.hidden = false;
1155        if (paymentSuccessTimer) window.clearTimeout(paymentSuccessTimer);
1156        paymentSuccessTimer = window.setTimeout(() => {
1157          paymentSuccess.hidden = true;
1158          paymentSuccess.textContent = "";
1159        }, 6000);
1160      }
1161    } catch (err) {
1162      setCheckoutMessage(err?.message || "Unable to finish checkout.", true);
1163    } finally {
1164      supabaseState.checkoutCompleteStarted = false;
1165    }
1166  };
1167
1168  const handleCheckoutReturn = async () => {
1169    let params;
1170    try {
1171      params = new URL(window.location.href).searchParams;
1172    } catch {
1173      return;
1174    }
1175    const checkout = params.get("checkout");
1176    if (checkout === "cancelled") {
1177      setCheckoutMessage(CHECKOUT_CANCELLED_MESSAGE, true);
1178      clearCheckoutParams();
1179      return;
1180    }
1181    if (checkout !== "success") return;
1182    const sessionId = String(params.get("session_id") || "").trim();
1183    if (!sessionId) {
1184      setCheckoutMessage("Missing checkout session. No credits were added.", true);
1185      clearCheckoutParams();
1186      return;
1187    }
1188    supabaseState.pendingCheckoutSessionId = sessionId;
1189    if (!isSignedIn()) {
1190      setCheckoutMessage(CHECKOUT_SIGN_IN_MESSAGE, true);
1191      setAuthMessage(CHECKOUT_SIGN_IN_MESSAGE, true);
1192      openAccountPanel();
1193      return;
1194    }
1195    await completePendingCheckout();
1196  };
1197
1198  const preserveCurrentScanForCheckout = () => {
1199    try {
1200      const completedData = lastCompletedPackage?.data;
1201      const customerSummary = completedData?.customerSummary;
1202      const compactCompletedPackage = completedData ? {
1203        data: {
1204          requestId: completedData.requestId,
1205          runId: completedData.runId,
1206          status: completedData.status,
1207          summary: completedData.summary,
1208          customerSummary: customerSummary && typeof customerSummary === "object"
1209            ? { ...customerSummary, discovery_coverage: undefined }
1210            : customerSummary,
1211          preview: Array.isArray(completedData.preview) ? completedData.preview.slice(0, 25) : [],
1212          artifacts: completedData.artifacts,
1213          report: completedData.report,
1214          message: completedData.message,
1215          catalogPotential: completedData.catalogPotential,
1216        },
1217        requestMeta: lastCompletedPackage.requestMeta,
1218      } : null;
1219      window.sessionStorage?.setItem("fsMvpCheckoutScan", JSON.stringify({
1220        url: String(urlInput?.value || ""),
1221        inputMode,
1222        intent: selectedMvpIntent,
1223        completedPackage: compactCompletedPackage,
1224      }));
1225    } catch {}
1226  };
1227
1228  const startCheckout = async (packageId) => {
1229    if (supabaseState.checkoutBusy) return;
1230    if (!supabaseState.client || !accessToken()) {
1231      setCheckoutMessage("Sign in to buy more credits.", true);
1232      setAuthMessage("Sign in to buy more credits.", true);
1233      return;
1234    }
1235    supabaseState.checkoutBusy = true;
1236    supabaseState.checkoutPackageId = packageId;
1237    setAuthBusy(supabaseState.authBusy);
1238    setCheckoutMessage("Starting checkout...");
1239    try {
1240      const res = await authenticatedFetch("/api/mvp/checkout", {
1241        method: "POST",
1242        headers: { "Content-Type": "application/json" },
1243        body: JSON.stringify({ packageId }),
1244      }, { endpointType: "checkout" });
1245      const data = await parseResponsePayload(res);
1246      if (!res.ok || data?.ok === false) {
1247        const message = String(data?.error || "Unable to start checkout.");
1248        const clearMessage = message.toLowerCase().includes("stripe")
1249          ? "Checkout is not configured yet. Please contact support."
1250          : message;
1251        throw new Error(clearMessage);
1252      }
1253      if (!data?.url) throw new Error("Checkout did not return a payment link.");
1254      preserveCurrentScanForCheckout();
1255      redirectToCheckout(data.url);
1256    } catch (err) {
1257      setCheckoutMessage(err?.message || "Unable to start checkout.", true);
1258    } finally {
1259      supabaseState.checkoutBusy = false;
1260      supabaseState.checkoutPackageId = "";
1261      setAuthBusy(supabaseState.authBusy);
1262    }
1263  };
1264
1265  for (const button of checkoutButtons) {
1266    button.addEventListener("click", () => {
1267      void startCheckout(button.dataset.mvpCheckoutPackage || "");
1268    });
1269  }
1270
1271  const authCredentials = () => ({
1272    email: String(authEmail?.value || "").trim(),
1273    password: String(authPassword?.value || ""),
1274  });
1275
1276  const runAuthAction = async (mode) => {
1277    if (supabaseState.authBusy || (scanActive && !isSignedIn())) return;
1278    if (!supabaseState.client) {
1279      setAuthMessage("Sign-in is not configured.", true);
1280      return;
1281    }
1282    const credentials = authCredentials();
1283    let focusPasswordAfterSignup = false;
1284    if (mode === "reset") {
1285      if (!credentials.email) {
1286        setAuthMessage("Enter your email address.", true);
1287        return;
1288      }
1289    } else if (mode === "recovery") {
1290      const confirmPassword = String(authConfirmPassword?.value || "");
1291      if (credentials.password.length < MIN_PASSWORD_LENGTH) {
1292        setAuthMessage(`Use at least ${MIN_PASSWORD_LENGTH} characters.`, true);
1293        return;
1294      }
1295      if (credentials.password !== confirmPassword) {
1296        setAuthMessage("Passwords do not match.", true);
1297        return;
1298      }
1299    } else if (!credentials.email || !credentials.password) {
1300      setAuthMessage("Enter email and password.", true);
1301      return;
1302    } else if (mode === "signup" && credentials.password.length < MIN_PASSWORD_LENGTH) {
1303      setAuthMessage(`Use at least ${MIN_PASSWORD_LENGTH} characters.`, true);
1304      return;
1305    } else if (mode === "signup" && credentials.password !== String(authConfirmPassword?.value || "")) {
1306      setAuthMessage("Passwords do not match.", true);
1307      return;
1308    }
1309    setAuthBusy(mode);
1310    setAuthMessage(
1311      mode === "signup"
1312        ? "Creating account..."
1313        : mode === "reset"
1314          ? "Sending reset link..."
1315          : mode === "recovery"
1316            ? "Updating password..."
1317            : "Signing in..."
1318    );
1319    try {
1320      if (mode === "reset") {
1321        const result = await supabaseState.client.auth.resetPasswordForEmail(credentials.email, {
1322          redirectTo: `${window.location.origin}/auth/accept?mode=recovery`,
1323        });
1324        if (result.error) {
1325          setAuthMessage(authErrorMessageFor(mode, result.error), true);
1326          return;
1327        }
1328        setAuthMessage(RESET_REQUESTED_MESSAGE);
1329        return;
1330      }
1331      if (mode === "recovery") {
1332        const result = await supabaseState.client.auth.updateUser({ password: credentials.password });
1333        if (result.error) {
1334          setAuthMessage(authErrorMessageFor(mode, result.error), true);
1335          return;
1336        }
1337        authMode = "signin";
1338        try {
1339          const url = new URL(window.location.href);
1340          url.searchParams.delete("auth");
1341          window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`);
1342        } catch {}
1343        renderAuthState();
1344        setAuthMessage("Password updated successfully.");
1345        await loadUsage();
1346        return;
1347      }
1348      const result = mode === "signup"
1349        ? await supabaseState.client.auth.signUp({
1350          email: credentials.email,
1351          password: credentials.password,
1352          options: {
1353            emailRedirectTo: (() => {
1354              const redirect = new URL("/app-mvp", window.location.origin);
1355              const claim = claimTokenFromLocation();
1356              if (claim) redirect.searchParams.set("claim", claim);
1357              return redirect.toString();
1358            })(),
1359          },
1360        })
1361        : await supabaseState.client.auth.signInWithPassword(credentials);
1362      if (result.error) {
1363        setAuthMessage(authErrorMessageFor(mode, result.error), true);
1364        return;
1365      }
1366      const session = result.data?.session || null;
1367      if (session) {
1368        supabaseState.session = session;
1369        renderAuthState();
1370        setAuthMessage("");
1371        await handleAuthenticatedTransition();
1372        return;
1373      }
1374      supabaseState.session = null;
1375      if (mode === "signup") {
1376        if (authPassword) authPassword.value = "";
1377        if (authConfirmPassword) authConfirmPassword.value = "";
1378        authMode = "signin";
1379      }
1380      renderAuthState();
1381      setAuthMessage(
1382        mode === "signup"
1383          ? "Account created. Confirm your email, then return here and sign in to buy credits."
1384          : "Sign in did not return a session. Check your email confirmation status and try again.",
1385        mode !== "signup"
1386      );
1387      if (mode === "signup") focusPasswordAfterSignup = true;
1388      await loadUsage();
1389    } catch (err) {
1390      setAuthMessage(err?.message || "Sign in failed.", true);
1391    } finally {
1392      setAuthBusy(null);
1393      if (focusPasswordAfterSignup) authPassword?.focus?.();
1394    }
1395  };
1396
1397  const setAuthMode = (mode) => {
1398    authMode = ["signup", "reset", "recovery"].includes(mode) ? mode : "signin";
1399    renderAuthState();
1400    setAuthMessage(
1401      authMode === "signup"
1402        ? "Create an account to purchase credits and save your scan history."
1403        : authMode === "reset"
1404          ? "Enter your email and we’ll send a password reset link."
1405          : authMode === "recovery"
1406            ? "Choose a new password for your account."
1407            : ""
1408    );
1409    (authMode === "recovery" ? authPassword : authEmail)?.focus?.();
1410  };
1411
1412  authForm?.addEventListener("submit", (event) => {
1413    event.preventDefault();
1414    if (typeof authForm.reportValidity === "function" && !authForm.reportValidity()) return;
1415    void runAuthAction(authMode);
1416  });
1417
1418  signUpButton?.addEventListener("click", () => {
1419    setAuthMode(authMode === "signin" ? "signup" : "signin");
1420  });
1421
1422  urlInput?.addEventListener("input", renderCheckoutRetentionMessage);
1423  csvFileInput?.addEventListener("change", renderCheckoutRetentionMessage);
1424
1425  forgotPasswordButton?.addEventListener("click", () => {
1426    setAuthMode("reset");
1427  });
1428
1429  for (const id of ["mvpNavSignIn", "mvpNavCreateAccount", "mvpPreviewCreateAccount", "mvpPreviewSignIn", "mvpCompletedSignupCreate", "mvpCompletedSignupSignIn", "mvpCreateAccountEnrich", "mvpSignInEnrich"]) {
1430    E(id)?.addEventListener("click", (event) => {
1431      if (!blockAuthDuringScan()) return;
1432      event.preventDefault();
1433      event.stopImmediatePropagation();
1434    }, true);
1435  }
1436
1437  navSignInButton?.addEventListener("click", () => {
1438    setAuthMode("signin");
1439    openAccountPanel(navSignInButton);
1440  });
1441
1442  navCreateAccountButton?.addEventListener("click", () => {
1443    setAuthMode("signup");
1444    openAccountPanel(navCreateAccountButton);
1445  });
1446
1447  navAccountButton?.addEventListener("click", () => {
1448    openAccountPanel(navAccountButton);
1449  });
1450
1451  navSignOutButton?.addEventListener("click", () => {
1452    signOutButton?.click();
1453  });
1454
1455  accountCloseButton?.addEventListener("click", () => {
1456    setAccountPanelOpen(false, { restoreFocus: true });
1457  });
1458
1459  accountPanel?.addEventListener("click", (event) => {
1460    if (event.target === accountPanel) setAccountPanelOpen(false, { restoreFocus: true });
1461  });
1462
1463  accountPanel?.addEventListener("keydown", (event) => {
1464    if (event.key !== "Escape" || (runHistory && !runHistory.hidden)) return;
1465    event.preventDefault();
1466    setAccountPanelOpen(false, { restoreFocus: true });
1467  });
1468
1469  signOutButton?.addEventListener("click", async () => {
1470    if (supabaseState.authBusy) return;
1471    if (!supabaseState.client) {
1472      setAuthMessage("Sign-in is not configured.", true);
1473      return;
1474    }
1475    setAuthBusy("signout");
1476    setAuthMessage("Signing out...");
1477    try {
1478      const result = await supabaseState.client.auth.signOut();
1479      if (result?.error) {
1480        setAuthMessage(result.error.message || "Sign out failed.", true);
1481        return;
1482      }
1483      supabaseState.session = null;
1484      renderAuthState();
1485      setAuthMessage("Signed out.");
1486      await loadUsage();
1487    } catch (err) {
1488      setAuthMessage(err?.message || "Sign out failed.", true);
1489    } finally {
1490      setAuthBusy(null);
1491    }
1492  });
1493
1494  const runStatusLabel = (run) => {
1495    const status = String(run?.status || "").toLowerCase();
1496    if (["queued", "running"].includes(status) && run?.jobType === "mvp-continue-scan") return "Discovering full catalog";
1497    if (status === "completed" && Number(run?.pagesRemaining || 0) > 0) return "Stopped at safety limit";
1498    if (status === "completed") return "Complete";
1499    if (status === "failed") return "Failed";
1500    if (status === "cancelled") return "Cancelled";
1501    if (status === "running") return "Running";
1502    return "Queued";
1503  };
1504
1505  const closeRunHistory = ({ restoreFocus = false } = {}) => {
1506    if (runHistory) runHistory.hidden = true;
1507    myRunsButton?.setAttribute("aria-expanded", "false");
1508    if (restoreFocus) myRunsButton?.focus?.();
1509  };
1510
1511  const openHistoryRun = async (run, button) => {
1512    const originalLabel = button.textContent;
1513    button.disabled = true;
1514    button.textContent = "Opening…";
1515    if (runHistoryStatus) {
1516      runHistoryStatus.hidden = false;
1517      runHistoryStatus.textContent = "Loading this import package…";
1518    }
1519    try {
1520      const res = await authenticatedFetch(`/api/mvp/runs/${encodeURIComponent(run.runId)}/result`, {
1521        credentials: "same-origin",
1522      }, { endpointType: "run_result", runId: run.runId });
1523      const data = await parseResponsePayload(res);
1524      if (!res.ok || data?.ok === false) throw responseError(res, data, "Unable to open this run.", { endpointType: "run_result", runId: run.runId });
1525      renderCompletedPackage(
1526        data,
1527        { sourceType: "url", url: run.sourceUrl, host: run.host },
1528        Date.now(),
1529        "mvp_history_run_opened"
1530      );
1531      closeRunHistory();
1532      successPanel?.scrollIntoView?.({ behavior: "smooth", block: "start" });
1533      return true;
1534    } catch (err) {
1535      if (runHistoryStatus) {
1536        runHistoryStatus.textContent = err?.message || "Unable to open this run.";
1537        runHistoryStatus.hidden = false;
1538      }
1539      return false;
1540    } finally {
1541      button.disabled = false;
1542      button.textContent = originalLabel;
1543    }
1544  };
1545
1546  const renderRunHistory = (runs, { showAll = false } = {}) => {
1547    if (!runHistoryList || !runHistoryStatus) return;
1548    runHistoryList.replaceChildren();
1549    if (!Array.isArray(runs) || !runs.length) {
1550      runHistoryStatus.textContent = "No authenticated URL runs yet.";
1551      runHistoryStatus.hidden = false;
1552      return;
1553    }
1554    runHistoryStatus.hidden = true;
1555    const visibleRuns = showAll ? runs : runs.slice(0, 5);
1556    for (const run of visibleRuns) {
1557      const item = document.createElement("article");
1558      item.className = "mvp-run-history-item";
1559      const body = document.createElement("div");
1560      const title = document.createElement("p");
1561      title.className = "mvp-run-history-item__title";
1562      title.textContent = run.host || "URL run";
1563      const meta = document.createElement("p");
1564      meta.className = "mvp-run-history-item__meta";
1565      const created = Number.isFinite(Date.parse(run.createdAt)) ? new Date(run.createdAt).toLocaleString() : "";
1566      meta.textContent = `${runStatusLabel(run)} · ${Number(run.productCount || 0)} products${created ? ` · ${created}` : ""}`;
1567      body.append(title, meta);
1568      if (run.failureMessage) {
1569        const failure = document.createElement("p");
1570        failure.className = "mvp-run-history-item__error";
1571        failure.textContent = run.failureMessage;
1572        body.appendChild(failure);
1573      }
1574      item.appendChild(body);
1575      if (run.canOpenResult === true) {
1576        const openButton = document.createElement("button");
1577        openButton.className = "button secondary";
1578        openButton.type = "button";
1579        openButton.textContent = "Open result";
1580        openButton.addEventListener("click", () => void openHistoryRun(run, openButton));
1581        item.appendChild(openButton);
1582      }
1583      runHistoryList.appendChild(item);
1584    }
1585    if (!showAll && runs.length > 5) {
1586      const viewAll = document.createElement("button");
1587      viewAll.id = "mvpRunHistoryViewAll";
1588      viewAll.className = "button secondary mvp-run-history__view-all";
1589      viewAll.type = "button";
1590      viewAll.textContent = `View all runs (${runs.length})`;
1591      viewAll.addEventListener("click", () => renderRunHistory(runs, { showAll: true }));
1592      runHistoryList.appendChild(viewAll);
1593    }
1594  };
1595
1596  const loadRunHistory = async () => {
1597    if (!runHistoryStatus) return;
1598    runHistoryStatus.textContent = "Loading your runs…";
1599    runHistoryStatus.hidden = false;
1600    try {
1601      const res = await authenticatedFetch("/api/mvp/runs?limit=10", { credentials: "same-origin" }, { endpointType: "run_history" });
1602      const data = await parseResponsePayload(res);
1603      if (!res.ok || data?.ok === false) throw responseError(res, data, "Unable to load your runs.", { endpointType: "run_history" });
1604      renderRunHistory(data?.runs);
1605      cachedRunHistory = Array.isArray(data?.runs) ? data.runs : [];
1606      runHistoryLoaded = true;
1607      return cachedRunHistory;
1608    } catch (err) {
1609      runHistoryStatus.textContent = err?.message || "Unable to load your runs.";
1610      return [];
1611    }
1612  };
1613
1614  const clearClaimParam = () => {
1615    try {
1616      const url = new URL(window.location.href);
1617      url.searchParams.delete("claim");
1618      url.searchParams.delete("preview");
1619      window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`);
1620    } catch {}
1621  };
1622
1623  const claimErrorMessage = (payload, status) => {
1624    const code = String(payload?.code || "");
1625    if (code === "claim_expired" || status === 410) return "This customer link has expired. Ask for a new link.";
1626    if (code === "claim_invalid" || code === "claim_unknown" || [400, 404].includes(status)) return "This customer link is invali
1626d.";
1627    if (code === "claim_unavailable" || code === "claim_source_owner_changed" || status === 409) return "This customer link has already been used or is unavailable.";
1628    return "The supplier dataset could not be opened yet. Please try again.";
1629  };
1630
1631  const claimSalesAgentRun = async () => {
1632    const token = claimTokenFromLocation();
1633    if (!token || claimInProgress || claimedToken === token || !isSignedIn()) return;
1634    if (!/^[A-Za-z0-9_-]{43}$/.test(token)) {
1635      setAuthMessage("This customer link is invalid.", true);
1636      return;
1637    }
1638    claimInProgress = true;
1639    setAuthMessage("Opening your supplier dataset…");
1640    try {
1641      const res = await authenticatedFetch(`/api/mvp/sales-agent-claims/${encodeURIComponent(token)}/claim`, {
1642        method: "POST",
1643        credentials: "same-origin",
1644      }, { endpointType: "sales_agent_claim" });
1645      const data = await parseResponsePayload(res);
1646      if (!res.ok || data?.ok === false || !data?.runId) throw new Error(claimErrorMessage(data, res.status));
1647      claimedToken = token;
1648      clearClaimParam();
1649      const runs = await loadRunHistory();
1650      const run = runs.find((candidate) => candidate?.runId === data.runId);
1651      if (!run) throw new Error("The dataset was claimed, but it is not visible in My runs yet. Refresh and open it from My runs.");
1652      const button = document.createElement("button");
1653      button.textContent = "Open result";
1654      const opened = await openHistoryRun(run, button);
1655      if (!opened) throw new Error("The dataset is in your account, but its result could not be opened yet. Try again from My runs.");
1656      setAuthMessage(data.alreadyClaimed ? "This supplier dataset is already in your account." : "Supplier dataset added to your account.");
1657    } catch (err) {
1658      setAuthMessage(err?.message || "The supplier dataset could not be opened yet. Please try again.", true);
1659    } finally {
1660      claimInProgress = false;
1661    }
1662  };
1663  const perfPhase = (phase, durationMs, payload = {}) => log("mvp_perf_phase", {
1664    phase,
1665    durationMs: Math.max(0, Math.round(Number(durationMs) || 0)),
1666    ...payload,
1667  });
1668  window.addEventListener("load", () => setTimeout(() => {
1669    const navigation = performance.getEntriesByType?.("navigation")?.[0];
1670    const resources = performance.getEntriesByType?.("resource") || [];
1671    const resource = (fragment) => resources.find((entry) => String(entry.name || "").includes(fragment));
1672    const appScript = resource("/app/mvpApp.js");
1673    const supabaseScript = resource("@supabase/supabase-js");
1674    const css = resources.filter((entry) => String(entry.name || "").endsWith(".css"));
1675    log("mvp_page_load_perf", {
1676      domContentLoadedMs: navigation ? Math.round(navigation.domContentLoadedEventEnd) : null,
1677      windowLoadMs: navigation ? Math.round(navigation.loadEventEnd) : null,
1678      jsInitializationMs: appInitCompletedMs,
1679      mvpAppDownloadMs: appScript ? Math.round(appScript.duration) : null,
1680      mvpAppTransferBytes: appScript?.transferSize || null,
1681      cssRequestCount: css.length,
1682      cssDurationMs: Math.round(css.reduce((total, entry) => total + entry.duration, 0)),
1683      cssTransferBytes: css.reduce((total, entry) => total + (entry.transferSize || 0), 0),
1684      supabaseCdnMs: supabaseScript ? Math.round(supabaseScript.duration) : null,
1685      supabaseTransferBytes: supabaseScript?.transferSize || null,
1686      totalRequestCount: resources.length + 1,
1687    });
1688  }, 0), { once: true });
1689
1690  const previewSalesAgentRun = async () => {
1691    const token = claimTokenFromLocation();
1692    if (!token || previewedToken === token || !/^[A-Za-z0-9_-]{43}$/.test(token)) return;
1693    try {
1694      const res = await fetch(`/api/mvp/sales-agent-claims/${encodeURIComponent(token)}/preview`, { credentials: "same-origin" });
1695      const data = await parseResponsePayload(res);
1696      if (!res.ok || data?.ok === false) throw new Error(claimErrorMessage(data, res.status));
1697      previewedToken = token;
1698      renderCompletedPackage(data, { sourceType: "url", url: data.sourceUrl, host: data.host, salesAgentClaimPreview: true }, Date.now(), "sales_agent_claim_previewed");
1699      if (!isSignedIn()) setAuthMessage("Previewing a prepared supplier dataset. Sign in to add it to your account and unlock paid continuation or enrichment.");
1700      successPanel?.scrollIntoView?.({ behavior: "smooth", block: "start" });
1701    } catch (err) {
1702      setAuthMessage(err?.message || "The supplier dataset preview could not be opened yet.", true);
1703    }
1704  };
1705
1706  myRunsButton?.addEventListener("click", () => {
1707    if (!runHistory) return;
1708    const opening = runHistory.hidden;
1709    runHistory.hidden = !opening;
1710    myRunsButton.setAttribute("aria-expanded", opening ? "true" : "false");
1711    if (opening) {
1712      void loadRunHistory();
1713      runHistory.focus?.();
1714    }
1715  });
1716
1717  runHistoryClose?.addEventListener("click", () => {
1718    closeRunHistory({ restoreFocus: true });
1719  });
1720
1721  runHistory?.addEventListener("keydown", (event) => {
1722    if (event.key !== "Escape") return;
1723    event.preventDefault();
1724    closeRunHistory({ restoreFocus: true });
1725  });
1726
1727  const setStateCopy = (state) => {
1728    const copy = STATE_COPY[state] || STATE_COPY.success;
1729    if (stateTitle) stateTitle.textContent = copy.title;
1730    if (resultBanner) {
1731      resultBanner.textContent = copy.banner;
1732      resultBanner.hidden = !["failure", "capture"].includes(state);
1733    }
1734    if (stateMessage) {
1735      stateMessage.textContent = copy.message;
1736      stateMessage.hidden = !copy.message;
1737    }
1738    if (stateHint) {
1739      stateHint.textContent = copy.hint;
1740      stateHint.hidden = !copy.hint;
1741    }
1742  };
1743
1744  const URL_WARNING_ISSUES = new Set(["missing_sku", "missing_price", "missing_image", "local_image_path", "weak_description"]);
1745  const UPLOAD_WARNING_ISSUES = new Set(["missing_image", "missing_description", "missing_category", "missing_tags"]);
1746
1747  const plural = (count, singular, pluralText = `${singular}s`) => `${count} ${count === 1 ? singular : pluralText}`;
1748
1749  const rowsNeedingReviewFromProducts = (products, totalProducts, sourceType) => {
1750    if (!Array.isArray(products) || !products.length) return null;
1751    const total = Number(totalProducts || 0);
1752    if (total > products.length) return null;
1753    const warningIssues = sourceType === "upload" ? UPLOAD_WARNING_ISSUES : 
1753URL_WARNING_ISSUES;
1754    return products.filter((product) =>
1755      Array.isArray(product?.issues) && product.issues.some((issue) => warningIssues.has(String(issue || "")))
1756    ).length;
1757  };
1758
1759  const cappedCount = (value, max) => {
1760    const count = Number(value);
1761    if (!Number.isFinite(count)) return null;
1762    return Math.max(0, Math.min(max, count));
1763  };
1764
1765  const rowsNeedingReviewCount = ({ productsFound, summary, customerSummary, preview, sourceType }) => {
1766    const products = Math.max(0, Number(productsFound || 0));
1767    if (!products) return 0;
1768    const summaryReviewRows = cappedCount(summary?.reviewRows, products);
1769    if (summaryReviewRows !== null) return summaryReviewRows;
1770    if (Object.prototype.hasOwnProperty.call(customerSummary || {}, "review_rows")) {
1771      const customerReviewRows = cappedCount(customerSummary?.review_rows, products);
1772      if (customerReviewRows !== null) return customerReviewRows;
1773    }
1774    const fromProducts = rowsNeedingReviewFromProducts(preview, products, sourceType);
1775    if (Number.isFinite(fromProducts)) return Math.max(0, Math.min(products, fromProducts));
1776    return 0;
1777  };
1778
1779  const importabilityText = (importableRows, totalRows) => {
1780    const total = Number(totalRows || 0);
1781    const importable = Number(importableRows || 0);
1782    if (!Number.isFinite(total) || total <= 0) return "0%";
1783    return `${Math.round((Math.max(0, importable) / total) * 100)}%`;
1784  };
1785
1786  const dataQualityStatus = ({ warningCount, blockingIssueCount, blockedProducts }) => {
1787    const warnings = Number(warningCount || 0);
1788    const blockers = Number(blockingIssueCount || 0);
1789    const blocked = Number(blockedProducts || 0);
1790    if (blockers > 0 || blocked > 0) return "Blocked issues found";
1791    if (warnings > 0) return "Manual review recommended";
1792    return "Good";
1793  };
1794
1795  const setImportQualityMetrics = ({ productsFound, importReadyProducts, warningCount, blockingIssueCount, blockedProducts }) => {
1796    const importability = importabilityText(importReadyProducts, productsFound);
1797    const dataQuality = dataQualityStatus({ warningCount, blockingIssueCount, blockedProducts });
1798    const metricImportability = E("mvpMetricImportability");
1799    const summaryMetricImportability = E("mvpSummaryMetricImportability");
1800    const metricDataQuality = E("mvpMetricDataQuality");
1801    const summaryMetricDataQuality = E("mvpSummaryMetricDataQuality");
1802    if (metricImportability) metricImportability.textContent = importability;
1803    if (summaryMetricImportability) summaryMetricImportability.textContent = importability;
1804    if (metricDataQuality) metricDataQuality.textContent = dataQuality;
1805    if (summaryMetricDataQuality) summaryMetricDataQuality.textContent = dataQuality;
1806  };
1807
1808  const hideDownloadLinks = () => {
1809    DOWNLOAD_LINKS.forEach(([id, _key, itemId]) => {
1810      const el = E(id);
1811      const item = E(itemId);
1812      if (!el) return;
1813      if (item) item.style.display = "none";
1814      el.href = "#";
1815    });
1816    if (freeFilesPanel) freeFilesPanel.hidden = true;
1817  };
1818
1819  const setNoReportMessageVisible = (visible) => {
1820    if (noReportMessage) noReportMessage.hidden = !visible;
1821  };
1822
1823  const mainIssuePriority = (issue) => {
1824    const text = String(issue || "").toLowerCase();
1825    return /filter|navigation|product title|product name|product url|add a sku|add a price|numeric price|duplicate sku|duplicate product title|duplicate title|duplicate handle/.test(text)
1826      ? 0
1827      : 1;
1828  };
1829
1830  const mainIssueTextFor = (mainIssues, fallback = "") => {
1831    const issues = Array.isArray(mainIssues)
1832      ? mainIssues.map((issue) => String(issue || "").trim()).filter(Boolean)
1833      : [];
1834    const uniqueIssues = [];
1835    const prioritizedIssues = issues
1836      .map((issue, index) => ({ issue, index, priority: mainIssuePriority(issue) }))
1837      .sort((a, b) => a.priority - b.priority || a.index - b.index)
1838      .map((item) => item.issue);
1839    for (const issue of prioritizedIssues) {
1840      if (!uniqueIssues.includes(issue)) uniqueIssues.push(issue);
1841      if (uniqueIssues.length >= 5) break;
1842    }
1843    const displayIssues = uniqueIssues.map((issue) => issue
1844      .replace(/^\d+\s+row\(s\):\s*/i, "")
1845      .replace(/\s+such as 19\.99\.?$/i, "")
1846      .replace(/\.+$/g, "")
1847      .trim()
1848    ).filter(Boolean);
1849    if (displayIssues.length > 1) return `Main issues: ${displayIssues.join("; ")}.`;
1850    if (displayIssues.length === 1) return `Main issue: ${displayIssues[0]}.`;
1851    return String(fallback || "").trim();
1852  };
1853
1854  const hasCleanupArtifacts = (result = lastMvpResult) => {
1855    const artifacts = result?.artifacts;
1856    return Boolean(!result?.readOnly && artifacts && typeof artifacts === "object" && artifacts.llmReadyJson);
1857  };
1858
1859  const resetCleanupAction = (message = "Generate a report first to unlock this review action.") => {
1860    if (cleanupTitle) cleanupTitle.textContent = "Prepare Shopify import draft";
1861    if (cleanupDescription) cleanupDescription.hidden = false;
1862    if (cleanupRequestLink) cleanupRequestLink.disabled = true;
1863    if (cleanupRequestLink) cleanupRequestLink.hidden = false;
1864    if (cleanupStatus) {
1865      cleanupStatus.textContent = message;
1866      cleanupStatus.hidden = false;
1867    }
1868    if (cleanupDownload) {
1869      cleanupDownload.hidden = true;
1870      cleanupDownload.removeAttribute("href");
1871    }
1872    if (cleanupPreview) cleanupPreview.hidden = true;
1873  };
1874
1875  const storeLastMvpResult = (data, requestMeta = {}) => {
1876    lastMvpResult = {
1877      requestId: data?.requestId || null,
1878      runId: data?.runId || null,
1879      readOnly: data?.readOnly === true,
1880      sourceType: requestMeta?.sourceType || "",
1881      url: requestMeta?.url || "",
1882      host: requestMeta?.host || data?.customerSummary?.supplier_context?.host || "",
1883      fileName: requestMeta?.fileName || "",
1884      summary: data?.summary || {},
1885      customerSummary: data?.customerSummary || {},
1886      preview: Array.isArray(data?.preview) ? data.preview : [],
1887      artifacts: data?.artifacts || {},
1888    };
1889    if (cleanupRequestLink) cleanupRequestLink.disabled = !hasCleanupArtifacts(lastMvpResult);
1890    if (cleanupRequestLink) cleanupRequestLink.hidden = false;
1891    if (cleanupTitle) cleanupTitle.textContent = "Prepare Shopify import draft";
1892    if (cleanupDescription) cleanupDescription.hidden = false;
1893    if (cleanupStatus) {
1894      const readyForCleanup = hasCleanupArtifacts(lastMvpResult);
1895      cleanupStatus.textContent = readyForCleanup
1896        ? ""
1897        : "Cleaned CSV generation needs the enrichment-ready JSON artifact from a completed package.";
1898      cleanupStatus.hidden = readyForCleanup;
1899    }
1900    if (cleanupDownload) {
1901      cleanupDownload.hidden = true;
1902      cleanupDownload.removeAttribute("href");
1903    }
1904    if (cleanupPreview) cleanupPreview.hidden = true;
1905  };
1906
1907  resetCleanupAction();
1908
1909  const authenticatedActionAllowed = () => isSignedIn();
1910  function renderActionAccessGates() {
1911    const allowed = authenticatedActionAllowed();
1912    const message = isSignedIn()
1913      ? t("mvp.paid.add", null, "Add credits to use this action")
1914      : t("mvp.paid.signIn", null, "Sign in to use this action");
1915    const balance = isSignedIn() ? Math.max(0, Number(lastUsage?.paidCreditsRemaining || 0)) : 0;
1916    if (!allowed && marketingTemplatePreview) {
1917      marketingTemplatePreview.textContent = "";
1918      marketingTemplatePreview.hidden = true;
1919    }
1920    const ids = [...DOWNLOAD_LINKS.map(([id]) => id), "mvpMainIssuesDownload", "mvpExportCurrentProducts",
1921      "mvpEnrichProductsBtn", "mvpOpenMarketingContent", "mvpPrepareMarketingRequest", "mvpMarketingDownload", "mvpCleanupRequestLink", "mvpCleanupDownload"];
1922    ids.forEach((id) => {
1923      const el = E(id);
1924      if (!el) return;
1925      const insufficientEnrichmentCredits = id === "mvpEnrichProductsBtn" && balance < Number(el.dataset.creditsRequired || 0);
1926      if (id === "mvpEnrichProductsBtn") el.dataset.requiresRuns = String(isSignedIn() && insufficientEnrichmentCredits);
1927      const unavailable = id === "mvpEnrichProductsBtn" ? enrichmentInProgress || insufficientEnrichmentCredits
1928        : el === cleanupRequestLink ? !hasCleanupArtifacts(lastMvpResult)
1929        : el === marketingDownload ? !el.getAttribute("href") : false;
1930      el.dataset.mvpPaidAction = "true";
1931      el.disabled = !allowed || unavailable;
1932      el.setAttribute("aria-disabled", String(el.disabled));
1933      if (el.tagName === "A") el.tabIndex = el.disabled ? -1 : 0;
1934      if (!allowed && el === marketingDownload) el.removeAttribute("href");
1935      let support = E(`${id}PaidGate`);
1936      if (!support) {
1937        support = document.createElement("div");
1938        support.id = `${id}PaidGate`;
1939        support.className = "status mvp-paid-gate";
1940        el.after(support);
1941      }
1942      support.hidden = el.hidden || (allowed && !insufficientEnrichmentCredits);
1943      support.textContent = "";
1944      const locked = !allowed || insufficientEnrichmentCredits;
1945      const cta = document.createElement("button");
1946      cta.type = "button";
1947      cta.className = "button secondary";
1948      cta.textContent = message;
1949      // Reserve the CTA space so purchasing credits does not move the actions.
1950      cta.style.visibility = locked ? "visible" : "hidden";
1951      cta.disabled = !locked;
1952      cta.setAttribute("aria-hidden", String(!locked));
1953      cta.addEventListener("click", () => focusAccountFromGate("signin", "download"));
1954      support.appendChild(cta);
1955    });
1956  }
1957  document.addEventListener("click", (event) => {
1958    const action = event.target instanceof Element ? event.target.closest('[data-mvp-paid-action][aria-disabled="true"]') : null;
1959    if (action) { event.preventDefault(); event.stopImmediatePropagation(); }
1960  }, true);
1961
1962  const renderDownloadLinks = (artifacts, authenticated = isSignedIn()) => {
1963    DOWNLOAD_LINKS.forEach(([id, key, itemId]) => {
1964      const el = E(id);
1965      const item = E(itemId);
1966      if (!el) return;
1967      const artifactPath = String(artifacts?.[key] || "");
1968      if (!el.dataset.mvpDownloadLabel) el.dataset.mvpDownloadLabel = el.textContent || "Download file";
1969      if (item) item.style.display = artifactPath ? "grid" : "none";
1970      el.textContent = el.dataset.mvpDownloadLabel;
1971      el.dataset.mvpDownloadPath = artifactPath;
1972      el.removeAttribute("href");
1973      let fileLabel = item?.querySelector("[data-mvp-file-label]");
1974      if (item && artifactPath && !fileLabel) {
1975        fileLabel = document.createElement("p");
1976        fileLabel.className = "status";
1977        fileLabel.dataset.mvpFileLabel = "true";
1978        item.appendChild(fileLabel);
1979      }
1980      if (fileLabel) fileLabel.textContent = `${el.dataset.mvpDownloadLabel} · ${artifactPath.split(/[\\/]/).pop() || artifactPath}`;
1981    });
1982    if (freeFilesPanel) {
1983      const visiblePrimaryKeys = ["exportCsv", "shopifyCsv", "bigcommerceCsv", "summaryJson", "llmReadyJson", "issuesCsv", "catalogMapCsv", "importReadinessReport", "importReadinessReportPdf"];
1984      freeFilesPanel.hidden = !visiblePrimaryKeys
1985        .some((key) => String(artifacts?.[key] || ""));
1986    }
1987    const mainIssuesDownload = E("mvpMainIssuesDownload");
1988    if (mainIssuesDownload) {
1989      const issuesPath = String(artifacts?.issuesCsv || "");
1990      if (!mainIssuesDownload.dataset.mvpDownloadLabel) mainIssuesDownload.dataset.mvpDownloadLabel = mainIssuesDownload.textContent || "Review issues";
1991      mainIssuesDownload.textContent = mainIssuesDownload.dataset.mvpDownloadLabel;
1992      mainIssuesDownload.dataset.mvpDownloadPath = issuesPath;
1993      mainIssuesDownload.removeAttribute("href");
1994      const item = mainIssuesDownload.closest(".mvp-download-item");
1995      if (item) item.style.display = issuesPath ? "grid" : "none";
1996      let fileLabel = item?.querySelector("[data-mvp-file-label]");
1997      if (item && issuesPath && !fileLabel) {
1998        fileLabel = document.createElement("p");
1999        fileLabel.className = "status";
2000        fileLabel.dataset.mvpFileLabel = "true";
2001        item.appendChild(fileLabel);
2002      }
2003      if (fileLabel) fileLabel.textContent = `${mainIssuesDownload.dataset.mvpDownloadLabel} · ${issuesPath.split(/[\\/]/).pop() || issuesPath}`;
2004    }
2005  };
2006
2007  const currentAnonymousCompletedRun = () => {
2008    const displayed = lastCompletedPackage?.data;
2009    if (anonymousCompletedRunId && displayed?.runId === anonymousCompletedRunId) {
2010      return { runId: anonymousCompletedRunId, sourceUrl: lastCompletedPackage?.requestMeta?.url, host: lastCompletedPackage?.requestMeta?.host };
2011    }
2012    try {
2013      const completed = JSON.parse(window.localStorage?.getItem(ANONYMOUS_COMPLETED_RUN_STORAGE_KEY) || "null");
2014      if (completed?.runId) return completed;
2015      const persisted = JSON.parse(window.localStorage?.getItem(ACTIVE_DISCOVERY_STORAGE_KEY) || "null");
2016      if (persisted?.runId && String(persisted.status || "").toLowerCase() === "completed") return persisted;
2017    } catch {}
2018    return null;
2019  };
2020
2021  const claimCurrentAnonymousRun = async () => {
2022    const run = currentAnonymousCompletedRun();
2023    if (!isSignedIn() || !run?.runId) return false;
2024    if (anonymousClaimPromise) return anonymousClaimPromise;
2025    anonymousClaimPromise = (async () => {
2026      const res = await authenticatedFetch(`/api/mvp/runs/${encodeURIComponent(run.runId)}/claim-anonymous`, {
2027        method: "POST",
2028        credentials: "same-origin",
2029      }, { endpointType: "anonymous_run_claim", runId: run.runId });
2030      const claimed = await parseResponsePayload(res);
2031      if (!res.ok || claimed?.ok === false) throw responseError(res, claimed, "Unable to add this run to your account.", { endpointType: "anonymous_run_claim", runId: run.runId });
2032      anonymousCompletedRunId = "";
2033      try { window.localStorage?.removeItem(ANONYMOUS_COMPLETED_RUN_STORAGE_KEY); } catch {}
2034      const resultRes = await authenticatedFetch(`/api/mvp/runs/${encodeURIComponent(run.runId)}/result`, {
2035        credentials: "same-origin",
2036      }, { endpointType: "run_result", runId: run.runId });
2037      const data = await parseResponsePayload(resultRes);
2038      if (!resultRes.ok || data?.ok === false) throw responseError(resultRes, data, "Unable to refresh this run.", { endpointType: "run_result", runId: run.runId });
2039      renderCompletedPackage(data, { sourceType: "url", url: run.sourceUrl || data.sourceUrl, host: run.host || data.host }, Date.now(), "mvp_anonymous_run_claimed");
2040      await loadRunHistory();
2041      return true;
2042    })().finally(() => { anonymousClaimPromise = null; });
2043    return anonymousClaimPromise;
2044  };
2045
2046  const handleAuthenticatedTransition = async () => {
2047    if (isSignedIn()) {
2048      if (!claimPreviewOnlyFromLocation()) {
2049        try { await claimCurrentAnonymousRun(); } catch (err) { setAuthMessage(err?.message || "Unable to add this run to your account.", true); }
2050      }
2051      if (supabaseState.pendingCheckoutSessionId) return completePendingCheckout();
2052      if (claimTokenFromLocation()) return claimSalesAgentRun();
2053    }
2054    return loadUsage();
2055  };
2056
2057  const downloadArtifact = async (artifactPath) => {
2058    if (!authenticatedActionAllowed()) return;
2059    const response = await authenticatedFetch(`/api/mvp/download?path=${encodeURIComponent(artifactPath)}`, {}, { endpointType: "download" });
2060    if (!response.ok) {
2061      const payload = await parseResponsePayload(response);
2062      throw responseError(response, payload, "Download failed.", { endpointType: "download", authenticatedRequest: true });
2063    }
2064    const blob = await response.blob();
2065    const disposition = String(response.headers.get("content-disposition") || "");
2066    const fileName = disposition.match(/filename="?([^";]+)"?/i)?.[1] || artifactPath.split(/[\\/]/).pop() || "download";
2067    const objectUrl = URL.createObjectURL(blob);
2068    const link = document.createElement("a");
2069    link.href = objectUrl;
2070    link.download = fileName;
2071    document.body.appendChild(link);
2072    link.click();
2073    link.remove();
2074    window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
2075  };
2076
2077  document.addEventListener("click", (event) => {
2078    const target = event.target instanceof Element ? event.target.closest("[data-mvp-download-path]") : null;
2079    if (!target) return;
2080    event.preventDefault();
2081    if (!isSignedIn()) {
2082      focusAccountFromGate("signin", "download");
2083      return;
2084    }
2085    const artifactPath = String(target.dataset.mvpDownloadPath || "");
2086    if (!artifactPath) {
2087      if (isSignedIn()) focusAccountFromGate("signin");
2088      else focusAccountFromGate("signin", "catalog");
2089      return;
2090    }
2091    downloadArtifact(artifactPath).catch((error) => setCheckoutMessage(error?.message || "Download failed.", true));
2092  });
2093
2094  const setText = (id, value) => {
2095    const el = E(id);
2096    if (el) el.textContent = String(value ?? "");
2097  };
2098
2099  if (explorerMode) {
2100    setText("mvpTitle", "Explore a product website");
2101    setText("mvpHeroDescription", "Discover products, categories, and product pages from a public website.");
2102    if (runButton) runButton.textContent = "Discover products";
2103    if (noReportMessage) noReportMessage.textContent = "No catalog yet. Enter a public website URL to discover products.";
2104    explorerTabs?.querySelectorAll("[data-mvp-explorer-tab]").forEach((button) => button.addEventListener("click", () => setExplorerTab(button.dataset.mvpExplorerTab)));
2105  }
2106
2107  const numberText = (value) => {
2108    const number = Number(value);
2109    return Number.isFinite(number) ? String(number) : "0";
2110  };
2111
2112  const textOrDash = (value) => {
2113    const text = String(value ?? "").trim();
2114    return text || "—";
2115  };
2116
2117  const finiteCount = (value, fallback = 0) => {
2118    const count = Number(value);
2119    return Number.isFinite(count) ? Math.max(0, count) : fallback;
2120  };
2121
2122  const firstFiniteCount = (...values) => {
2123    const value = values.find((candidate) => candidate !== null && candidate !== undefined && candidate !== "" && Number.isFinite(Number(candidate)));
2124    return value === undefined ? null : finiteCount(value);
2125  };
2126
2127  const discoveryLabel = (value) => {
2128    const raw = String(value ?? "").trim().toLowerCase();
2129    if (raw === "not_catalog_candidate") return "Not a product or collection page";
2130    if (raw === "queued") return "Waiting to scan";
2131    const text = raw.replace(/_/g, " ");
2132    return text ? text.charAt(0).toUpperCase() + text.slice(1) : "—";
2133  };
2134
2135  const discoveryStoppedReasonLabel = (value) => {
2136    const labels = {
2137      max_extract_pages: "Current scan batch completed",
2138      no_more_candidates: "All available pages checked",
2139      fallback_single_page: "Only the submitted page could be checked",
2140      error: "The catalog scan encountered an error",
2141    };
2142    const reason = String(value ?? "").trim().toLowerCase();
2143    return labels[reason] || discoveryLabel(reason);
2144  };
2145
2146  const safeDiscoveryPageUrl = (value) => {
2147    try {
2148      const parsed = new URL(String(value || ""));
2149      return /^https?:$/.test(parsed.protocol) ? parsed.toString() : "";
2150    } catch {
2151      return "";
2152    }
2153  };
2154
2155  const renderDiscoveryPageTable = (pages) => {
2156    const table = document.createElement("table");
2157    table.className = "mvp-discovery-map-table";
2158    const head = document.createElement("thead");
2159    const headRow = document.createElement("tr");
2160    ["Page", "URL", "Page type", "Status", t("mvp.details.candidates", null, "Product candidates detected"), "Reason"].forEach((label) => {
2161      const cell = document.createElement("th");
2162      cell.scope = "col";
2163      cell.textContent = label;
2164      headRow.append(cell);
2165    });
2166    head.append(headRow);
2167    table.append(head);
2168    const body = document.createElement("tbody");
2169    pages.forEach((page) => {
2170      const row = document.createElement("tr");
2171      const values = [
2172        page.title || "Page",
2173        page.url,
2174        discoveryLabel(page.pageType),
2175        discoveryLabel(page.status),
2176        page.status === "scanned" ? numberText(page.productsFound) : "—",
2177        page.reason ? discoveryLabel(page.reason) : "—",
2178      ];
2179      values.forEach((value, index) => {
2180        const cell = document.createElement("td");
2181        cell.dataset.label = ["Page", "URL", "Page type", "Status", t("mvp.details.candidates", null, "Product candidates detected"), "Reason"][index];
2182        if (index === 1 && page.url) {
2183          const link = document.createElement("a");
2184          link.href = page.url;
2185          link.target = "_blank";
2186          link.rel = "noopener";
2187          link.textContent = page.url;
2188          cell.append(link);
2189        } else {
2190          cell.textContent = String(value);
2191        }
2192        row.append(cell);
2193      });
2194      body.append(row);
2195    });
2196    table.append(body);
2197    return table;
2198  };
2199
2200  const isProblemDiscoveryPage = (page) => {
2201    const status = String(page?.status || "").toLowerCase();
2202    const reason = String(page?.reason || "").toLowerCase();
2203    const pageType = String(page?.pageType || "").toLowerCase();
2204    if (status === "scanned") {
2205      return finiteCount(page.productsFound) === 0 && ["listing_or_category", "product_detail"].includes(pageType);
2206    }
2207    if (status === "failed") return true;
2208    return status === "excluded" && ["classification_uncertain", "uncertain_catalog_relevance"].includes(reason);
2209  };
2210
2211  const discoveryProblemPages = (discovery) => (
2212    Array.isArray(discovery?.pages) ? discovery.pages.filter(isProblemDiscoveryPage) : []
2213  );
2214
2215  const renderDiscoveryMap = (discovery, { problemOnly = true, filter = "needs_review" } = {}) => {
2216    const container = E("mvpDiscoveryMapGroups");
2217    if (!container) return;
2218    container.replaceChildren();
2219    const allPages = Array.isArray(discovery?.pages) ? discovery.pages : [];
2220    const selectedFilter = problemOnly ? "needs_review" : filter;
2221    const pages = selectedFilter === "needs_review"
2222      ? allPages.filter(isProblemDiscoveryPage)
2223      : allPages.filter((page) => page.status === selectedFilter);
2224    const grouped = { scanned: [], queued: [], excluded: [], failed: [] };
2225    pages.forEach((page) => {
2226      if (grouped[page.status]) grouped[page.status].push(page);
2227    });
2228    setText("mvpCrawlStartingPage", textOrDash(discovery?.submittedUrl));
2229    setText("mvpCrawlHost", textOrDash(discovery?.host));
2230    setText("mvpCrawlCandidates", numberText(discovery?.candidatePagesFound));
2231    setText("mvpCrawlScanned", numberText(discovery?.pagesScanned));
2232    setText("mvpCrawlProductsReturned", numberText(discovery?.productsFound));
2233    setText("mvpCrawlQueued", numberText(discovery?.pagesRemaining));
2234    setText("mvpCrawlExcluded", numberText(discovery?.excludedPages)
2234);
2235    setText("mvpCrawlFailed", numberText(discovery?.failedPages));
2236    const filterNotice = E("mvpDiscoveryProblemFilter");
2237    if (filterNotice) filterNotice.hidden = selectedFilter !== "needs_review";
2238    document.querySelectorAll("[data-mvp-page-filter]").forEach((button) => {
2239      button.setAttribute("aria-pressed", button.dataset.mvpPageFilter === selectedFilter ? "true" : "false");
2240      const totals = {
2241        needs_review: discoveryProblemPages(discovery).length,
2242        scanned: discovery?.pagesScanned,
2243        queued: discovery?.pagesRemaining,
2244        excluded: discovery?.excludedPages,
2245        failed: discovery?.failedPages,
2246      };
2247      const labels = { needs_review: "Needs review", scanned: "Scanned", queued: "Waiting to scan", excluded: "Excluded", failed: "Failed" };
2248      const status = button.dataset.mvpPageFilter;
2249      button.textContent = `${labels[status] || discoveryLabel(status)} (${numberText(totals[status])})`;
2250    });
2251    if (pages.length === 0) {
2252      const empty = document.createElement("p");
2253      empty.className = "mvp-discovery-map-empty";
2254      empty.textContent = t("mvp.details.emptyFilter", null, "No pages match this filter.");
2255      container.append(empty);
2256      return;
2257    }
2258    Object.entries(grouped).forEach(([status, groupPages]) => {
2259      if (!groupPages.length) return;
2260      const section = document.createElement("section");
2261      section.className = `mvp-discovery-map-group mvp-discovery-map-group--${status}`;
2262      const heading = document.createElement("h4");
2263      const canonicalQueued = finiteCount(discovery?.pagesRemaining);
2264      heading.textContent = status === "queued" && groupPages.length !== canonicalQueued
2265        ? `Waiting-to-scan routes shown (${groupPages.length} of ${canonicalQueued})`
2266        : `${discoveryLabel(status)} (${groupPages.length})`;
2267      section.append(heading);
2268      if (groupPages.length === 0) {
2269        const empty = document.createElement("p");
2270        empty.className = "status mvp-discovery-map-empty";
2271        empty.textContent = status === "queued" && canonicalQueued > 0
2272          ? `${plural(canonicalQueued, "page")} remain waiting to scan; route details are not included in this view.`
2273          : `No ${status === "queued" ? "waiting-to-scan" : status} pages.`;
2274        section.append(empty);
2275      } else {
2276        const previewLimit = ["queued", "excluded"].includes(status) ? 3 : 10;
2277        section.append(renderDiscoveryPageTable(groupPages.slice(0, previewLimit)));
2278        if (groupPages.length > previewLimit) {
2279          const remaining = document.createElement("details");
2280          remaining.className = "mvp-discovery-map-more";
2281          const summary = document.createElement("summary");
2282          summary.textContent = `Show all ${groupPages.length} ${status} pages`;
2283          remaining.append(summary, renderDiscoveryPageTable(groupPages.slice(previewLimit)));
2284          section.append(remaining);
2285        }
2286      }
2287      container.append(section);
2288    });
2289  };
2290
2291  const discoveryModelFromResponse = (data) => {
2292    const coverage = data?.customerSummary?.discovery_coverage || data?.discoveryCoverage || null;
2293    if (!coverage || coverage.enabled !== true) return null;
2294    const selectedExtractUrls = Array.isArray(coverage.selected_extract_urls)
2295      ? coverage.selected_extract_urls
2296      : Array.isArray(coverage.selectedExtractUrls) ? coverage.selectedExtractUrls : [];
2297    const routesExtracted = Array.isArray(coverage.routes_extracted)
2298      ? coverage.routes_extracted
2299      : Array.isArray(coverage.routesExtracted) ? coverage.routesExtracted : [];
2300    const productsFound = finiteCount(
2301      coverage.products_after_dedupe
2302        ?? coverage.productsAfterDedupe
2303        ?? data?.summary?.productsFound
2304        ?? data?.customerSummary?.products_found,
2305    );
2306    const hasPageStatuses = Array.isArray(coverage.pages) && coverage.pages.length > 0;
2307    const pages = (hasPageStatuses ? coverage.pages : []).flatMap((page) => {
2308      const url = safeDiscoveryPageUrl(page?.url);
2309      const status = String(page?.status || "").toLowerCase();
2310      if (!url || !["scanned", "queued", "excluded", "failed"].includes(status)) return [];
2311      return [{
2312        title: String(page?.title || "").trim(),
2313        url,
2314        pageType: page?.page_type ?? page?.pageType,
2315        status,
2316        productsFound: finiteCount(page?.products_found ?? page?.productsFound),
2317        reason: page?.reason,
2318      }];
2319    });
2320    const reportedFailedOrBlockedPages = Array.isArray(coverage.pages_blocked_or_failed)
2321      ? coverage.pages_blocked_or_failed
2322      : Array.isArray(coverage.pagesBlockedOrFailed) ? coverage.pagesBlockedOrFailed : [];
2323    const reportedCandidates = firstFiniteCount(coverage.candidate_pages_found, coverage.candidatePagesFound);
2324    const reportedAttempted = firstFiniteCount(coverage.pages_attempted, coverage.pagesAttempted);
2325    const reportedRemaining = firstFiniteCount(coverage.pages_remaining, coverage.pagesRemaining);
2326    const submittedUrl = safeDiscoveryPageUrl(coverage.submitted_url ?? coverage.submittedUrl);
2327    pages.forEach((page) => { page.isSubmitted = Boolean(submittedUrl && page.url === submittedUrl); });
2328    if (submittedUrl && !pages.some((page) => page.isSubmitted)) {
2329      pages.unshift({ title: "Submitted page", url: submittedUrl, pageType: coverage.source_page_type ?? coverage.sourcePageType, status: "mapped", productsFound: null, reason: null, isSubmitted: true });
2330    }
2331    const listedFailedPages = pages.filter((page) => page.status === "failed").length;
2332    const listedScanned = hasPageStatuses ? pages.filter((page) => page.status === "scanned").length : 0;
2333    const pagesScanned = Math.max(listedScanned, reportedAttempted ?? 0, selectedExtractUrls.length);
2334    const listedRemaining = pages.filter((page) => page.status === "queued").length;
2335    const pagesRemaining = reportedRemaining ?? (hasPageStatuses ? listedRemaining : 0);
2336    const candidatePagesFound = Math.max(pagesScanned + pagesRemaining, reportedCandidates ?? 0);
2337    return {
2338      enabled: true,
2339      submittedUrl,
2340      sourcePageType: coverage.source_page_type ?? coverage.sourcePageType,
2341      candidatePagesFound,
2342      pagesScanned,
2343      pagesRemaining,
2344      reportedPagesRemaining: reportedRemaining,
2345      pagesWithProducts: firstFiniteCount(
2346        coverage.pages_with_product_signals,
2347        coverage.pagesWithProductSignals,
2348        hasPageStatuses ? pages.filter((page) => page.status === "scanned" && finiteCount(page.productsFound) > 0).length : null,
2349      ) ?? Math.max(selectedExtractUrls.length, routesExtracted.length),
2350      routesExtracted,
2351      productsFound,
2352      stoppedReason: coverage.stopped_reason ?? coverage.stoppedReason,
2353      note: coverage.note,
2354      host: data?.customerSummary?.supplier_context?.host,
2355      pages,
2356      excludedPages: pages.filter((page) => page.status === "excluded").length,
2357      failedPages: pages.length ? listedFailedPages : reportedFailedOrBlockedPages.length,
2358      failedOrBlockedPages: [
2359        ...reportedFailedOrBlockedPages,
2360        ...pages.filter((page) => page.status === "failed"),
2361      ],
2362    };
2363  };
2364
2365  const deriveMvpResultViewModel = (data, requestMeta = {}) => {
2366    const summary = data?.summary || {};
2367    const customerSummary = data?.customerSummary || {};
2368    const report = data?.report || {};
2369    const discovery = discoveryModelFromResponse(data);
2370    const products = Array.isArray(data?.products) ? data.products : null;
2371    const productsCollected = finiteCount(
2372      products ? products.length : (customerSummary.discovery_coverage || data?.discoveryCoverage)?.products_after_dedupe ?? summary.productsFound ?? customerSummary.products_found ?? report.totalRows,
2373    );
2374    const importableProducts = finiteCount(summary.importReadyProducts ?? customerSummary.import_ready_products ?? report.readyRows);
2375    const issueCounts = resultIssueCounts(data);
2376    const productsNeedingReview = rowsNeedingReviewCount({
2377      productsFound: productsCollected,
2378      summary,
2379      customerSummary,
2380      preview: products || data?.preview,
2381      sourceType: requestMeta?.sourceType,
2382    });
2383    const pages = Array.isArray(discovery?.pages) ? discovery.pages : [];
2384    const pagesScanned = finiteCount(discovery?.pagesScanned);
2385    const pagesRemaining = finiteCount(discovery?.pagesRemaining);
2386    const pagesProducingProducts = finiteCount(discovery?.pagesWithProducts);
2387    const reviewPages = pages.filter(isProblemDiscoveryPage);
2388    const pagesFailedOrBlocked = pages.filter((page) => page.status === "failed").length;
2389    const enrichment = data?.enrichment || summary.enrichment || customerSummary.enrichment || null;
2390    const eligibility = data?.enrichmentEligibility || {};
2391    const productsRemainingForEnrichment = finiteCount(eligibility.productsEligible);
2392    const productsAttemptedForEnrichment = finiteCount(enrichment?.productsAttempted);
2393    const productsEnriched = finiteCount(enrichment?.productsEnriched);
2394    const productsChecked = finiteCount(
2395      enrichment?.productsChecked
2396      ?? enrichment?.completedUrls?.length
2397      ?? (finiteCount(enrichment?.productsEnriched) + finiteCount(enrichment?.productsPartial)),
2398    );
2399    const productsImproved = finiteCount(enrichment?.productsImproved ?? enrichment?.productsEnriched);
2400    const productsEligibleForEnrichment = Math.min(
2401      productsCollected,
2402      Math.max(finiteCount(enrichment?.productsEligible), finiteCount(enrichment?.productsAttempted), productsChecked + productsRemainingForEnrichment),
2403    );
2404    const productsIneligibleForEnrichment = Math.max(0, productsCollected - productsEligibleForEnrichment);
2405    const productsPartiallyEnriched = finiteCount(enrichment?.productsPartial);
2406    const productsFailedEnrichment = finiteCount(enrichment?.productsFailed);
2407    const enrichmentStatus = enrichmentInProgress ? "running"
2408      : enrichment ? String(enrichment.status || "completed").toLowerCase()
2409        : productsRemainingForEnrichment > 0 ? "available" : "not_started";
2410    const runStatus = String(data?.status || "completed").toLowerCase() === "failed" ? "failed" : "completed";
2411    const datasetStatus = runStatus === "failed" || productsCollected === 0 ? "failed"
2412      : pagesRemaining > 0 ? "partial"
2413        : productsNeedingReview > 0 || reviewPages.length > 0 || productsFailedEnrichment > 0 ? "needs_review" : "complete";
2414    const recommendedAction = runStatus === "failed" ? "retry"
2415      : pagesRemaining > 0 ? "continue_scan"
2416        : productsRemainingForEnrichment > 0 ? "enrich"
2417          : "generate_files";
2418    return {
2419      runStatus,
2420      datasetStatus,
2421      productsCollected,
2422      importableProducts,
2423      productsNeedingReview,
2424      productsWithoutImages: finiteCount(issueCounts.missing_image),
2425      productsWithoutSkus: finiteCount(issueCounts.missing_sku),
2426      pagesDiscovered: firstFiniteCount(discovery?.candidatePagesFound, pages.length) ?? 0,
2427      pagesScanned,
2428      pagesRemaining,
2429      pagesProducingProducts,
2430      pagesNeedingReview: reviewPages.length,
2431      pagesFailedOrBlocked,
2432      routesExtracted: discovery?.routesExtracted || [],
2433      enrichmentStatus,
2434      productsEligibleForEnrichment,
2435      productsRemainingForEnrichment,
2436      productsIneligibleForEnrichment,
2437      productsAttemptedForEnrichment,
2438      productsEnriched,
2439      productsChecked,
2440      productsImproved,
2441      productsPartiallyEnriched,
2442      productsFailedEnrichment,
2443      stoppedReason: discovery?.stoppedReason || null,
2444      recommendedAction,
2445      discovery,
2446    };
2447  };
2448
2449  const commerceReadinessFrom = (data) => {
2450    const potential = catalogPotentialFromResponse(data);
2451    return potential?.commerceReadiness || potential?.commerce_readiness || null;
2452  };
2453
2454  const setMvpIntent = (intent, { renderResult = true } = {}) => {
2455    selectedMvpIntent = intent === "visibility" ? "visibility" : "extraction";
2456    intentInputs.forEach((input) => {
2457      input.checked = input.value === selectedMvpIntent;
2458    });
2459    if (runButton) {
2460      runButton.textContent = selectedMvpIntent === "visibility"
2461        ? t("mvp.intent.visibilityCta", null, "Check product visibility")
2462        : t("mvp.intent.extractionCta", null, "Extract product catalog");
2463    }
2464    if (renderResult && lastMvpResultViewModel && lastMvpResult) {
2465      renderMvpResultViewModel(lastMvpResultViewModel, lastMvpResult);
2466    }
2467  };
2468
2469  const readinessDisplayLabel = (value) => {
2470    const normalized = String(value || "").toLowerCase();
2471    return normalized ? normalized.charAt(0).toUpperCase() + normalized.slice(1) : t("mvp.findings.notAssessed", null, "Not assessed");
2472  };
2473
2474  const renderMachineVisibility = (viewModel, data, isUrlResult) => {
2475    const section = E("mvpMachineVisibility");
2476    const visibility = data?.customerSummary?.machine_visibility || data?.machineVisibility || null;
2477    const fields = visibility?.fields;
2478    if (!section) return;
2479    const zeroExtractionResult = selectedMvpIntent === "extraction" && viewModel.productsCollected === 0;
2480    section.hidden = !isUrlResult || !fields || zeroExtractionResult;
2481    if (section.hidden) return;
2482    const machineVisibilityTitle = E("mvpMachineVisibilityTitle");
2483    const machineVisibilitySupport = E("mvpMachineVisibilitySupport");
2484    if (machineVisibilityTitle) machineVisibilityTitle.hidden = selectedMvpIntent === "extraction";
2485    if (machineVisibilitySupport) machineVisibilitySupport.hidden = selectedMvpIntent === "extraction";
2486    const countAndPercent = (field) => {
2487      const visible = finiteCount(field?.visible);
2488      const total = finiteCount(field?.total);
2489      const percent = Math.min(100, finiteCount(field?.percent));
2490      return t(
2491        "mvp.visibility.countPercent",
2492        { visible, total, percent },
2493        `${visible} of ${total} · ${percent}%`,
2494      );
2495    };
2496    setText("mvpMachineVisibilityName", countAndPercent(fields.name));
2497    setText("mvpMachineVisibilityUrl", countAndPercent(fields.product_url || fields.productUrl));
2498    setText("mvpMachineVisibilityPrice", countAndPercent(fields.price));
2499    setText("mvpMachineVisibilityImage", countAndPercent(fields.image));
2500    setText("mvpMachineVisibilitySku", countAndPercent(fields.sku));
2501    setText("mvpMachineVisibilityDescription", countAndPercent(fields.description));
2502    const setCollectedProductCoverage = (id, field, label, key) => {
2503      const visible = finiteCount(field?.visible);
2504      const total = finiteCount(field?.total);
2505      setText(id, t(key, { visible, total }, `${label} present for ${visible} of ${total} collected products.`));
2506    };
2507    setCollectedProductCoverage("mvpMachineVisibilityNameExplanation", fields.name, "Product name", "mvp.visibility.nameCollected");
2508    setCollectedProductCoverage("mvpMachineVisibilityUrlExplanation", fields.product_url || fields.productUrl, "Product URL", "mvp.visibility.urlCollected");
2509    setCollectedProductCoverage("mvpMachineVisibilityImageExplanation", fields.image, "Image", "mvp.visibility.imageCollected");
2510    setCollectedProductCoverage("mvpMachineVisibilityPriceExplanation", fields.price, "Price", "mvp.visibility.priceCollected");
2511    setCollectedProductCoverage("mvpMachineVisibilitySkuExplanation", fields.sku, "SKU", "mvp.visibility.skuCollected");
2512    setCollectedProductCoverage("mvpMachineVisibilityDescriptionExplanation", fields.description, "Description", "mvp.visibility.descriptionCollected");
2513    const products = finiteCount(visibility.products_analyzed ?? visibility.productsAnalyzed);
2514    const zeroVisibilityResult = selectedMvpIntent === "visibility" && viewModel.productsCollected === 0;
2515    section.classList.toggle("mvp-machine-visibility--zero", zeroVisibilityResult);
2516    const visibilityFields = E("mvpMachineVisibilityFields");
2517    if (visibilityFields) visibilityFields.hidden = zeroVisibilityResult;
2518    const zeroVisibilityOffer = E("mvpZeroVisibilityOffer");
2519    if (zeroVisibilityOffer) zeroVisibilityOffer.hidden = !zeroVisibilityResult;
2520    const pagesScanned = viewModel?.discovery
2521      ? firstFiniteCount(viewModel.pagesScanned, visibility.pages_scanned, visibility.pagesScanned)
2522      : firstFiniteCount(visibility.pages_scanned, visibility.pagesScanned);
2523    const pagesDiscovered = viewModel?.discovery
2524      ? firstFiniteCount(
2525        viewModel.pagesDiscovered,
2526        visibility.candidate_catalog_pages_discovered,
2527        visibility.candidateCatalogPagesDiscovered,
2528      )
2529      : firstFiniteCount(visibility.candidate_catalog_pages_discovered, visibility.candidateCatalogPagesDiscovered);
2530    const coverage = String(
2531      visibility.scan_coverage
2532      || visibility.scanCoverage
2533      || (viewModel?.discovery ? (viewModel.pagesRemaining > 0 ? "partial" : "complete") : ""),
2534    ).toLowerCase();
2535    const productScope = pagesScanned !== null
2536      ? t(
2537        "mvp.visibility.productsAcrossPages",
2538        { products, pages: pagesScanned },
2539        `${products} products analyzed across ${pagesScanned}
2539 sampled catalog pages`,
2540      )
2541      : t(
2542        "mvp.visibility.productsAnalyzed",
2543        { count: products },
2544        `${products} products analyzed in the catalog sample`,
2545      );
2546    const coverageScope = coverage === "partial"
2547      ? t("mvp.visibility.partialCoverage", null, "partial")
2548      : coverage === "complete" ? t("mvp.visibility.completeCoverage", null, "complete") : "";
2549    setText("mvpMachineVisibilityScope", productScope);
2550    setText(
2551      "mvpMachineVisibilityCoverage",
2552      pagesDiscovered !== null && coverageScope
2553        ? t(
2554          "mvp.visibility.coverageContext",
2555          { discovered: pagesDiscovered, coverage: coverageScope },
2556          `${pagesDiscovered} catalog pages discovered · ${coverageScope} catalog coverage`,
2557        )
2558        : pagesDiscovered !== null
2559          ? t("mvp.visibility.routesDiscovered", { discovered: pagesDiscovered }, `${pagesDiscovered} catalog pages discovered`)
2560          : coverageScope,
2561    );
2562    setText(
2563      "mvpMachineVisibilityNote",
2564      t(
2565        "mvp.visibility.note",
2566        null,
2567        "Not detected does not necessarily mean missing. Some information may exist deeper on product pages.",
2568      ),
2569    );
2570    const hasEnrichmentEvidence = viewModel.productsAttemptedForEnrichment > 0 || viewModel.productsChecked > 0;
2571    setText(
2572      "mvpMachineVisibilityTitle",
2573      zeroVisibilityResult
2574        ? t("mvp.visibility.zeroTitle", null, "Bots may have trouble finding your products")
2575        : selectedMvpIntent === "extraction"
2576        ? t("mvp.visibility.collectedTitle", null, "Collected product data")
2577        : hasEnrichmentEvidence
2578        ? t("mvp.visibility.originalTitle", null, "What product information was easy to find before enrichment?")
2579        : t("mvp.visibility.title", null, "What product information is easy to find?"),
2580    );
2581    setText(
2582      "mvpMachineVisibilitySupport",
2583      zeroVisibilityResult
2584        ? t("mvp.visibility.zeroBody", null, "ScrapingSmart found your catalog structure, but could not reliably detect product records on the public pages checked. Search engines, AI tools, shopping platforms, and other automated systems may have similar difficulty understanding or reaching your product catalog.")
2585        : selectedMvpIntent === "extraction"
2586        ? t("mvp.visibility.collectedSupport", null, "A compact summary of the product information collected so far.")
2587        : t("mvp.visibility.support", null, "See which product details were found on the catalog pages checked."),
2588    );
2589    if (zeroVisibilityResult) {
2590      setText("mvpMachineVisibilityNote", t("mvp.visibility.zeroQualification", null, "This does not mean your products are missing. They may be loaded dynamically, buried deeper in the site, or exposed in ways that are difficult for automated systems to interpret."));
2591      setText("mvpZeroVisibilityOfferTitle", t("mvp.visibility.zeroOfferTitle", null, "Create AI-ready product JSON"));
2592      setText("mvpZeroVisibilityOfferBody", t("mvp.visibility.zeroOfferBody", null, "Upload your existing product CSV or saved catalog HTML. ScrapingSmart will normalize the product data and generate structured JSON that AI tools, agents, automations, and integrations can use directly."));
2593      setText("mvpCreateAiJson", t("mvp.visibility.zeroOfferCta", null, "Create AI-ready JSON"));
2594      setText("mvpZeroVisibilityCsvTitle", t("mvp.visibility.zeroCsvTitle", null, "Have a product CSV?"));
2595      setText("mvpZeroVisibilityCsvBody", t("mvp.visibility.zeroCsvBody", null, "Upload it and ScrapingSmart will normalize it into structured JSON."));
2596      setText("mvpZeroVisibilityHtmlTitle", t("mvp.visibility.zeroHtmlTitle", null, "Products visible in your browser but not detected?"));
2597      setText("mvpZeroVisibilityHtmlBody", t("mvp.visibility.zeroHtmlBody", null, "Upload saved catalog HTML and ScrapingSmart will extract the visible product data and convert it to structured JSON."));
2598      setText("mvpZeroVisibilityUsesTitle", t("mvp.visibility.zeroUsesTitle", null, "AI-ready JSON can be used for:"));
2599      [["Agents", "AI agents"], ["Automation", "automation workflows"], ["Search", "internal product search"], ["Rag", "RAG / knowledge bases"], ["Integrations", "catalog integrations"], ["Apis", "downstream APIs and data pipelines"]].forEac
2599h(([id, fallback]) => setText(`mvpZeroVisibilityUse${id}`, t(`mvp.visibility.zeroUse${id}`, null, fallback)));
2600    }
2601    const recovered = E("mvpRecoveredProductData");
2602    if (hasEnrichmentEvidence) {
2603      const issueCounts = resultIssueCounts(data);
2604      const ownCount = (...keys) => {
2605        const key = keys.find((candidate) => Object.prototype.hasOwnProperty.call(issueCounts, candidate));
2606        return key ? finiteCount(issueCounts[key]) : null;
2607      };
2608      const available = (missing) => missing === null ? null : Math.max(0, viewModel.productsCollected - missing);
2609      const recoveredCount = (count) => t(
2610        "mvp.visibility.recoveredCount",
2611        { count, total: viewModel.productsCollected },
2612        `${count} of ${viewModel.productsCollected}`,
2613      );
2614      const metrics = [
2615        ["mvpRecoveredSkuItem", "mvpRecoveredSku", available(ownCount("missing_sku"))],
2616        ["mvpRecoveredDescriptionItem", "mvpRecoveredDescription", available(ownCount("missing_description", "weak_description"))],
2617        ["mvpRecoveredPriceItem", "mvpRecoveredPrice", available(ownCount("missing_price"))],
2618      ];
2619      metrics.forEach(([itemId, valueId, count]) => {
2620        const item = E(itemId);
2621        if (item) item.hidden = count === null;
2622        if (count !== null) setText(valueId, recoveredCount(count));
2623      });
2624      if (recovered) recovered.hidden = !metrics.some(([, , count]) => count !== null);
2625    } else if (recovered) {
2626      recovered.hidden = true;
2627    }
2628  };
2629
2630  const catalogSourcesFrom = (data) => {
2631    const files = (data?.customerSummary?.discovery_coverage || data?.discoveryCoverage)?.catalog_files;
2632    return Array.isArray(files) ? files.filter((file) => Boolean(toSafeHttpUrl(file?.url))) : [];
2633  };
2634
2635  // Apply after access gates too: those can restore download and cleanup controls.
2636  const renderCompletedResultLayout = (viewModel, data) => {
2637    if (["queued", "running", "discovering", "processing", "failed", "cancelled", "canceled"].includes(String(data?.discoveryJob?.state || "").toLowerCase()) || data?.status === "failed") return;
2638    const zero = viewModel.productsCollected === 0;
2639    const sources = catalogSourcesFrom(data);
2640    const hide = (id, value = true) => { const node = E(id); if (node) node.hidden = value; };
2641    for (const id of ["mvpFindingsCoverage", "mvpFindingsCommerce", "mvpFindingsDataset", "mvpCatalogMapSummary", "mvpCatalogPageList", "mvpScanProgressSummary", "mvpScanProgressSubcopy", "mvpDiscoveryCompleteMessage", "mvpMainIssuesDownload", "mvpRecommendedActionMessage", "mvpResultBanner", "mvpDownloadsTitle", "mvpPreviewAccountGate", "mvpStateMessage", "mvpStateHint"]) hide(id);
2642    hide("mvpFreeFiles", false);
2643    hide("mvpNoProductExport", !zero);
2644    setText("mvpNoProductExport", t("mvp.results.noExport", null, "No product export available. Product exports become available when usable products are extracted."));
2645    successPanel?.classList.toggle("mvp-results--zero", zero);
2646    E("mvpDiscoveryCoverage")?.querySelector(".mvp-discovery-heading")?.setAttribute("hidden", "");
2647    E("mvpDiscoveryCoverage")?.querySelector(".mvp-discovery-details")?.setAttribute("hidden", "");
2648    E("mvpCatalogPotential")?.querySelector(".mvp-catalog-potential-summary")?.querySelectorAll("div").forEach((node, index) => { node.hidden = index < 3; });
2649    setText("mvpCrawlProductsReturned", viewModel.productsCollected);
2650    setText("mvpCrawlSignalPages", viewModel.pagesProducingProducts);
2651    const duplicateIssues = E("mvpMainIssuesDownload")?.parentElement;
2652    if (duplicateIssues) duplicateIssues.hidden = true;
2653    hide("mvpFindingsTitle", true);
2654    setText("mvpCrawlProductCandidates", finiteCount(viewModel.discovery?.productsFound));
2655    setText("mvpCrawlPageType", discoveryLabel(viewModel.discovery?.sourcePageType));
2656    for (const id of ["mvpCsvDownloadItem", "mvpShopifyDownloadItem", "mvpBigcommerceDownloadItem", "mvpSummaryDownloadItem", "mvpLlmDownloadItem", "mvpEnrichmentDownloadItem"]) hide(id, zero);
2657    if (zero) {
2658      for (const id of ["mvpExportStep", "mvpEnrichmentAction", "mvpProductReviewSection", "mvpReviewPreviewSlot", "mvpMachineVisibility", "mvpZeroVisibilityOffer", "mvpPrimaryRecommendation", "mvpMarketingContentStep", "mvpCleanupPanel"]) hide(id);
2659      if (!["queued", "running", "discovering", "processing"].includes(String(data?.discoveryJob?.state || "").toLowerCase())) {
2660        setText("mvpResultsTitle", t("mvp.results.zeroRetained", null, "No usable products extracted"));
2661        const candidates = finiteCount(viewModel.discovery?.productsFound);
2662        setText("mvpCatalogProgressSummary", t(viewModel.pagesScanned === 1 ? "mvp.results.retainedSummaryOne" : "mvp.results.retainedSummary", { pages: viewModel.pagesScanned, products: 0, sources: sources.length, sourceSummary: t(sources.length === 1 ? "mvp.results.sourceOne" : "mvp.results.sourceMany", { count: sources.length }, `${plural(sources.length, "catalog source")} found`) }, `${plural(viewModel.pagesScanned, "page")} scanned · 0 products retained · ${plural(sources.length, "catalog source")} found`) + (candidates > 0 ? ` · ${plural(candidates, "product candidate")} detected` : ""));
2663      }
2664      setText("mvpCommerceProductStructuredData", t("mvp.results.notAssessed", null, "Not assessed: no usable products retained"));
2665    }
2666    setText("mvpCompletedSignupTitle", t(zero ? "mvp.signup.saveContinue" : "mvp.signup.claim", null, zero ? "Save this scan and continue" : "Claim this scan"));
2667    setText("mvpCompletedSignupBody", t(zero ? "mvp.signup.catalogBody" : "mvp.signup.productBody", null, zero ? "Create an account to keep these catalog sources, reopen this 
2667scan, and continue building the dataset." : "Create an account or sign in to save this scan and reopen it later."));
2668    setText("mvpCompletedSignupCreate", t("mvp.signup.createFree", null, "Create free account"));
2669    completedSignupCta?.querySelector("ul")?.setAttribute("hidden", "");
2670    const upsell = completedSignupCta?.querySelector(".mvp-completed-signup-upsell");
2671    if (upsell) upsell.hidden = zero;
2672    setText("mvpCatalogSourcesGuidance", t("mvp.results.catalogNext", null, "Try one of these catalog sources or a product/category page to continue building the dataset."));
2673    const guidance = E("mvpCatalogSourcesGuidance");
2674    if (guidance) E("mvpCatalogSources")?.after(guidance);
2675  };
2676
2677  const renderZeroExtractionDiagnostic = (viewModel, isUrlResult, data = {}) => {
2678    const diagnostic = E("mvpZeroExtractionDiagnostic");
2679    if (!diagnostic) return;
2680    const files = (data?.customerSummary?.discovery_coverage || data?.discoveryCoverage)?.catalog_files;
2681    const catalogFiles = Array.isArray(files) ? files.filter((file) => /^https?:\/\//i.test(String(file?.url || ""))) : [];
2682    const showCatalogs = catalogFiles.length > 0
2683      && (data.status === "needs_capture" || data.reasonCode === "too_few_products" || viewModel.productsCollected === 0);
2684    const showDiagnostic = isUrlResult
2685      && (selectedMvpIntent === "extraction" || showCatalogs)
2686      && (viewModel.productsCollected === 0 || showCatalogs);
2687    diagnostic.hidden = !showDiagnostic;
2688    if (!showDiagnostic) return;
2689    const catalogList = E("mvpCatalogSources");
2690    E("mvpCatalogSourcesGuidance").hidden = !showCatalogs;
2691    catalogList.replaceChildren();
2692    catalogList.hidden = !showCatalogs;
2693    diagnostic.querySelector(".mvp-zero-extraction-actions").hidden = showCatalogs;
2694    diagnostic.querySelector(".mvp-dataset-actions").hidden = showCatalogs;
2695    if (showCatalogs) {
2696      setText("mvpZeroExtractionTitle", t("mvp.results.sourcesTitle", null, "Catalog sources detected"));
2697      setText("mvpZeroExtractionBody", t("mvp.results.sourcesBody", null, "ScrapingSmart found product catalog sources linked from this supplier."));
2698      catalogFiles.forEach((file) => {
2699        if (!/^https?:\/\//i.test(file.url)) return;
2700        const item = document.createElement("li");
2701        const link = document.createElement("a");
2702        const title = String(file.title || "").trim();
2703        const pathTitle = (new URL(file.url).pathname.split("/").filter(Boolean).pop() || "").replace(/\.[^.]+$/, "").replace(/[-_]+/g, " ");
2704        link.textContent = !title || /^catalog(ue)?s?$/i.test(title) ? pathTitle || new URL(file.url).hostname : title;
2705        link.href = file.url;
2706        link.target = "_blank";
2707        link.rel = "noopener noreferrer";
2708        item.append(link);
2709        catalogList.append(item);
2710      });
2711      return;
2712    }
2713    setText("mvpZeroExtractionTitle", t("mvp.extraction.zeroTitle", null, "No products were found"));
2714    setText(
2715      "mvpZeroExtractionBody",
2716      t(
2717        "mvp.extraction.zeroBody",
2718        null,
2719        "Bots may also have trouble discovering products from this page. We couldn’t find enough accessible product signals or catalog links to extract a product catalog during this scan.",
2720      ),
2721    );
2722    setText("mvpZeroExtractionTryPage", t("mvp.extraction.zeroTryPage", null, "Try a category or collection page instead of the homepage."));
2723    setText("mvpZeroExtractionHtml", t("mvp.extraction.zeroHtml", null, "Upload saved HTML when the site relies heavily on JavaScript or blocks automated access."));
2724    setText("mvpZeroExtractionCsv", t("mvp.extraction.zeroCsv", null, "Upload CSV or product data if the website cannot be extracted reliably."));
2725    setText("mvpZeroExtractionTryAnother", t("mvp.extraction.zeroTryAnother", null, "Try another page"));
2726    setText("mvpZeroExtractionUseHtml", t("mvp.extraction.zeroUseHtml", null, "Upload saved HTML"));
2727    setText("mvpZeroExtractionUseCsv", t("mvp.extraction.zeroUseCsv", null, "Upload product CSV"));
2728    const discoveredAction = E("mvpZeroExtractionScanDiscovered");
2729    if (discoveredAction) {
2730      discoveredAction.textContent = t(
2731        viewModel.pagesRemaining === 1
2732          ? "mvp.extraction.zeroScanDiscoveredOne"
2733          : "mvp.extraction.zeroScanDiscovered",
2734        { count: viewModel.pagesRemaining },
2735        `${plural(viewModel.pagesRemaining, "discovered catalog page")} ${viewModel.pagesRemaining === 1 ? "is" : "are"} available to scan.`,
2736      );
2737      discoveredAction.hidden = viewModel.pagesRemaining <= 0;
2738    }
2739  };
2740
2741  const renderFindings = (viewModel, data) => {
2742    const coverage = E("mvpFindingsCoverage");
2743    if (coverage) coverage.hidden = !viewModel.discovery;
2744    if (viewModel.discovery) {
2745      const complete = viewModel.pagesRemaining <= 0 && viewModel.pagesFailedOrBlocked <= 0;
2746      setText(
2747        "mvpFindingsCoverageStatus",
2748        complete
2749          ? t("mvp.findings.coverageComplete", null, "Complete scan: all discovered catalog pages were scanned.")
2750          : t("mvp.findings.coveragePartial", { count: viewModel.pagesRemaining }, `Partial scan: ${plural(viewModel.pagesRemaining, "catalog page")} remain.`),
2751      );
2752      setText("mvpFindingsPagesScanned", viewModel.pagesScanned);
2753      setText("mvpFindingsPagesRemaining", viewModel.pagesRemaining);
2754      setText("mvpFindingsPagesProducing", viewModel.pagesProducingProducts);
2755      setText("mvpFindingsPagesFailed", viewModel.pagesFailedOrBlocked);
2756      const failedItem = E("mvpFindingsFailedItem");
2757      if (failedItem) failedItem.hidden = viewModel.pagesFailedOrBlocked <= 0;
2758    }
2759
2760    const readiness = commerceReadinessFrom(data);
2761    const commerce = E("mvpFindingsCommerce");
2762    if (commerce) commerce.hidden = !readiness;
2763    if (readiness) {
2764      setText("mvpFindingsCatalogDiscoverability", readinessDisplayLabel(readiness.catalogDiscoverability || readiness.catalog_discoverability));
2765      setText("mvpFindingsProductStructuredData", readinessDisplayLabel(readiness.productStructuredData || readiness.product_structured_data));
2766      setText("mvpFindingsSeoFoundation", readinessDisplayLabel(readiness.seoFoundation || readiness.seo_foundation));
2767      setText("mvpFindingsOverallReadiness", readinessDisplayLabel(readiness.status));
2768      const observation = E("mvpFindingsCommerceObservation");
2769      if (observation) {
2770        observation.textContent = readiness.primaryObservation || readiness.primary_observation || readiness.note || "";
2771        observation.hidden = !observation.textContent;
2772      }
2773    }
2774
2775    setText(
2776      "mvpFindingsDatasetSummary",
2777      t(
2778        "mvp.findings.datasetSummary",
2779        { products: viewModel.productsCollected, importable: viewModel.importableProducts },
2780        `${viewModel.productsCollected} products collected · ${viewModel.importableProducts} currently importable`,
2781      ),
2782    );
2783    const hasEnrichmentEligibility = Number.isFinite(Number(data?.enrichmentEligibility?.productsEligible));
2784    const hasEnrichmentEvidence = viewModel.productsAttemptedForEnrichment > 0 || viewModel.productsChecked > 0;
2785    setText(
2786      "mvpFindingsEnrichmentState",
2787      hasEnrichmentEligibility || hasEnrichmentEvidence
2788        ? t(
2789          "mvp.findings.enrichmentState",
2790          { checked: viewModel.productsChecked, improved: viewModel.productsImproved },
2791          `${viewModel.productsChecked} product pages checked · ${viewModel.productsImproved} products improved`,
2792        )
2793        : t("mvp.findings.notEnriched", null, "Product details have not been enriched yet."),
2794    );
2795    const completeness = E("mvpFindingsProductCompleteness");
2796    if (completeness) completeness.hidden = !hasEnrichmentEvidence;
2797  };
2798
2799  const renderMvpResultViewModel = (viewModel, data) => {
2800    if (!viewModel) return;
2801    lastMvpResultViewModel = viewModel;
2802    const statusLabels = {
2803      complete: t("mvp.results.readyStatus", null, "Ready"),
2804      partial: t("mvp.results.available", null, "More products available"),
2805      needs_review: t("mvp.dynamic.readyWarnings", null, "Ready with warnings"),
2806      failed: t("mvp.results.noProducts", null, "No products found"),
2807    };
2808    const isUrlResult = data?.customerSummary?.supplier_context?.source_type === "url" || Boolean(viewModel.discovery);
2809    const hasMachineVisibility = isUrlResult && Boolean(data?.customerSummary?.machine_visibility?.fields);
2810    const machineVisibilityPrimary = hasMachineVisibility && selectedMvpIntent === "visibility";
2811    successPanel?.classList.toggle("mvp-results--url", isUrlResult);
2812    successPanel?.classList.toggle("mvp-results--machine-visibility", machineVisibilityPrimary);
2813    successPanel?.classList.toggle("mvp-results--intent-visibility", selectedMvpIntent === "visibility");
2814    successPanel?.classList.toggle("mvp-results--intent-extraction", selectedMvpIntent === "extraction");
2815    E("mvpMachineVisibility")?.classList.remove("mvp-machine-visibility--expanded");
2816    setText("mvpDataQualitySummary", t("mvp.status.dataQuality", null, "Data quality"));
2817    const anonymousResult = data?.auth?.authenticated !== true && !isSignedIn();
2818    const anonymousUrlContinuation = anonymousResult
2819      && isUrlResult
2820      && viewModel.productsCollected > 0
2821      && viewModel.pagesRemaining > 0;
2822    const paidContinuationAvailable = entitlementState().canContinueScan;
2823    const continuationActionAvailable = anonymousResult || paidContinuationAvailable;
2824    const backendScanState = String(data?.discoveryJob?.state || "").toLowerCase();
2825    const scanIsRunning = ["queued", "running", "discovering", "processing"].includes(backendScanState);
2826    const scanIsWaiting = Boolean(viewModel.discovery)
2827      && viewModel.pagesRemaining > 0
2828      && !scanIsRunning
2829      && !["failed", "cancelled", "canceled"].includes(backendScanState);
2830    const scanIsComplete = Boolean(viewModel.discovery)
2831      && viewModel.pagesRemaining <= 0
2832      && !scanIsRunning
2833      && !["failed", "cancelled", "canceled"].includes(backendScanState);
2834    completedSignupCtaEligible = !scanIsRunning
2835      && !["failed", "cancelled", "canceled"].includes(backendScanState)
2836      && data?.ok !== false && data?.status !== "failed"
2837      && (viewModel.productsCollected > 0 || catalogSourcesFrom(data).length > 0);
2838    if (completedSignupCta) completedSignupCta.hidden = !completedSignupCtaEligible || !anonymousResult;
2839    if (stateTitle) {
2840      stateTitle.textContent = scanIsRunning
2841        ? `Scanning catalog — ${viewModel.pagesScanned} of ${viewModel.pagesDiscovered} pages complete`
2842        : scanIsWaiting
2843          ? `${viewModel.pagesRemaining} additional ${viewModel.pagesRemaining === 1 ? "page was" : "pages were"} discovered`
2844          : scanIsComplete
2845            ? "Catalog discovery complete"
2846            : viewModel.productsCollected > 0
2847              ? t("mvp.results.ready", null, "Your dataset is ready")
2848              : t("mvp.results.noProducts", null, "No products found");
2849      stateTitle.hidden = false;
2850    }
2851    const resultIntro = E("mvpResultIntro");
2852    if (resultIntro) resultIntro.hidden = true;
2853    setText("mvpResultStatus", statusLabels[viewModel.datasetStatus] || "Needs review");
2854    if (resultStatus) resultStatus.dataset.state = viewModel.datasetStatus;
2855    setText("mvpOverviewProducts", viewModel.productsCollected);
2856    setText(
2857      "mvpOverviewProductsLabel",
2858      selectedMvpIntent === "extraction" && isUrlResult
2859        ? t("mvp.results.productsFoundSoFar", null, "products found so far")
2860        : t("mvp.results.productsFoundLower", null, "products found"),
2861    );
2862    setText("mvpOverviewPagesScanned", viewModel.pagesScanned);
2863    setText("mvpOverviewPagesDiscovered", viewModel.pagesDiscovered);
2864    setText("mvpOverviewPagesRemaining", viewModel.pagesRemaining);
2865    setText(
2866      "mvpCatalogProgressSummary",
2867      viewModel.discovery
2868        ? t(
2869          "mvp.results.compactProgress",
2870          {
2871            products: viewModel.productsCollected,
2872            scanned: viewModel.pagesScanned,
2873            total: viewModel.pagesDiscovered,
2874            remaining: viewModel.pagesRemaining,
2875          },
2876          scanIsComplete
2877            ? `${viewModel.productsCollected} products collected · ${viewModel.pagesDiscovered} routes evaluated · ${viewModel.routesExtracted.length} catalog pages extracted`
2878            : scanIsWaiting
2879              ? `${viewModel.productsCollected} products found so far · ${viewModel.pagesScanned} pages complete`
2880              : `${viewModel.productsCollected} products found so far · ${viewModel.pagesRemaining} pages remaining`,
2881        )
2882        : t("mvp.results.compactProducts", { products: viewModel.productsCollected }, `${viewModel.productsCollected} products`),
2883    );
2884    const overviewCoverage = E("mvpOverviewCoverage");
2885    if (overviewCoverage) overviewCoverage.hidden = true;
2886    setText("mvpOverviewProductsEnriched", viewModel.productsChecked);
2887    setText("mvpOverviewProductsImproved", viewModel.productsImproved);
2888    setText("mvpOverviewProductsRemaining", viewModel.productsRemainingForEnrichment);
2889    setText("mvpOverviewProductsIneligible", viewModel.productsIneligibleForEnrichment);
2890    if (overviewEnrichment) overviewEnrichment.hidden = true;
2891    const overviewIneligible = E("mvpOverviewEnrichmentIneligible");
2892    if (overviewIneligible) overviewIneligible.hidden = true;
2893    setText("mvpContinueProductsCollected", viewModel.productsCollected);
2894    setText("mvpContinuePagesScanned", viewModel.pagesScanned);
2895    setText("mvpContinuePagesRemaining", viewModel.pagesRemaining);
2896    setText("mvpMetricImportReady", viewModel.importableProducts);
2897    setText("mvpMetricReviewRows", viewModel.productsNeedingReview);
2898    setText("mvpSummaryWarnings", viewModel.productsNeedingReview);
2899    setText("mvpMetricMissingImages", viewModel.productsWithoutImages);
2900    setText("mvpMetricMissingSkus", viewModel.productsWithoutSkus);
2901    renderMachineVisibility(viewModel, data, isUrlResult);
2902    renderZeroExtractionDiagnostic(viewModel, isUrlResult, data);
2903    renderFindings(viewModel, data);
2904    const remainingCatalog = E("mvpOverviewRemainingCatalog");
2905    if (remainingCatalog) {
2906      const remaining = Math.max(0, viewModel.pagesRemaining);
2907      remainingCatalog.textContent = remaining > 0
2908        ? t("mvp.results.pagesStillToScan", { count: remaining }, `${plural(remaining, "page")} still to scan`)
2909        : "";
2910      remainingCatalog.hidden = true;
2911    }
2912    const remainingCatalogSupport = E("mvpOverviewRemainingCatalogSupport");
2913    if (remainingCatalogSupport) remainingCatalogSupport.hidden = true;
2914    if (resultBanner) {
2915      resultBanner.textContent = viewModel.datasetStatus === "failed" ? STATE_COPY.failure.banner : "";
2916      resultBanner.hidden = viewModel.datasetStatus !== "failed";
2917    }
2918    if (stateMessage) {
2919      stateMessage.textContent = "";
2920      stateMessage.hidden = true;
2921    }
2922    if (datasetRecommendation) datasetRecommendation.hidden = true;
2923    if (continueScanLoading) continueScanLoading.hidden = !scanIsRunning;
2924    if (continueScanLoadingStatus) continueScanLoadingStatus.textContent = scanIsRunning ? "Scanning discovered catalog pages…" : "";
2925    if (scanReassurance) {
2926      scanReassurance.textContent = scanIsRunning ? "You can safely leave this page." : "";
2927      scanReassurance.hidden = !scanIsRunning;
2928    }
2929    if (scanIsComplete) {
2930      if (continueScanLoading) continueScanLoading.hidden = true;
2931      if (continueScanLoadingStatus) continueScanLoadingStatus.textContent = "";
2932      if (scanReassurance) scanReassurance.hidden = true;
2933      if (continueScanStatus) {
2934        continueScanStatus.textContent = "";
2935        continueScanStatus.hidden = true;
2936      }
2937    }
2938    if (continueScanProgress) {
2939      continueScanProgress.hidden = !(scanIsRunning || scanIsWaiting) || scanIsComplete;
2940      const progress = viewModel.pagesDiscovered > 0 ? Math.min(100, (viewModel.pagesScanned / viewModel.pagesDiscovered) * 100) : 0;
2941      continueScanProgress.setAttribute("aria-valuemin", "0");
2942      continueScanProgress.setAttribute("aria-valuemax", "100");
2943      continueScanProgress.setAttribute("aria-valuenow", String(Math.round(progress)));
2944      const bar = continueScanProgress.querySelector("span");
2945      if (bar) bar.style.width = `${progress}%`;
2946    }
2947    if (stopDiscoveryButton) {
2948      stopDiscoveryButton.hidden = viewModel.pagesRemaining <= 0 || !continuationActionAvailable;
2949      stopDiscoveryButton.textContent = t("mvp.next.downloadData", null, "Download data");
2950      stopDiscoveryButton.classList.remove("button--primary");
2951      stopDiscoveryButton.classList.add("mvp-secondary-download");
2952    }
2953    if (datasetActions) datasetActions.hidden = true;
2954    if (recommendedAction) recommendedAction.hidden = viewModel.productsCollected <= 0;
2955    const nextActionsTitle = E("mvpNextActionsTitle");
2956    if (nextActionsTitle) {
2957      nextActionsTitle.textContent = t("mvp.next.steps", null, "Next steps");
2958      nextActionsTitle.hidden = viewModel.productsCollected <= 0;
2959    }
2960    const continuationStep = E("mvpContinuationStep");
2961    const continuationStepTitle = E("mvpContinuationStepTitle");
2962    const continuationStepCopy = E("mvpContinuationStepCopy");
2963    if (continuationStep) continuationStep.hidden = true;
2964    if (continuationStepTitle) continuationStepTitle.hidden = true;
2965    if (continuationStepCopy) continuationStepCopy.hidden = true;
2966    const enrichmentTitle = E("mvpEnrichmentTitle");
2967    const enrichmentSupport = E("mvpEnrichmentSupport");
2968    if (enrichmentTitle) enrichmentTitle.textContent = machineVisibilityPrimary
2969      ? t("mvp.next.inspectTitle", null, "2. Inspect product pages")
2970      : t("mvp.next.deeperDetailsTitle", null, "2. Add deeper product details");
2971    if (enrichmentSupport) enrichmentSupport.textContent = machineVisibilityPrimary
2972      ? t(
2973        "mvp.visibility.recoverSupport",
2974        null,
2975        "Check individual product pages for prices, descriptions, specifications, variants, and structured product data.",
2976      )
2977      : t("mvp.next.deeperDetailsCopy", { count: viewModel.productsCollected }, `Check the ${viewModel.productsCollected} products already found for descriptions, specifications, variants, prices, images, and other available product-page data.`);
2978    if (continuationGate) continuationGate.hidden = true;
2979
2980    const primary = E("mvpPrimaryRecommendation");
2981    const message = E("mvpRecommendedActionMessage");
2982    const exportCurrent = E("mvpExportCurrentProducts");
2983    const exportStep = E("mvpExportStep");
2984    const technicalStep = E("mvpTechnicalStep");
2985    setText("mvpExportStepTitle", t("mvp.next.exportTitle", null, "Export your data"));
2986    if (exportStep) exportStep.hidden = machineVisibilityPrimary;
2987    if (technicalStep) technicalStep.hidden = !machineVisibilityPrimary;
2988    const actions = {
2989      enrich: [t("mvp.enrichment.action", null, "Enrich products"), t("mvp.next.enrichHelp", null, "The catalog scan is complete. Enrich eligible products from their product pages.")],
2990      review: [t("mvp.downloads.review", null, "Review issues"), t("mvp.dynamic.problem", null, "Problems were detected that need attention before the dataset is treated as complete.")],
2991      generate_files: [t("mvp.next.downloadCurrent", null, "Download current dataset"), t("mvp.next.complete", null, "The available scan and enrichment work is complete. Your dataset is ready to use.")],
2992      retry: [t("mvp.dynamic.retry", null, "Retry scan"), t("mvp.dynamic.scanFailed", null, "The scan failed before a usable dataset was created.")],
2993    };
2994    const genericAction = ["generate_files", "retry", "review"].includes(viewModel.recommendedAction)
2995      ? viewModel.recommendedAction
2996      : null;
2997    const [label, copy] = actions[genericAction] || ["", ""];
2998    if (primary) {
2999      primary.textContent = label;
3000      if (genericAction) primary.dataset.action = genericAction;
3001      else delete primary.dataset.action;
3002      primary.hidden = !["generate_files", "retry"].includes(genericAction);
3003    }
3004    if (message) {
3005      message.textContent = copy;
3006      message.hidden = !genericAction || anonymousUrlContinuation;
3007    }
3008    if (exportCurrent) {
3009      exportCurrent.hidden = machineVisibilityPrimary;
3010      exportCurrent.textContent = "Download products CSV";
3011    }
3012    if (message && viewModel.productsCollected > 0) {
3013      const readiness = commerceReadinessFrom(data);
3014      const observation = readiness?.primaryObservation || readiness?.primary_observation || readiness?.note || "";
3015      const scanState = viewModel.discovery
3016        ? viewModel.pagesRemaining > 0
3017          ? t("mvp.next.summaryPartial", { count: viewModel.pagesRemaining }, `${plural(viewModel.pagesRemaining, "catalog page")} remain to scan.`)
3018          : t("mvp.next.summaryComplete", null, "The catalog scan is complete.")
3019        : "";
3020      const enrichmentState = viewModel.productsChecked > 0
3021        ? t("mvp.next.summaryEnriched", { checked: viewModel.productsChecked, improved: viewModel.productsImproved }, `${viewModel.productsChecked} product pages checked · ${viewModel.productsImproved} products improved.`)
3022        : t("mvp.findings.notEnriched", null, "Product details have not been enriched yet.");
3023      message.textContent = "";
3024      message.hidden = true;
3025    }
3026    renderCompletedResultLayout(viewModel, data);
3027  };
3028
3029  const renderIncompleteScanWarning = (discovery, resultProductsFound = null) => {
3030    let warning = E("mvpIncompleteScanWarning");
3031    const failedOrBlockedPages = Array.isArray(discovery?.failedOrBlockedPages)
3032      ? discovery.failedOrBlockedPages
3033      : [];
3034    const productsFound = Number(resultProductsFound ?? discovery?.productsFound ?? 0);
3035    const pageType = String(discovery?.sourcePageType || "").toLowerCase();
3036    const extractedRoutes = new Set((discovery?.routesExtracted || []).map(safeDiscoveryPageUrl).filter(Boolean));
3037    const failureMateriallyAffectedExtraction = failedOrBlockedPages.some((page) => {
3038      const url = safeDiscoveryPageUrl(typeof page === "string" ? page : page?.url);
3039      return !url || extractedRoutes.has(url);
3040    });
3041    const shouldShow = discovery?.enabled === true && (
3042      (failedOrBlockedPages.length > 0 && (productsFound <= 0 || failureMateriallyAffectedExtraction))
3043      || (discovery.pagesRemaining === 0 && pageType === "homepage_or_navigation" && productsFound <= 4)
3044    );
3045    if (!shouldShow) {
3046      if (warning) warning.hidden = true;
3047      return;
3048    }
3049    if (!warning && discoveryCoveragePanel) {
3050      warning = document.createElement("section");
3051      warning.id = "mvpIncompleteScanWarning";
3052      warning.className = "mvp-recommended-action mvp-incomplete-scan-warning";
3053      warning.setAttribute("role", "status");
3054      const overview = discoveryCoveragePanel.querySelector(".mvp-discovery-overview");
3055      discoveryCoveragePanel.insertBefore(warning, overview);
3056    }
3057    if (!warning) return;
3058    warning.replaceChildren();
3059    const title = document.createElement("h3");
3060    title.textContent = "Results may be incomplete";
3061    const explanation = document.createElement("p");
3062    explanation.className = "status";
3063    explanation.append("ScrapingSmart found ");
3064    const productCount = document.createElement("strong");
3065    productCount.textContent = `${productsFound} products`;
3066    explanation.append(productCount, ", but some catalog pages could not be accessed or discovered.");
3067    const action = document.createElement("p");
3068    action.className = "status";
3069    const actionStrong = document.createElement("strong");
3070    actionStrong.textContent = "Try a category or collection URL, or upload saved page HTML.";
3071    action.append(actionStrong);
3072    const scope = document.createElement("p");
3073    scope.className = "status";
3074    scope.textContent = "Generated files include only the products ScrapingSmart could access.";
3075    warning.append(title, explanation, action, scope);
3076    warning.hidden = false;
3077  };
3078
3079  const catalogPageDisplayName = (page) => {
3080    const title = String(page?.title || "").trim();
3081    if (title && title.toLowerCase() !== "page" && title !== "Submitted page") return title;
3082    try {
3083      const url = new URL(page?.url);
3084      const segment = decodeURIComponent(url.pathname).replace(/\/+$/, "").split("/").filter(Boolean).pop();
3085      if (segment) return segment.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
3086    } catch { /* fall through to page type */ }
3087    const pageType = String(page?.pageType || "").trim();
3088    return pageType ? discoveryLabel(pageType) : "Catalog page";
3089  };
3090
3091  const catalogPageDisplayPath = (page) => {
3092    try { return new URL(page?.url).pathname || "/"; }
3093    catch { return String(page?.url || ""); }
3094  };
3095
3096  const renderCatalogRoute = (page, status, marker) => {
3097    const row = document.createElement("div");
3098    row.className = `mvp-catalog-route mvp-catalog-route--${status}`;
3099    const icon = document.createElement("span");
3100    icon.className = "mvp-catalog-route-marker";
3101    icon.textContent = marker;
3102    icon.setAttribute("aria-label", status === "queued" ? "Waiting" : status === "review" ? "Needs review" : discoveryLabel(status));
3103    const content = document.createElement("div");
3104    content.className = "mvp-catalog-route-content";
3105    const link = document.createElement("a");
3106    link.className = "mvp-catalog-route-title";
3107    link.href = page.url;
3108    link.target = "_blank";
3109    link.rel = "noopener";
3110    link.textContent = catalogPageDisplayName(page);
3111    const path = document.createElement("span");
3112    path.className = "mvp-catalog-route-path";
3113    path.textContent = catalogPageDisplayPath(page);
3114    content.append(link, path);
3115    row.append(icon, content);
3116    if (page.status === "scanned") {
3117      const count = document.createElement("span");
3118      count.className = "mvp-catalog-route-products";
3119      count.textContent = t("mvp.details.candidateCount", { count: finiteCount(page.productsFound) }, `${finiteCount(page.productsFound)} product candidates detected`);
3120      row.append(count);
3121    }
3122    return row;
3123  };
3124
3125  const appendCatalogGroup = (container, label, pages, status, marker, preview = 0, wasOpen = false) => {
3126    if (!pages.length) return;
3127    const section = document.createElement("section");
3128    section.className = "mvp-catalog-route-group";
3129    const heading = document.createElement("h4");
3130    heading.textContent = label;
3131    section.append(heading);
3132    (preview ? pages.slice(0, preview) : pages).forEach((page) => section.append(renderCatalogRoute(page, status, marker)));
3133    if (preview && pages.length > preview) {
3134      const more = document.createElement("details");
3135      more.className = "mvp-catalog-route-more";
3136      more.open = wasOpen;
3137      const moreSummary = document.createElement("summary");
3138      moreSummary.textContent = `Show ${pages.length - preview} more`;
3139      more.append(moreSummary);
3140      pages.slice(preview).forEach((page) => more.append(renderCatalogRoute(page, status, marker)));
3141      section.append(more);
3142    }
3143    container.append(section);
3144  };
3145
3146  const renderPrimaryCatalogMap = (discovery) => {
3147    const summary = E("mvpCatalogMapSummary");
3148    const list = E("mvpCatalogPageList");
3149    if (!summary || !list) return;
3150    const openGroups = new Set(Array.from(list.querySelectorAll(".mvp-catalog-route-more[open]"))
3151      .map((details) => details.closest(".mvp-catalog-route-group")?.querySelector("h4")?.textContent));
3152    summary.replaceChildren();
3153    list.replaceChildren();
3154    if (!discovery) return;
3155    const pages = Array.isArray(discovery.pages) ? discovery.pages : [];
3156    [
3157      `${plural(discovery.candidatePagesFound, "page")} discovered`,
3158      `${finiteCount(discovery.pagesScanned)} scanned · ${finiteCount(discovery.pagesRemaining)} waiting`,
3159      `${plural(finiteCount(discovery.pagesWithProducts), "page")} produced products`,
3160    ].forEach((text) => {
3161      const line = document.createElement("p");
3162      line.textContent = text;
3163      summary.append(line);
3164    });
3165    const detailedPages = pages.filter((page) => ["scanned", "queued", "excluded", "failed"].includes(page.status));
3166    if (!detailedPages.length) {
3167      const fallback = document.createElement("p");
3168      fallback.className = "status";
3169      fallback.textContent = "Detailed catalog routes are not available for this saved run.";
3170      list.append(fallback);
3171      return;
3172    }
3173    const submitted = pages.find((page) => page.isSubmitted);
3174    const problems = pages.filter((page) => page.status !== "failed" && !page.isSubmitted && isProblemDiscoveryPage(page));
3175    const scanned = pages.filter((page) => page.status === "scanned" && !page.isSubmitted && !isProblemDiscoveryPage(page));
3176    const queued = pages.filter((page) => page.status === "queued" && !page.isSubmitted
3177      && !(discovery.pagesRemaining === 0 && String(page.pageType || "").toLowerCase() === "product_detail"));
3178    const failed = pages.filter((page) => page.status === "failed" && !page.isSubmitted);
3179    if (submitted) appendCatalogGroup(list, "Starting page", [submitted], submitted.status === "failed" ? "failed" : submitted.status === "queued" ? "queued" : "scanned", submitted.status === "failed" ? "×" : submitted.status === "queued" ? "○" : "✓");
3180    appendCatalogGroup(list, "Scanned", scanned, "scanned", "✓");
3181    appendCatalogGroup(list, "Waiting to scan", queued, "queued", "○", 5, openGroups.has("Waiting to scan"));
3182    appendCatalogGroup(list, "Needs review", problems, "review", "!");
3183    appendCatalogGroup(list, "Failed", failed, "failed", "×");
3184    const note = document.createElement("p");
3185    note.className = "status mvp-catalog-complete";
3186    if (discovery.candidatePagesFound === 1 && discovery.pagesScanned === 1) {
3187      note.textContent = "No additional catalog pages were discovered.";
3188      list.append(note);
3189    } else if (discovery.pagesRemaining === 0) {
3190      note.textContent = "All discovered catalog pages have been scanned.";
3191      list.append(note);
3192    }
3193  };
3194
3195  const renderDiscoveryCoverage = (discovery) => {
3196    if (!discoveryCoveragePanel) return 0;
3197    renderIncompleteScanWarning(null);
3198    if (discovery?.enabled !== true) {
3199      lastDiscoveryModel = null;
3200      renderCatalogPotential(null);
3201      discoveryCoveragePanel.hidden = true;
3202      if (continueScanButton) {
3203        continueScanButton.hidden = true;
3204        continueScanButton.disabled = false;
3205        continueScanButton.textContent = "";
3206      }
3207      if (stopDiscoveryButton) stopDiscoveryButton.hidden = true;
3208      setText("mvpDiscoverySubmittedUrl", "—");
3209      setText("mvpDiscoverySourcePageType", "—");
3210      setText("mvpDiscoveryCandidatePagesFound", "0");
3211      setText("mvpDiscoveryPagesAttempted", "0");
3212      setText("mvpDiscoveryPagesRemaining", "0");
3213      setText("mvpDiscoveryRoutesExtracted", "0");
3214      setText("mvpDiscoveryFailedOrBlockedPages", "0");
3215      setText("mvpDiscoveryStoppedReason", "—");
3216      setText("mvpDiscoveryNote", "—");
3217      setText("mvpScanProgressSummary", "");
3218      setText("mvpScanProgressSubcopy", "");
3219      setText("mvpMetricPagesRemaining", "0");
3220      renderDiscoveryMap(null);
3221      renderPrimaryCatalogMap(null);
3222      const completeMessage = E("mvpDiscoveryCompleteMessage");
3223      if (completeMessage) completeMessage.hidden = true;
3224      if (continueScanStatus) {
3225        continueScanStatus.hidden = true;
3226        continueScanStatus.textContent = "";
3227      }
3228      renderRecommendedActions();
3229      return 0;
3230    }
3231    const pagesRemaining = discovery.pagesRemaining;
3232    lastDiscoveryModel = discovery;
3233    setText("mvpDiscoverySubmittedUrl", textOrDash(discovery.submittedUrl));
3234    setText("mvpDiscoverySourcePageType", discoveryLabel(discovery.sourcePageType));
3235    setText("mvpDiscoveryCandidatePagesFound", numberText(discovery.candidatePagesFound));
3236    setText("mvpDiscoveryPagesAttempted", numberText(discovery.pagesScanned));
3237    setText("mvpDiscoveryPagesRemaining", numberText(discovery.pagesRemaining));
3238    setText("mvpMetricPagesRemaining", numberText(discovery.pagesRemaining));
3239    const pagesProducingProducts = finiteCount(discovery.pagesWithProducts);
3240    setText("mvpDiscoveryRoutesExtracted", numberText(pagesProducingProducts));
3241    setText("mvpDiscoveryFailedOrBlockedPages", numberText(discovery.failedOrBlockedPages.length));
3242    setText("mvpDiscoveryStoppedReason", discoveryStoppedReasonLabel(discovery.stoppedReason));
3243    setText(
3244      "mvpDiscoveryNote",
3245      pagesRemaining > 0
3246        ? "The customer-facing action appears under Next step."
3247        : "All discovered catalog pages have been scanned.",
3248    );
3249    renderDiscoveryMap(discovery, { problemOnly: true });
3250    renderPrimaryCatalogMap(discovery);
3251    setText(
3252      "mvpScanProgressSummary",
3253      pagesRemaining === 0
3254        ? `${discovery.productsFound} products collected · ${discovery.candidatePagesFound} routes evaluated · ${discovery.routesExtracted.length} catalog pages extracted`
3255        : `${discovery.productsFound} products collected so far · ${discovery.pagesScanned} pages scanned · ${pagesRemaining} pages waiting.`,
3256    );
3257    const discoveredButNotExtractedProductPages = discovery.pages.some((page) => {
3258      if (String(page?.pageType || "").toLowerCase() !== "product_detail") return false;
3259      const url = safeDiscoveryPageUrl(page?.url);
3260      return Boolean(url) && !discovery.routesExtracted.some((route) => safeDiscoveryPageUrl(route) === url);
3261    });
3262    setText(
3263      "mvpScanProgressSubcopy",
3264      pagesRemaining > 0 ? `${plural(pagesRemaining, "catalog page")} ${pagesRemaining === 1 ? "remains" : "remain"}. Scan them to find more products.` : discoveredButNotExtractedProductPages ? "Product pages were discovered but were not individually extracted in this scan." : "All discovered catalog pages have been scanned.",
3265    );
3266    const canContinueScan = entitlementState().canContinueScan;
3267    if (continueScanButton) continueScanButton.hidden = pagesRemaining <= 0 || !canContinueScan;
3268    if (stopDiscoveryButton) stopDiscoveryButton.hidden = pagesRemaining <= 0 || !canContinueScan;
3269    const completeMessage = E("mvpDiscoveryCompleteMessage");
3270    if (completeMessage) completeMessage.hidden = pagesRemaining > 0;
3271    if (continueScanStatus) {
3272      continueScanStatus.hidden = true;
3273      continueScanStatus.textContent = "";
3274    }
3275    discoveryCoveragePanel.hidden = false;
3276    return pagesRemaining;
3277  };
3278
3279  const catalogPotentialFromResponse = (data) => data?.catalogPotential || data?.customerSummary?.catalog_potential || null;
3280
3281  const extractionCoverageDescription = ({ pagesWithProducts, attemptedPages, scannedWithoutProducts, failedPages }) => {
3282    const sentences = [`Products were found on ${pagesWithProducts} of ${attemptedPages} attempted catalog ${attemptedPages === 1 ? "page" : "pages"}.`];
3283    if (scannedWithoutProducts === 1) sentences.push("Review the page that returned no products.");
3284    if (scannedWithoutProducts > 1) sentences.push(`Review the ${scannedWithoutProducts} pages that returned no products.`);
3285    if (failedPages > 0) sentences.push(`${plural(failedPages, "page")} failed or could not be accessed.`);
3286
3287    return sentences.join(" ");
3288  };
3289
3290  const supplierWideExpansionHost = (discovery) => {
3291    const submitted = toSafeHttpUrl(discovery?.submittedUrl);
3292    if (!submitted || !Array.isArray(discovery?.pages)) return "";
3293    try {
3294      const submittedUrl = new URL(submitted);
3295      const submittedPath = submittedUrl.pathname.replace(/\/+$/, "") || "/";
3296      if (submittedPath === "/") return "";
3297      const extendsSubmittedRoute = discovery.pages.some((page) => {
3298        if (page?.status !== "queued") return false;
3299        const candidate = toSafeHttpUrl(page.url, submittedUrl.href);
3300        if (!candidate) return false;
3301        const candidateUrl = new URL(candidate);
3302        const candidatePath = candidateUrl.pathname.replace(/\/+$/, "") || "/";
3303        return candidateUrl.host === submittedUrl.host
3304          && candidatePath !== submittedPath
3305          && !candidatePath.startsWith(`${submittedPath}/`);
3306      });
3307      return extendsSubmittedRoute ? submittedUrl.host : "";
3308    } catch {
3309      return "";
3310    }
3311  };
3312
3313  const renderCatalogPotential = (potential, discovery = null, viewModel = null) => {
3314    const panel = E("mvpCatalogPotential");
3315    if (!panel) return;
3316    if (!potential || typeof potential !== "object") {
3317      panel.hidden = true;
3318      const expansion = E("mvpCatalogScopeExpansion");
3319      if (expansion) expansion.hidden = true;
3320      return;
3321    }
3322    const structure = potential.catalogStructure || {};
3323    const extraction = potential.extractionCoverage || null;
3324    const coverage = potential.scanCoverage || {};
3325    const structureStatus = String(structure.status || "limited").toLowerCase();
3326    const coverageStatus = String(coverage.status || "limited").toLowerCase();
3327    const submittedPageType = String(structure.submittedPageType || "");
3328    const structureDescription = structureStatus === "clear"
3329      ? submittedPageType === "product_detail"
3330        ? "Clear product-based catalog structure."
3331        : "Clear collection-based catalog structure."
3332      : structureStatus === "mixed"
3333        ? "Mixed catalog structure: some recognizable catalog routes were found."
3334        : "Limited catalog structure: few recognizable catalog routes were found.";
3335    setText(
3336      "mvpCatalogPotentialStructure",
3337      structureDescription,
3338    );
3339    const remainingPages = viewModel ? viewModel.pagesRemaining : finiteCount(coverage.pagesRemaining);
3340    const coverageFailedPages = viewModel ? viewModel.pagesFailedOrBlocked : finiteCount(coverage.failedOrBlockedPages);
3341    const scanDescription = coverageStatus === "complete"
3342      ? "Complete scan: no discovered catalog pages remain."
3343      : coverageStatus === "partial"
3344        ? remainingPages > 0
3345          ? `Partial scan: ${remainingPages} discovered ${remainingPages === 1 ? "page remains" : "pages remain"}.`
3346          : `Partial scan: ${coverageFailedPages} ${coverageFailedPages === 1 ? "page failed or was inaccessible" : "pages failed or were inaccessible"}.`
3347        : `Limited scan: ${viewModel ? viewModel.pagesScanned : finiteCount(coverage.pagesScanned)} catalog ${(viewModel ? viewModel.pagesScanned : finiteCount(coverage.pagesScanned)) === 1 ? "page was" : "pages were"} scanned.`;
3348    setText(
3349      "mvpCatalogPotentialCoverage",
3350      scanDescription,
3351    );
3352    const optionalCount = (value) => value !== null && value !== undefined && Number.isFinite(Number(value))
3353      ? finiteCount(value)
3354      : null;
3355    const pagesWithProducts = extraction
3356      ? finiteCount(extraction.pagesWithProducts)
3357      : viewModel ? viewModel.pagesProducingProducts
3358      : optionalCount(collected.pagesWithProducts);
3359    const attemptedPages = extraction
3360      ? finiteCount(extraction.pagesScanned)
3361      : viewModel ? viewModel.pagesScanned
3362      : optionalCount(collected.pagesScanned);
3363    const scannedWithoutProducts = viewModel
3364      ? (Array.isArray(discovery?.pages) ? discovery.pages : []).filter((page) => page.status === "scanned" && finiteCount(page.productsFound) === 0 && isProblemDiscoveryPage(page)).length
3365      : extraction
3366      ? finiteCount(extraction.scannedPagesWithoutProducts)
3367      : finiteCount(structure.scannedPagesWithoutProducts);
3368    const failedPages = viewModel ? viewModel.pagesFailedOrBlocked : extraction
3369      ? finiteCount(extraction.failedOrBlockedPages)
3370      : finiteCount(coverage.failedOrBlockedPages ?? structure.failedOrBlockedPages);
3371    setText(
3372      "mvpCatalogPotentialData",
3373      extraction
3374        ? extractionCoverageDescription({ pagesWithProducts, attemptedPages, scannedWithoutProducts, failedPages })
3375        : "Product extraction coverage is not available for this saved run.",
3376    );
3377    const readiness = potential.commerceReadiness || potential.commerce_readiness || null;
3378    const readinessValues = [
3379      readiness?.catalogDiscoverability ?? readiness?.catalog_discoverability,
3380      readiness?.productStructuredData ?? readiness?.product_structured_data,
3381      readiness?.seoFoundation ?? readiness?.seo_foundation,
3382      readiness?.status,
3383    ].map((value) => String(value || "").trim().toLowerCase());
3384    if (readinessValues.length && readinessValues.every((value) => !value || value === "not_assessed" || value === "not assessed")) {
3385      panel.hidden = true;
3386      return;
3387    }
3388    setText("mvpCommerceCatalogDiscoverability", readinessDisplayLabel(readiness?.catalogDiscoverability || readiness?.catalog_discoverability));
3389    setText("mvpCommerceProductStructuredData", readinessDisplayLabel(readiness?.productStructuredData || readiness?.product_structured_data));
3390    setText("mvpCommerceSeoFoundation", readinessDisplayLabel(readiness?.seoFoundation || readiness?.seo_foundation));
3391    setText("mvpCommerceOverallReadiness", readinessDisplayLabel(readiness?.status));
3392    const readinessNote = E("mvpCommerceReadinessNote");
3393    if (readinessNote) {
3394      readinessNote.textContent = readiness?.primaryObservation || readiness?.primary_observation || readiness?.note || "";
3395      readinessNote.hidden = !readinessNote.textContent;
3396    }
3397    const evidence = readiness?.evidenceScope || readiness?.evidence_scope || null;
3398    const signals = readiness?.signals || null;
3399    const evidencePanel = E("mvpCommerceEvidence");
3400    if (evidencePanel) evidencePanel.hidden = !evidence && !signals;
3401    if (evidence || signals) {
3402      setText("mvpCommerceEvidenceScope", String(readiness?.scope || "").replaceAll("_", " ") || "—");
3403      setText("mvpCommercePdpPagesAttempted", finiteCount(evidence?.productDetailPagesAttempted ?? evidence?.product_detail_pages_attempted));
3404      setText("mvpCommercePdpPagesSampled", finiteCount(evidence?.sampledProductDetailPages ?? evidence?.sampled_product_detail_pages));
3405      setText("mvpCommerceJsonLdBlocks", finiteCount(signals?.jsonLdBlocks ?? signals?.json_ld_blocks));
3406      const structuredFields = Array.isArray(signals?.structuredFields)
3407        ? signals.structuredFields
3408        : Array.isArray(signals?.structured_fields) ? signals.structured_fields : [];
3409      setText("mvpCommerceStructuredFields", structuredFields.length ? structuredFields.join(", ") : "—");
3410      const sampledUrls = Array.isArray(evidence?.sampledProductDetailPageUrls)
3411        ? evidence.sampledProductDetailPageUrls
3412        : Array.isArray(evidence?.sampled_product_detail_page_urls) ? evidence.sampled_product_detail_page_urls : [];
3413      const safeSampledUrls = sampledUrls.map((url) =>
3413 toSafeHttpUrl(url)).filter(Boolean);
3414      const sampledUrlsPanel = E("mvpCommerceSampledUrls");
3415      if (sampledUrlsPanel) {
3416        sampledUrlsPanel.replaceChildren();
3417        if (safeSampledUrls.length) {
3418          const label = document.createElement("strong");
3419          label.textContent = t("mvp.technical.sampledUrls", null, "Sampled product-detail URLs:");
3420          const list = document.createElement("ul");
3421          safeSampledUrls.forEach((url) => {
3422            const item = document.createElement("li");
3423            const link = document.createElement("a");
3424            link.href = url;
3425            link.target = "_blank";
3426            link.rel = "noopener";
3427            link.textContent = url;
3428            item.append(link);
3429            list.append(item);
3430          });
3431          sampledUrlsPanel.append(label, list);
3432        }
3433        sampledUrlsPanel.hidden = safeSampledUrls.length <= 0;
3434      }
3435    }
3436
3437    const expansion = E("mvpCatalogScopeExpansion");
3438    if (expansion) {
3439      const expansionHost = supplierWideExpansionHost(discovery);
3440      expansion.textContent = expansionHost
3441        ? `ScrapingSmart expanded beyond the submitted collection or category to other catalog sections on ${expansionHost}.`
3442        : "";
3443      expansion.hidden = !expansion.textContent;
3444    }
3445
3446    panel.hidden = false;
3447  };
3448
3449  const setResultWorkflowStage = (state, { stopDiscovery = false } = {}) => {
3450    if (!state) return;
3451    resultWorkflowState = { ...state, stopDiscovery };
3452    const discoveryInProgress = state.discovery?.enabled === true
3453      && state.discovery.pagesRemaining > 0
3454      && !stopDiscovery;
3455    if (stateTitle) stateTitle.textContent = state.hasSuccessfulExport ? "Your dataset is ready" : state.finalTitle;
3456    if (resultStatus) {
3457      resultStatus.textContent = discoveryInProgress ? "More products available" : state.finalStatus;
3458      resultStatus.dataset.state = discoveryInProgress ? "scanning" : state.finalStatusState;
3459    }
3460    if (resultBanner && !discoveryInProgress && !state.hasSuccessfulExport) {
3461      resultBanner.textContent = state.finalBanner;
3462      resultBanner.hidden = false;
3463    }
3464    if (resultSource) {
3465      setResultSourceLabel(state.requestMeta);
3466    }
3467    if (stateMessage) {
3468      stateMessage.textContent = discoveryInProgress ? "" : state.finalMessage;
3469      stateMessage.hidden = discoveryInProgress || !state.finalMessage;
3470    }
3471    const paidContinuationAvailable = entitlementState().canContinueScan;
3472    if (continueScanButton) continueScanButton.hidden = state.automaticFullCatalog || !discoveryInProgress || !paidContinuationAvailable;
3473    if (stopDiscoveryButton) stopDiscoveryButton.hidden = true;
3474    if (datasetActions) datasetActions.hidden = stopDiscovery || !state.hasSuccessfulExport || continuationScanActive || !paidContinuationAvailable;
3475
3476    if (downloadsTitle) downloadsTitle.hidden = false;
3477    if (currentDraftExport) {
3478      currentDraftExport.classList.toggle("mvp-current-draft-export--main", stopDiscovery);
3479    }
3480    if (recommendedAction) recommendedAction.hidden = !state.hasSuccessfulExport;
3481    if (cleanupPanel) cleanupPanel.hidden = lastMvpResultViewModel?.productsCollected <= 0;
3482    if (previewDetails) {
3483      previewDetails.hidden = !state.hasPreview;
3484      previewDetails.classList.remove("mvp-preview-details--discovery");
3485    }
3486    if (discoveryPreviewSlot) discoveryPreviewSlot.hidden = true;
3487    if (reviewPreviewSlot) reviewPreviewSlot.hidden = !state.hasPreview;
3488    const reviewDetails = E("mvpReviewDetails");
3489    if (reviewDetails) reviewDetails.hidden = !state.showReview;
3490    const progressSummary = E("mvpScanProgressSummary");
3491    const progressSubcopy = E("mvpScanProgressSubcopy");
3492    if (progressSummary) progressSummary.hidden = discoveryInProgress;
3493    if (progressSubcopy) progressSubcopy.hidden = discoveryInProgress;
3494  };
3495
3496  const resultIssueCounts = (data) => data?.summary?.issueCounts || {};
3497
3498  const resultContentMetrics = (data) => {
3499    const issueCounts = resultIssueCounts(data);
3500    const missingImages = Number(issueCounts?.missing_image ?? 0);
3501    const weakDescriptions = Number(
3502      issueCounts?.weak_description
3503      ?? issueCounts?.missing_description
3504      ?? 0
3505    );
3506    return {
3507      missingImages: Number.isFinite(missingImages) ? Math.max(0, missingImages) : 0,
3508      weakDescriptions: Number.isFinite(weakDescriptions) ? Math.max(0, weakDescriptions) : 0,
3509    };
3510  };
3511
3512  const setEnrichmentState = (state, message) => {
3513    const normalized = ["queued", "starting", "processing", "completed", "partial", "failed"].includes(state)
3514      ? state
3515      : "processing";
3516    if (enrichmentStatusPanel) enrichmentStatusPanel.hidden = false;
3517    if (enrichmentStatusState) {
3518      enrichmentStatusState.textContent = normalized.charAt(0).toUpperCase() + normalized.slice(1);
3519      enrichmentStatusState.dataset.state = normalized;
3520    }
3521    if (enrichmentProgress) enrichmentProgress.hidden = ["completed", "partial", "failed"].includes(normalized);
3522    if (enrichmentStatus) enrichmentStatus.textContent = message;
3523  };
3524
3525  const enrichmentFailureSummary = (enrichment) => {
3526    const failures = Array.isArray(enrichment?.failures) ? enrichment.failures : [];
3527    if (!failures.length) return "";
3528    const counts = failures.reduce((out, failure) => {
3529      const code = String(failure?.reasonCode || "unknown");
3530      out[code] = (out[code] || 0) + 1;
3531      return out;
3532    }, {});
3533    const labels = {
3534      blocked: "blocked",
3535      http_error: "returned HTTP errors",
3536      fetch_timeout: "timed out",
3537      non_html_response: "returned non-HTML responses",
3538      no_product_data: "returned no product data",
3539      invalid_url: "had invalid URLs",
3540      unknown: "failed for unknown reasons",
3541    };
3542    const parts = Object.entries(counts).map(([code, count]) => `${count} ${labels[code] || "failed"}`);
3543    return parts.length ? `${Number(enrichment.productsFailed || failures.length)} failed: ${parts.join(", ")}.` : "";
3544  };
3545
3546  /**
3547   * @typedef {Object} MarketingContentProductContext
3548   * @property {string} name
3549   * @property {string=} description
3550   * @property {string=} price
3551   * @property {string=} sku
3552   * @property {string=} category
3553   * @property {string=} brand
3554   * @property {string=} productUrl
3555   * @property {string[]} images
3556   *
3557   * @typedef {Object} MarketingContentRequest
3558   * @property {"template"|"ai"} creationType
3559   * @property {MarketingContentProductContext} product
3560   * @property {Object} creativeOverrides
3561   * @property {string} selectedImage
3562   * @property {string} purpose
3563   * @property {{preset: string, width: number, height: number, aspectRatio: string}} output
3564   * @property {string} instruction
3565   * @property {string=} template
3566   */
3567
3568  const marketingHttpUrl = (value) => {
3569    const raw = String(value || "").trim();
3570    if (!raw) return "";
3571    try {
3572      const url = new URL(raw);
3573      return url.protocol === "http:" || url.protocol === "https:" ? url.href : "";
3574    } catch {
3575      return "";
3576    }
3577  };
3578
3579  const marketingImagesFor = (product) => {
3580    const images = [];
3581    const seen = new Set();
3582    const add = (value) => {
3583      if (Array.isArray(value)) {
3584        value.forEach(add);
3585        return;
3586      }
3587      const image = marketingHttpUrl(value);
3588      if (!image || seen.has(image)) return;
3589      seen.add(image);
3590      images.push(image);
3591    };
3592    add(product?.image);
3593    add(product?.images);
3594    add(product?.imageUrl);
3595    add(product?.image_url);
3596    add(product?.imageUrls);
3597    add(product?.image_urls);
3598    if (Array.isArray(product?.variants)) product.variants.forEac
3598h((variant) => add(variant?.image));
3599    return images;
3600  };
3601
3602  const marketingText = (value) => String(value ?? "").trim();
3603
3604  const formatDetectedPrice = (product) => {
3605    const raw = marketingText(product?.price);
3606    if (!raw) return "";
3607    const currency = marketingText(product?.currency ?? product?.priceCurrency ?? product?.price_currency).toUpperCase();
3608    if (!currency || /[A-Za-z$€£¥]/.test(raw)) return raw;
3609    const numeric = Number(raw.replace(/[^0-9,.-]/g, "").replace(/,(?=\d{1,2}$)/, ".").replace(/,/g, ""));
3610    if (!Number.isFinite(numeric)) return `${currency} ${raw}`;
3611    return `${currency} ${new Intl.NumberFormat(undefined, { maximumFractionDigits: Number.isInteger(numeric) ? 0 : 2 }).format(numeric)}`;
3612  };
3613
3614  const normalizeMarketingProductContext = (product) => {
3615    const context = {
3616      name: marketingText(product?.name ?? product?.title),
3617      description: marketingText(product?.description),
3618      price: formatDetectedPrice(product),
3619      sku: marketingText(product?.sku),
3620      category: marketingText(product?.category),
3621      brand: marketingText(product?.brand),
3622      productUrl: marketingHttpUrl(product?.url ?? product?.productUrl ?? product?.product_url),
3623      images: marketingImagesFor(product),
3624    };
3625    for (const key of ["description", "price", "sku", "category", "brand", "productUrl"]) {
3626      if (!context[key]) delete context[key];
3627    }
3628    return context;
3629  };
3630
3631  const productOptionLabel = (product) => {
3632    const context = normalizeMarketingProductContext(product);
3633    if (context.name && context.sku) return `${context.name} — ${context.sku}`;
3634    return context.name || context.sku || context.productUrl || t("mvp.marketing.unnamedProduct", null, "Unnamed product");
3635  };
3636
3637  const productStableIdentifier = (product, index) => {
3638    const context = normalizeMarketingProductContext(product);
3639    return marketingText(product?.id ?? product?.productId ?? product?.product_id)
3640      || context.sku
3641      || context.productUrl
3642      || `product-${index + 1}`;
3643  };
3644
3645  const hasMarketingContext = (context) => Boolean(
3646    context?.name
3647    || context?.description
3648    || context?.price
3649    || context?.sku
3650    || context?.category
3651    || context?.brand
3652    || context?.productUrl
3653    || context?.images?.length
3654  );
3655
3656  const setMarketingStatus = (message, state = "") => {
3657    if (!marketingContentStatus) return;
3658    marketingContentStatus.textContent = message;
3659    if (state) marketingContentStatus.dataset.state = state;
3660    else delete marketingContentStatus.dataset.state;
3661  };
3662
3663  const clearPreparedMarketingContent = () => {
3664    marketingContentState.preparedRequest = null;
3665    if (window.__MVP_TEST_HOOKS__ && typeof window.__MVP_TEST_HOOKS__ === "object") {
3666      window.__MVP_TEST_HOOKS__.marketingContentRequest = null;
3667    }
3668    if (marketingTemplatePreview) {
3669      marketingTemplatePreview.hidden = true;
3670      marketingTemplatePreview.textContent = "";
3671    }
3672    if (marketingDownload) {
3673      marketingDownload.hidden = false;
3674      marketingDownload.removeAttribute("href");
3675    }
3676    setMarketingStatus("");
3677  };
3678
3679  const creativeOverridesFor = (context, product = {}) => ({
3680    name: context.name || "",
3681    price: context.price || "",
3682    brand: context.brand || "",
3683    sku: context.sku || "",
3684    description: context.description || "",
3685    headline: marketingText(product?.headline) || context.name || "",
3686    cta: marketingText(product?.cta),
3687  });
3688
3689  const readCreativeOverrides = () => ({
3690    name: marketingText(marketingName?.value),
3691    price: marketingText(marketingPrice?.value),
3692    brand: marketingText(marketingBrand?.value),
3693    sku: marketingText(marketingSku?.value),
3694    description: marketingText(marketingDescription?.value),
3695    headline: marketingText(marketingHeadline?.value),
3696    cta: marketingText(marketingCta?.value),
3697  });
3698
3699  const writeCreativeOverrides = (overrides) => {
3700    const fields = [
3701      [marketingName, "name"],
3702      [marketingPrice, "price"],
3703      [marketingBrand, "brand"],
3704      [marketingSku, "sku"],
3705      [marketingDescription, "description"],
3706      [marketingHeadline, "headline"],
3707      [marketingCta, "cta"],
3708    ];
3709    fields.forEach(([field, key]) => {
3710      if (field) field.value = overrides?.[key] || "";
3711    });
3712  };
3713
3714  const renderSelectedMarketingProduct = () => {
3715    const product = marketingContentState.products[marketingContentState.selectedIndex];
3716    const context = normalizeMarketingProductContext(product || {});
3717    marketingContentState.creativeOverrides = creativeOverridesFor(context, product);
3718    marketingContentState.selectedImage = context.images[0] || "";
3719    writeCreativeOverrides(marketingContentState.creativeOverrides);
3720    if (marketingImages) {
3721      marketingImages.textContent = "";
3722      context.images.forEach((source, index) => {
3723        const label = document.createElement("label");
3724        label.className = "mvp-marketing-image-option";
3725        const image = document.createElement("img");
3726        image.src = source;
3727        image.alt = context.name
3728          ? t("mvp.marketing.productImageAlt", { name: context.name }, `Product image for ${context.name}`)
3729          : t("mvp.marketing.productImage", null, "Product image");
3730        image.loading = "lazy";
3731        if (context.images.length > 1) {
3732          const radio = document.createElement("input");
3733          radio.type = "radio";
3734          radio.name = "mvpMarketingImage";
3735          radio.value = source;
3736          radio.checked = source === marketingContentState.selectedImage;
3737          const selection = document.createElement("span");
3738          selection.textContent = t("mvp.marketing.imageNumber", { number: index + 1 }, `Image ${index + 1}`);
3739          label.append(radio, image, selection);
3740        } else {
3741          label.append(image);
3742        }
3743        marketingImages.appendChild(label);
3744      });
3745      if (!context.images.length) {
3746        const empty = document.createElement("p");
3747        empty.className = "status";
3748        empty.textContent = t("mvp.marketing.noImages", null, "No product images were collected.");
3749        marketingImages.appendChild(empty);
3750      }
3751    }
3752    clearPreparedMarketingContent();
3753  };
3754
3755  const renderMarketingProductSelector = () => {
3756    if (!marketingProductSelect) return;
3757    marketingProductSelect.textContent = "";
3758    marketingContentState.products.forEach((product, index) => {
3759      const option = document.createElement("option");
3760      option.value = String(index);
3761      option.textContent = productOptionLabel(product);
3762      marketingProductSelect.appendChild(option);
3763    });
3764    marketingContentState.selectedIndex = Math.min(
3765      marketingContentState.selectedIndex,
3766      Math.max(0, marketingContentState.products.length - 1),
3767    );
3768    marketingProductSelect.value = String(marketingContentState.selectedIndex);
3769    if (marketingCustomize && "open" in marketingCustomize) marketingCustomize.open = false;
3770    renderSelectedMarketingProduct();
3771  };
3772
3773  const marketingCanvasText = (value, fallback = "") => marketingText(value) || fallback;
3774
3775  const drawMarketingStory = async (request) => {
3776    if (!marketingTemplatePreview) return;
3777    marketingTemplatePreview.textContent = "";
3778    marketingTemplatePreview.hidden = false;
3779    const canvas = document.createElement("canvas");
3780    canvas.className = "mvp-marketing-story-canvas";
3781    canvas.width = 1080;
3782    canvas.height = 1920;
3783    marketingTemplatePreview.appendChild(canvas);
3784    let context;
3785    try { context = canvas.getContext("2d"); } catch { context = null; }
3786    if (!context) return;
3787
3788    const gradient = context.createLinearGradient(0, 0, 1080, 1920);
3789    gradient.addColorStop(0, "#f8f4ec");
3790    gradient.addColorStop(1, "#dce8e5");
3791    context.fillStyle = gradient;
3792    context.fillRect(0, 0, 1080, 1920);
3793    context.fillStyle = "#172033";
3794    context.fillRect(0, 0, 1080, 18);
3795
3796    if (request.selectedImage && typeof window.Image === "function") {
3797      await new Promise((resolve) => {
3798        const image = new window.Image();
3799        image.crossOrigin = "anonymous";
3800        image.onload = () => {
3801          const area = { x: 72, y: 180, width: 936, height: 1010 };
3802          const scale = Math.min(area.width / image.naturalWidth, area.height / image.naturalHeight);
3803          const width = image.naturalWidth * scale;
3804          const height = image.naturalHeight * scale;
3805          context.drawImage(image, area.x + (area.width - width) / 2, area.y + (area.height - height) / 2, width, height);
3806          resolve();
3807        };
3808        image.onerror = () => resolve();
3809        image.src = request.selectedImage;
3810      });
3811    }
3812
3813    const wrap = (value, maxChars) => {
3814      const text = marketingCanvasText(value);
3815      return text.length > maxChars ? `${text.slice(0, maxChars - 1).trimEnd()}…` : text;
3816    };
3817    const copy = request.creativeOverrides;
3818    context.fillStyle = "#172033";
3819    context.textBaseline = "top";
3820    if (copy.brand) { context.font = "700 28px Arial"; context.fillText(wrap(copy.brand, 30).toUpperCase(), 84, 1260); }
3821    context.font = "900 74px Arial";
3822    context.fillText(wrap(copy.headline || copy.name, 28), 84, 1320, 912);
3823    context.font = "500 34px Arial";
3824    context.fillText(wrap(copy.name, 42), 84, 1430, 912);
3825    if (copy.description) { context.font = "400 28px Arial"; context.fillText(wrap(copy.description, 52), 84, 1492, 912); }
3826    if (copy.price) { context.fillStyle = "#b0441e"; context.font = "900 58px Arial"; context.fillText(wrap(copy.price, 24), 84, 1580, 912); }
3827    context.fillStyle = "#fff";
3828    context.fillRect(84, 1710, 300, 76);
3829    context.fillStyle = "#172033";
3830    context.font = "700 28px Arial";
3831    context.fillText(wrap(copy.cta || "Shop now", 24), 116, 1732, 236);
3832    if (marketingDownload && authenticatedActionAllowed()) {
3833      try {
3834        marketingDownload.href = canvas.toDataURL("image/png");
3835        marketingDownload.hidden = false;
3836        renderActionAccessGates();
3837      } catch { setMarketingStatus("Story preview created, but this image cannot be exported by the browser.", "error"); }
3838    }
3839  };
3840
3841  const renderMarketingLabels = () => {
3842    setText("mvpMarketingContentCopy", "Create an Instagram Story from a product in this dataset.");
3843    setText("mvpMarketingProductSelectLabel", t("mvp.marketing.chooseProduct", null, "Choose a product"));
3844    setText("mvpMarketingPriceLabel", t("mvp.marketing.price", null, "Price"));
3845    setText("mvpMarketingBrandLabel", t("mvp.marketing.brand", null, "Brand"));
3846    setText("mvpMarketingDescriptionLabel", t("mvp.marketing.description", null, "Description"));
3847    setText("mvpMarketingHeadlineLabel", t("mvp.marketing.headline", null, "Headline"));
3848    setText("mvpMarketingCtaLabel", t("mvp.marketing.cta", null, "CTA"));
3849    setText("mvpMarketingCustomizeSummary", t("mvp.marketing.customize", null, "Customize story"));
3850    setText("mvpPrepareMarketingRequest", t("mvp.marketing.createStory", null, "Create story"));
3851    setText("mvpMarketingDownload", t("mvp.marketing.downloadPng", null, "Download PNG"));
3852  };
3853
3854  function renderMarketingContentAuthState({ primary = marketingContentState.primary } = {}) {
3855    const signedIn = isSignedIn();
3856    if (prepareMarketingRequestButton) {
3857      prepareMarketingRequestButton.classList.toggle("button--primary", primary);
3858      prepareMarketingRequestButton.classList.toggle("secondary", !primary);
3859    }
3860    if (openMarketingContentButton) {
3861      openMarketingContentButton.hidden = false;
3862      openMarketingContentButton.disabled = !authenticatedActionAllowed();
3863      openMarketingContentButton.textContent = "Create marketing content";
3864      openMarketingContentButton.classList.toggle("button--primary", primary);
3865      openMarketingContentButton.classList.toggle("secondary", !primary);
3866    }
3867    renderActionAccessGates();
3868    if (marketingContentAuthSupport) marketingContentAuthSupport.hidden = true;
3869    if (marketingContentState.products.length && marketingContentWorkspace) {
3870      renderMarketingProductSelector();
3871      marketingContentWorkspace.hidden = false;
3872      openMarketingContentButton?.setAttribute("aria-expanded", "true");
3873      if (signedIn && !marketingContentState.productsComplete) void loadMarketingWorkspaceProducts();
3874    }
3875  }
3876
3877  const renderMarketingContentStep = (data, productsFound, { showAsNextAction = true, primary = false } = {}) => {
3878    const availableProducts = [
3879      ...(Array.isArray(data?.products) ? data.products : []),
3880      ...(Array.isArray(data?.preview) ? data.preview : []),
3881    ];
3882    const hasUsableProduct = availableProducts.some((product) => hasMarketingContext(normalizeMarketingProductContext(product)));
3883    const show = showAsNextAction
3884      && selectedMvpIntent === "extraction"
3885      && Number(productsFound || 0) > 0
3886      && hasUsableProduct
3887      && String(data?.status || "").toLowerCase() !== "failed";
3888    marketingContentState.primary = Boolean(primary);
3889    if (marketingContentStep) marketingContentStep.hidden = !show;
3890    if (!show) {
3891      if (marketingContentWorkspace) marketingContentWorkspace.hidden = true;
3892      openMarketingContentButton?.setAttribute("aria-expanded", "false");
3893      return;
3894    }
3895    setText("mvpMarketingContentTitle", "Create marketing content");
3896    if (marketingContentState.sourceData !== data) {
3897      const fullProducts = Array.isArray(data?.products) ? data.products : [];
3898      const preview = Array.isArray(data?.preview) ? data.preview : [];
3899      marketingContentState.sourceData = data;
3900      marketingContentState.productsComplete = fullProducts.length >= Number(productsFound || 0);
3901      marketingContentState.products = marketingContentState.productsComplete
3902        ? fullProducts
3903        : preview;
3904      marketingContentState.selectedIndex = 0;
3905      marketingContentState.creativeOverrides = null;
3906      marketingContentState.selectedImage = "";
3907      marketingContentState.preparedRequest = null;
3908      marketingContentState.productsLoadPromise = null;
3909      if (marketingContentWorkspace) marketingContentWorkspace.hidden = true;
3910      openMarketingContentButton?.setAttribute("aria-expanded", "false");
3911    }
3912    renderMarketingLabels();
3913    renderMarketingContentAuthState({ primary });
3914  };
3915
3916  const loadFullMarketingProducts = async (sourceData = marketingContentState.sourceData) => {
3917    if (marketingContentState.productsComplete) return true;
3918    const runId = marketingContentState.sourceData?.runId || lastMvpResult?.runId;
3919    if (!runId) return false;
3920    const response = await authenticatedFetch(`/api/mvp/runs/${encodeURIComponent(runId)}/result`, {
3921      credentials: "same-origin",
3922    }, { endpointType: "marketing_content_result", runId });
3923    const data = await parseResponsePayload(response);
3924    if (!response.ok || data?.ok === false || !Array.isArray(data?.products)) return false;
3925    if (marketingContentState.sourceData !== sourceData) return false;
3926    marketingContentState.products = data.products;
3927    marketingContentState.productsComplete = true;
3928    return true;
3929  };
3930
3931  const loadMarketingWorkspaceProducts = () => {
3932    if (marketingContentState.productsComplete || marketingContentState.productsLoadPromise) {
3933      return marketingContentState.productsLoadPromise;
3934    }
3935    const sourceData = marketingContentState.sourceData;
3936    if (!sourceData) return null;
3937    if (marketingProductSelect) marketingProductSelect.disabled = true;
3938    if (prepareMarketingRequestButton) prepareMarketingRequestButton.disabled = true;
3939    setMarketingStatus(t("mvp.marketing.loading", null, "Loading products…"));
3940    const loadPromise = loadFullMarketingProducts(sourceData)
3941      .then((loaded) => {
3942        if (marketingContentState.sourceData !== sourceData) return false;
3943        if (!loaded || !marketingContentState.products.length) {
3944          setMarketingStatus(t("mvp.marketing.loadFailed", null, "The full product collection could not be loaded. Try opening this dataset again from My runs."), "error");
3945          return false;
3946        }
3947        if (marketingContentState.selectedIndex >= marketingContentState.products.length) marketingContentState.selectedIndex = 0;
3948        renderMarketingProductSelector();
3949        setMarketingStatus("");
3950        return true;
3951      })
3952      .catch(() => {
3953        if (marketingContentState.sourceData === sourceData) {
3954          setMarketingStatus(t("mvp.marketing.loadFailed", null, "The full product collection could not be loaded. Try opening this dataset again from My runs."), "error");
3955        }
3956        return false;
3957      })
3958      .finally(() => {
3959        if (marketingContentState.sourceData !== sourceData) return;
3960        if (marketingProductSelect) marketingProductSelect.disabled = false;
3961        if (prepareMarketingRequestButton) prepareMarketingRequestButton.disabled = false;
3962        if (marketingContentState.productsLoadPromise === loadPromise) marketingContentState.productsLoadPromise = null;
3963      });
3964    marketingContentState.productsLoadPromise = loadPromise;
3965    return loadPromise;
3966  };
3967
3968  const renderRecommendedActions = ({ data = null, productsFound = 0, viewModel = null } = {}) => {
3969    const eligibility = data?.enrichmentEligibility || {};
3970    const hasEligibility = Number.isFinite(Number(eligibility.productsEligible));
3971    const eligible = viewModel ? viewModel.productsRemainingForEnrichment : Math.max(0, Number(eligibility.productsEligible || 0));
3972    const eligibleTotal = viewModel ? viewModel.productsEligibleForEnrichment : eligible;
3973    const checkedTotal = viewModel ? viewModel.productsChecked : 0;
3974    const improvedTotal = viewModel ? viewModel.productsImproved : 0;
3975    const ineligible = viewModel ? viewModel.productsIneligibleForEnrichment : Math.max(0, Number(productsFound || 0) - eligibleTotal);
3976    const credits = Math.max(0, Number(eligibility.creditsRequired || 0));
3977    const enrichment = data?.enrichment || data?.summary?.enrichment || data?.customerSummary?.enrichment;
3978    const authenticated = data?.auth?.authenticated === true
3979      || data?.usage?.authenticated === true
3980      || lastUsage?.authenticated === true
3981      || isSignedIn();
3982    const paidCredits = Math.max(0, Number(
3983      data?.usage?.paidCreditsRemaining
3984      ?? eligibility.paidCreditsRemaining
3985      ?? lastUsage?.paidCreditsRemaining
3986      ?? 0
3987    ));
3988    const canEnrich = isSignedIn() && paidCredits > 0 && paidCredits >= credits;
3989    const continuationPending = Number(viewModel?.pagesRemaining || 0) > 0;
3990    if (enrichment) enrichmentInProgress = false;
3991    const showEnrichment = Number(productsFound || 0) > 0 && (Boolean(enrichment) || hasEligibility);
3992    const enrichmentCompleted = Boolean(enrichment)
3993      && String(enrichment.status || "completed").toLowerCase() === "completed"
3994      && eligible <= 0
3995      && Number(enrichment.productsFailed || 0) <= 0
3996      && Number(enrichment.productsPartial || 0) <= 0;
3997    const enrichedCount = Math.max(0, Number(enrichment?.productsEnriched ?? checkedTotal ?? productsFound));
3998    const enrichmentHasWork = eligible > 0
3999      || Number(enrichment?.productsFailed || 0) > 0
4000      || Number(enrichment?.productsPartial || 0) > 0;
4001    const showEnrichmentStage = enrichmentCompleted || enrichmentHasWork || enrichmentInProgress;
4002    if (enrichmentAction) enrichmentAction.hidden = !showEnrichmentStage;
4003    if (enrichmentSummary) {
4004      enrichmentSummary.textContent = enrichmentCompleted
4005        ? t("mvp.enrichment.compactComplete", { count: enrichedCount }, `✓ ${enrichedCount} products enriched`)
4006        : "";
4007      enrichmentSummary.hidden = !enrichmentCompleted;
4008    }
4009    if (enrichProductsButton) {
4010      const retriesExist = eligible > 0 || Number(enrichment?.productsFailed || 0) > 0 || Number(enrichment?.productsPartial || 0) > 0;
4011      const showEnrichmentButton = showEnrichmentStage
4012        && !enrichmentCompleted
4013        && retriesExist
4014        && !enrichmentInProgress;
4015      enrichProductsButton.hidden = !showEnrichmentButton;
4016      enrichProductsButton.disabled = !canEnrich;
4017      const enrichmentIsPrimary = !continuationPending;
4018      enrichProductsButton.classList.toggle("button--primary", enrichmentIsPrimary);
4019      enrichProductsButton.classList.toggle("secondary", !enrichmentIsPrimary);
4020      enrichProductsButton.dataset.creditsRequired = String(credits);
4021      enrichProductsButton.dataset.requiresRuns = authenticated && !canEnrich ? "true" : "false";
4022      enrichProductsButton.textContent = authenticated && paidCredits < credits
4023        ? t("mvp.enrichment.requiresCredits", { cost: credits, balance: paidCredits }, `Requires ${credits} credits · You have ${paidCredits}`)
4024        : t("mvp.enrichment.exactCost", { count: eligible, cost: credits }, `Enrich ${eligible} products · ${credits} credits`);
4025    }
4026    const enrichmentGateCopy = E("mvpEnrichmentGateCopy");
4027    const enrichmentGateActions = E("mvpEnrichmentGateActions");
4028    const enrichmentGateAction = E("mvpCreateAccountEnrich");
4029    const enrichmentGateSignIn = E("mvpSignInEnrich");
4030    const enrichmentLocked = authenticated && showEnrichmentStage && !enrichmentCompleted && eligible > 0 && !canEnrich;
4031    if (enrichmentGateCopy) {
4032      enrichmentGateCopy.hidden = true;
4033      enrichmentGateCopy.textContent = "";
4034    }
4035    if (enrichmentGateActions) enrichmentGateActions.hidden = true;
4036    if (enrichmentGateAction) {
4037      enrichmentGateAction.hidden = !authenticated || !enrichmentLocked;
4038      enrichmentGateAction.textContent = t("mvp.runs.add", null, "Add credits");
4039      enrichmentGateAction.classList.toggle("button--primary", !continuationPending);
4040      enrichmentGateAction.classList.toggle("secondary", continuationPending);
4041    }
4042    if (enrichmentGateSignIn) {
4043      enrichmentGateSignIn.hidden = true;
4044      enrichmentGateSignIn.textContent = t("mvp.auth.signIn", null, "Sign in");
4045    }
4046    if (enrichment) {
4047      const attempted = Number(enrichment.productsAttempted ?? (Number(enrichment.productsEnriched || 0) + Number(enrichment.productsPartial || 0) + Number(enrichment.productsFailed || 0)));
4048      const checked = Number(enrichment.productsChecked ?? (Number(enrichment.productsEnriched || 0) + Number(enrichment.productsPartial || 0)));
4049      const improved = Number(enrichment.productsImproved ?? enrichment.productsEnriched ?? 0);
4050      const parsedNoChange = Number(enrichment.productsParsedNoChange || 0);
4051      const partial = Number(enrichment.productsPartial || 0);
4052      const failed = Number(enrichment.productsFailed || 0);
4053      const outcomes = `${checked} product page${checked === 1 ? "" : "s"} checked · ${improved} product${improved === 1 ? "" : "s"} improved.`
4054        + `${parsedNoChange ? ` ${parsedNoChange} had no useful new data.` : ""}`
4055        + `${partial ? ` ${partial} remain partial.` : ""}`
4056        + `${failed ? ` ${failed} failed out of ${attempted} attempted.` : ""}`;
4057      const failureSummary = enrichmentFailureSummary(enrichment);
4058      if (enrichmentCompleted) {
4059        if (enrichmentStatusPanel) enrichmentStatusPanel.hidden = true;
4060      } else {
4061        setEnrichmentState(
4062          ["completed", "partial", "failed"].includes(String(enrichment.status)) ? String(enrichment.status) : "completed",
4063          `${outcomes}${failureSummary ? ` ${failureSummary}` : ""}`
4064        );
4065      }
4066    } else if (!enrichmentInProgress && enrichmentStatusPanel) {
4067      enrichmentStatusPanel.hidden = true;
4068      if (enrichmentStatus) enrichmentStatus.textContent = "";
4069    }
4070    renderMarketingContentStep(data, productsFound, {
4071      showAsNextAction: true,
4072      primary: !continuationPending && !enrichmentHasWork,
4073    });
4074    renderActionAccessGates();
4075    return showEnrichment;
4076  };
4077
4078  openMarketingContentButton?.addEventListener("click", async () => {
4079    if (!isSignedIn()) {
4080      focusAccountFromGate("signin", "story");
4081      return;
4082    }
4083    const originalLabel = openMarketingContentButton.textContent;
4084    openMarketingContentButton.disabled = true;
4085    openMarketingContentButton.textContent = t("mvp.marketing.loading", null, "Loading products…");
4086    setMarketingStatus("");
4087    try {
4088      const loaded = await loadFullMarketingProducts();
4089      if (!loaded || !marketingContentState.products.length) {
4090        setMarketingStatus(t("mvp.marketing.loadFailed", null, "The full product collection could not be loaded. Try opening this dataset again from My runs."), "error");
4091        return;
4092      }
4093      renderMarketingLabels();
4094      renderMarketingProductSelector();
4095      if (marketingContentWorkspace) marketingContentWorkspace.hidden = false;
4096      openMarketingContentButton.setAttribute("aria-expanded", "true");
4097      marketingContentWorkspace?.scrollIntoView?.({ behavior: "smooth", block: "nearest" });
4098    } catch {
4099      setMarketingStatus(t("mvp.marketing.loadFailed", null, "The full product collection could not be loaded. Try opening this dataset again from My runs."), "error");
4100    } finally {
4101      openMarketingContentButton.textContent = originalLabel || t("mvp.marketing.open", null, "Create marketing content");
4102      renderActionAccessGates();
4103    }
4104  });
4105
4106  marketingProductSelect?.addEventListener("change", () => {
4107    const index = Number(marketingProductSelect.value);
4108    marketingContentState.selectedIndex = Number.isInteger(index) && index >= 0 ? index : 0;
4109    renderSelectedMarketingProduct();
4110  });
4111
4112  marketingContextFields?.addEventListener("input", () => {
4113    marketingContentState.creativeOverrides = readCreativeOverrides();
4114    clearPreparedMarketingContent();
4115  });
4116
4117  marketingImages?.addEventListener("change", (event) => {
4118    const input = event.target;
4119    if (!(input instanceof HTMLInputElement) || input.name !== "mvpMarketingImage") return;
4120    marketingContentState.selectedImage = input.value;
4121    clearPreparedMarketingContent();
4122  });
4123
4124  prepareMarketingRequestButton?.addEventListener("click", async () => {
4125    if (!authenticatedActionAllowed() || marketingContentWorkspace?.hidden) return;
4126    const currentUsage = await loadUsage();
4127    if (!currentUsage || !authenticatedActionAllowed()) return;
4128    const sourceProduct = marketingContentState.products[marketingContentState.selectedIndex];
4129    if (!sourceProduct) {
4130      setMarketingStatus(t("mvp.marketing.selectRequired", null, "Choose a product before preparing creative content."), "error");
4131      return;
4132    }
4133    const product = normalizeMarketingProductContext(sourceProduct);
4134    if (!hasMarketingContext(product)) {
4135      setMarketingStatus(t("mvp.marketing.contextRequired", null, "This product does not have enough usable information to prepare creative content."), "error");
4136      return;
4137    }
4138    const creativeOverrides = readCreativeOverrides();
4139    marketingContentState.creativeOverrides = creativeOverrides;
4140    marketingContentState.preparedRequest = {
4141      creationType: "template",
4142      product,
4143      creativeOverrides,
4144      selectedImage: marketingContentState.selectedImage,
4145      purpose: "instagram_story",
4146      output: { preset: "story", width: 1080, height: 1920, aspectRatio: "9:16" },
4147      instruction: "",
4148      template: "product_spotlight",
4149    };
4150    if (window.__MVP_TEST_HOOKS__ && typeof window.__MVP_TEST_HOOKS__ === "object") {
4151      window.__MVP_TEST_HOOKS__.marketingContentRequest = marketingContentState.preparedRequest;
4152    }
4153    setMarketingStatus("Creating Instagram Story…", "prepared");
4154    await drawMarketingStory(marketingContentState.preparedRequest);
4155    setMarketingStatus("");
4156  });
4157
4158  const resetPreview = () => {
4159    if (previewBody) previewBody.textContent = "";
4160    if (previewSummary) previewSummary.textContent = "";
4161    if (previewMore) {
4162      previewMore.textContent = "";
4163      previewMore.hidden = true;
4164    }
4165    if (previewAccountGate) previewAccountGate.hidden = true;
4166    if (previewEmpty) {
4167      previewEmpty.textContent = "";
4168      previewEmpty.hidden = true;
4169    }
4170    if (previewTable) previewTable.hidden = false;
4171    if (previewWrap) previewWrap.style.display = "none";
4172    if (previewDetails) {
4173      previewDetails.hidden = true;
4174      previewDetails.open = false;
4175    }
4176  };
4177
4178  const previewProductState = { products: [], totalCount: 0, selectedIndex: "first", searchQuery: "", authenticated: false };
4179
4180  const toSafeHttpUrl = (value, base = "") => {
4181    const raw = String(value || "").trim();
4182    if (!raw) return "";
4183    try {
4184      const url = base ? new URL(raw, base) : new URL(raw);
4185      return url.protocol === "http:" || url.protocol === "https:" ? url.href : "";
4186    } catch {
4187      return "";
4188    }
4189  };
4190
4191  const renderCompletedScanSource = (requestMeta) => {
4192    const source = E("mvpCompletedScanSource");
4193    const logo = E("mvpCompletedScanLogo");
4194    const host = E("mvpCompletedScanHost");
4195    const link = E("mvpCompletedScanUrl");
4196    if (!source || !logo || !host || !link) return;
4197    source.hidden = true;
4198    logo.hidden = true;
4199    logo.onerror = null;
4200    logo.removeAttribute("src");
4201    host.textContent = "";
4202    link.textContent = "";
4203    link.removeAttribute("href");
4204    const safeUrl = requestMeta?.sourceType === "url" ? toSafeHttpUrl(requestMeta.url) : "";
4205    if (!safeUrl) return;
4206    const url = new URL(safeUrl);
4207    host.textContent = url.hostname.replace(/^www\./i, "");
4208    link.textContent = String(requestMeta.url).trim();
4209    link.href = safeUrl;
4210    logo.onerror = () => { logo.hidden = true; };
4211    logo.src = `${url.origin}/favicon.ico`;
4212    logo.hidden = false;
4213    source.hidden = false;
4214  };
4215
4216  const appendTextCell = (row, value, className = "", label = "") => {
4217    const cell = document.createElement("td");
4218    if (className) cell.className = className;
4219    if (label) cell.dataset.label = label;
4220    if (className === "mvp-preview-name") {
4221      const text = document.createElement("span");
4222      text.textContent = String(value || "");
4223      text.title = String(value || "");
4224      cell.appendChild(text);
4225    } else {
4226      cell.textContent = String(value || "");
4227    }
4228    row.appendChild(cell);
4229  };
4230
4231  const firstText = (...values) => {
4232    for (const value of values) {
4233      const text = String(value || "").trim();
4234      if (text) return text;
4235    }
4236    return "";
4237  };
4238
4239  const productImageFor = (product) => firstText(product?.image, product?.imageUrl, product?.image_url);
4240
4241  const productUrlFor = (product) => firstText(product?.url, product?.productUrl, product?.product_url);
4242
4243  const appendProductLinkCell = (row, value) => {
4244    const cell = document.createElement("td");
4245    cell.className = "mvp-preview-product";
4246    cell.dataset.label = t("mvp.preview.sourceProduct", null, "Source product");
4247    const href = toSafeHttpUrl(value);
4248    if (href) {
4249      const link = document.createElement("a");
4250      link.href = href;
4251      link.target = "_blank";
4252      link.rel = "noopener noreferrer";
4253      link.textContent = t("mvp.preview.openSource", null, "Open source page");
4254      cell.appendChild(link);
4255    } else {
4256      cell.textContent = t("mvp.preview.noLink", null, "No product link");
4257    }
4258    row.appendChild(cell);
4259  };
4260
4261  const imageHrefFor = (product) => {
4262    const raw = productImageFor(product);
4263    if (!raw) return "";
4264    return toSafeHttpUrl(raw, firstText(productUrlFor(product), product?.sourcePage, product?.sourceUrl, product?.source_url));
4265  };
4266
4267  const appendImageCell = (row, product) => {
4268    const cell = document.createElement("td");
4269    cell.className = "mvp-preview-image-cell";
4270    cell.dataset.label = t("mvp.preview.image", null, "Image");
4271    const raw = productImageFor(product);
4272    const href = imageHrefFor(product);
4273    if (href) {
4274      const link = document.createElement("a");
4275      link.href = href;
4276      link.target = "_blank";
4277      link.rel = "noopener noreferrer";
4278      link.className = "mvp-preview-image-link";
4279      link.title = t("mvp.preview.imageAvailable", null, "Image available");
4280      const image = document.createElement("img");
4281      image.src = href;
4282      image.alt = "";
4283      image.loading = "lazy";
4284      image.className = "mvp-preview-image";
4285      image.addEventListener("error", () => {
4286        image.hidden = true;
4287        link.classList.add("mvp-preview-image-link--fallback");
4288      });
4289      const text = document.createElement("span");
4290      text.className = "mvp-preview-image-link-text";
4291      text.textContent = t("mvp.preview.imageAvailable", null, "Image available");
4292      link.appendChild(image);
4293      link.appendChild(text);
4294      cell.appendChild(link);
4295    } else if (raw) {
4296      cell.textContent = t("mvp.preview.imageAvailable", null, "Image available");
4297    } else {
4298      cell.classList.add("mvp-preview-image-cell--missing");
4299      cell.textContent = t("mvp.preview.noImage", null, "No image.");
4300    }
4301    row.appendChild(cell);
4302  };
4303
4304  const descriptionStatusFor = (product) => {
4305    const description = String(product?.description || product?.body_html || product?.bodyHtml || product?.body || "").trim();
4306    if (!description) return "Missing";
4307    return description.length >= 40 ? "Ready" : "Limited";
4308  };
4309
4310  const appendImageStatusCell = (row, product) => {
4311    const hasImage = Boolean(productImageFor(product));
4312    const status = hasImage ? "Available" : "Missing image";
4313    const cell = document.createElement("td");
4314    cell.className = `mvp-preview-image-status mvp-preview-image-status--${hasImage ? "has" : "missing"}`;
4315    cell.dataset.label = "Image";
4316    cell.textContent = status;
4317    row.appendChild(cell);
4318  };
4319
4320  const isNoiseTagToken = (token) => {
4321    const text = String(token || "").toLowerCase();
4322    if (!text || text.length < 4) return true;
4323    if (/^\d+$/.test(text)) return true;
4324    if (/^\d+(?:x|by|-)\d+/.test(text)) return true;
4325    if (/^\d+(?:cm|mm|m|in|inch|ft|kg|g|lb|oz)$/i.test(text)) return true;
4326    if (/^\$?\d+(?:[.,]\d+)?$/.test(text)) return true;
4327    if (/^(usd|cad|eur|gbp|cop|mxn|price|sale|new|the|and|for|with|from|product|products|producto|productos|item|items|shop|store|collections|collection|categoria|categorias|category|categories|gen|kit|badge|amps|cm|mm|inch|inches|feet|meter|meters|www|html|htm)$/.test(text)) return true;
4328    if (/^[a-z]{0,4}\d{2,}[a-z0-9-]*$/i.test(text)) return true;
4329    if (/^[a-z]+-\d+[a-z0-9-]*$/i.test(text)) return true;
4330    return false;
4331  };
4332
4333  const tagTokensFrom = (value) => String(value || "")
4334    .toLowerCase()
4335    .split(/[^a-z0-9]+/i)
4336    .map((token) => token.trim())
4337    .filter((token) => token && !isNoiseTagToken(token));
4338
4339  const suggestedTagsFor = (product) => {
4340    const candidates = [];
4341    const addTokens = (items) => {
4342      for (const token of items) {
4343        if (!candidates.includes(token)) candidates.push(token);
4344      }
4345    };
4346    addTokens(tagTokensFrom(product?.name || product?.title));
4347    const sourceUrl = firstText(product?.sourcePage, product?.sourceUrl, product?.source_url, productUrlFor(product));
4348    try {
4349      const parsed = new URL(sourceUrl, "https://example.invalid");
4350      addTokens(tagTokensFrom(parsed.pathname));
4351    } catch {
4352      addTokens(tagTokensFrom(sourceUrl));
4353    }
4354    const canonicalTag = (token) => token.endsWith("s") && token.length > 4 ? token.slice(0, -1) : token;
4355    return candidates.filter((token, index, allTokens) => {
4356      if (allTokens.slice(0, index).some((other) => canonicalTag(other) === canonicalTag(token))) return false;
4357      return !allTokens.some((left, leftIndex) => allTokens.some((right, rightIndex) => (
4358        leftIndex !== rightIndex
4359        && leftIndex !== index
4360        && rightIndex !== index
4361        && `${left}${right}` === token
4362      )));
4363    }).slice(0, 5);
4364  };
4365
4366  const appendSuggestedTagsCell = (row, product) => {
4367    const tags = suggestedTagsFor(product);
4368    const cell = document.createElement("td");
4369    cell.className = "mvp-preview-tags";
4370    if (tags.length) cell.dataset.i18nSkip = "";
4371    cell.dataset.label = "Suggested tags";
4372    cell.textContent = tags.length ? tags.join(", ") : "No suggested tags";
4373    row.appendChild(cell);
4374  };
4375
4376  const normalizedPreviewSearch = () => String(previewProductState.searchQuery || "").trim().toLocaleLowerCase();
4377
4378  const accessiblePreviewProducts = () => previewProductState.products;
4379
4380  const matchingPreviewEntries = () => {
4381    const query = normalizedPreviewSearch();
4382    return accessiblePreviewProducts()
4383      .map((product, index) => ({ product, index, id: productStableIdentifier(product, index) }))
4384      .filter(({ product }) => {
4385        if (!query) return true;
4386        const context = normalizeMarketingProductContext(product);
4387        return [context.name, context.sku].some((value) => String(value || "").toLocaleLowerCase().includes(query));
4388      });
4389  };
4390
4391  const renderPreviewSelect = () => {
4392    if (!previewProductSelect) return;
4393    const matches = matchingPreviewEntries();
4394    const query = normalizedPreviewSearch();
4395    const selectedStillMatches = previewProductState.selectedIndex !== "first"
4396      && matches.some(({ id }) => id === previewProductState.selectedIndex);
4397    if (!selectedStillMatches) previewProductState.selectedIndex = "first";
4398
4399    previewProductSelect.textContent = "";
4400    const defaultOption = document.createElement("option");
4401    defaultOption.value = "first";
4402    const defaultCount = 10;
4403    const selector = previewProductSelect.closest(".mvp-preview-product-selector");
4404    if (selector) selector.hidden = false;
4405    defaultOption.textContent = query
4406      ? t(`mvp.preview.showFirst${defaultCount}Matches`, null, `Show first ${defaultCount} matching products`)
4407      : t(`mvp.preview.showFirst${defaultCount}`, null, `Show first ${defaultCount} products`);
4408    previewProductSelect.appendChild(defaultOption);
4409    for (const { product, id } of matches) {
4410      const option = document.createElement("option");
4411      option.value = id;
4412      option.textContent = productOptionLabel(product);
4413      previewProductSelect.appendChild(option);
4414    }
4415    previewProductSelect.value = previewProductState.selectedIndex;
4416    previewProductSelect.disabled = matches.length === 0;
4417  };
4418
4419  const renderPreviewRows = () => {
4420    resetPreview();
4421    if (!previewBody || !accessiblePreviewProducts().length) return;
4422    const matches = matchingPreviewEntries();
4423    const query = normalizedPreviewSearch();
4424    const selectedEntry = matches.find(({ id }) => id === previewProductState.selectedIndex);
4425    const defaultCount = 10;
4426    const products = previewProductState.selectedIndex === "first" || !selectedEntry
4427      ? matches.slice(0, defaultCount).map(({ product }) => product)
4428      : [selectedEntry.product];
4429    const renderedCount = products.length;
4430    const total = Number(previewProductState.totalCount || 0);
4431    if (previewSummary) {
4432      previewSummary.textContent = query
4433        ? t("mvp.preview.searchSummary", { shown: renderedCount, matches: matches.length }, `Showing ${renderedCount} of ${matches.length} matching products`)
4434        : !previewProductState.authenticated
4435        ? t("mvp.preview.anonymousSummary", { shown: renderedCount, total }, `Showing ${renderedCount} of ${total} collected products`)
4436        : total > renderedCount
4437        ? `Product preview — showing ${renderedCount} of ${total} collected products`
4438        : `Product preview — showing ${renderedCount} of ${total || renderedCount} collected ${renderedCount === 1 ? "product" : "products"}`;
4439    }
4440    const hiddenProductCount = Math.max(0, total - accessiblePreviewProducts().length);
4441    if (previewMore) {
4442      previewMore.textContent = hiddenProductCount
4443        ? t("mvp.preview.moreFound", { count: hiddenProductCount }, `${hiddenProductCount} more products found`)
4444        : "";
4445      previewMore.hidden = hiddenProductCount <= 0;
4446    }
4447    if (previewAccountGate) previewAccountGate.hidden = true;
4448    if (!matches.length) {
4449      if (previewEmpty) {
4450        previewEmpty.textContent = t("mvp.preview.noMatches", { query: String(previewProductState.searchQuery || "").trim() }, `No products match "${String(previewProductState.searchQuery || "").trim()}".`);
4451        previewEmpty.hidden = false;
4452      }
4453      if (previewTable) previewTable.hidden = true;
4454    }
4455    for (const product of products) {
4456      const context = normalizeMarketingProductContext(product);
4457      const row = document.createElement("tr");
4458      const productIndex = previewProductState.products.indexOf(product);
4459      const productId = productStableIdentifier(product, Math.max(0, productIndex));
4460      row.tabIndex = 0;
4461      row.title = context.name || "Select product";
4462      const selectRow = () => {
4463        previewProductState.selectedIndex = productId;
4464        const marketingIndex = marketingContentState.products.indexOf(product);
4465        if (marketingIndex >= 0) {
4466          marketingContentState.selectedIndex = marketingIndex;
4467          if (marketingProductSelect) marketingProductSelect.value = String(marketingIndex);
4468        }
4469        previewBody?.querySelectorAll("tr").forEach((candidate) => candidate.setAttribute("aria-selected", candidate === row ? "true" : "false"));
4470      };
4471      row.addEventListener("click", selectRow);
4472      row.addEventListener("keydown", (event) => {
4473        if (event.key === "Enter" || event.key === " ") {
4474          event.preventDefault();
4475          selectRow();
4476        }
4477      });
4478      appendImageCell(row, product);
4479      appendTextCell(row, context.name, "mvp-preview-name", t("mvp.preview.name", null, "Product name"));
4480      appendTextCell(row, context.sku, "mvp-preview-sku", t("mvp.preview.sku", null, "SKU"));
4481      appendTextCell(row, context.price, "mvp-preview-price", t("mvp.preview.price", null, "Price"));
4482      appendProductLinkCell(row, context.productUrl || productUrlFor(product));
4483      if (explorerMode) {
4484        appendTextCell(row, String(explorerPopulatedFieldCount(product)), "mvp-explorer-field-count", "Populated fields");
4485        const useCell = document.createElement("td");
4486        useCell.className = "mvp-explorer-use";
4487        const use = document.createElement("a");
4488        const productUrl = context.productUrl || productUrlFor(product);
4489        use.href = `/app-mvp2?pipeline=instagram-story&url=${encodeURIComponent(productUrl || "")}`;
4490        use.textContent = "Use product";
4491        useCell.append(use); row.append(useCell);
4492      }
4493      previewBody.appendChild(row);
4494    }
4495    if (previewDetails) {
4496      previewDetails.hidden = false;
4497      previewDetails.open = true;
4498    }
4499    if (previewWrap) previewWrap.style.display = "block";
4500  };
4501
4502  const renderPreview = (products, totalCount, authenticated = isSignedIn()) => {
4503    const selectedIdentifier = previewProductState.selectedIndex;
4504    previewProductState.products = Array.isArray(products) ? products : [];
4505    previewProductState.totalCount = Number(totalCount || previewProductState.products.length);
4506    previewProductState.authenticated = authenticated;
4507    const selectionStillExists = selectedIdentifier !== "first"
4508      && previewProductState.products.some((product, index) => productStableIdentifier(product, index) === selectedIdentifier);
4509    previewProductState.selectedIndex = selectionStillExists ? selectedIdentifier : "first";
4510    previewProductState.searchQuery = "";
4511    if (previewSearch) {
4512      previewSearch.value = "";
4513      previewSearch.placeholder = t("mvp.preview.searchPlaceholder", null, "Search products by name or SKU…");
4514      previewSearch.setAttribute("aria-label", t("mvp.preview.searchLabel", null, "Search products"));
4515    }
4516    setText("mvpPreviewSearchLabel", t("mvp.preview.searchLabel", null, "Search products"));
4517    setText("mvpPreviewProductSelectLabel", t("mvp.marketing.chooseProduct", null, "Choose a product"));
4518    renderPreviewSelect();
4519    renderPreviewRows();
4520  };
4521
4522  const renderResultAccessState = (authenticated) => {
4523    previewProductState.authenticated = authenticated;
4524    renderPreviewSelect();
4525    renderPreviewRows();
4526    renderDownloadLinks(lastMvpResult?.artifacts || {}, authenticated);
4527    if (productReviewSection) productReviewSection.hidden = true;
4528    if (downloadsPanel) downloadsPanel.hidden = false;
4529    if (cleanupPanel) cleanupPanel.hidden = lastMvpResultViewModel?.productsCollected <= 0;
4530    renderActionAccessGates();
4531    if (completedSignupCta) completedSignupCta.hidden = authenticated || !completedSignupCtaEligible;
4532    if (lastMvpResultViewModel) renderCompletedResultLayout(lastMvpResultViewModel, lastMvpResult);
4533  };
4534
4535  previewSearch?.addEventListener("input", () => {
4536    previewProductState.searchQuery = previewSearch.value;
4537    renderPreviewSelect();
4538    renderPreviewRows();
4539  });
4540
4541  previewProductSelect?.addEventListener("change", () => {
4542    previewProductState.selectedIndex = previewProductSelect.value;
4543    renderPreviewRows();
4544  });
4545
4546  E("mvpPreviewCreateAccount")?.addEventListener("click", () => {
4547    focusAccountFromGate("signup", "catalog");
4548  });
4549
4550  E("mvpPreviewSignIn")?.addEventListener("click", () => {
4551    focusAccountFromGate("signin", "catalog");
4552  });
4553
4554  E("mvpCompletedSignupCreate")?.addEventListener("click", () => {
4555    focusAccountFromGate("signup", "catalog");
4556  });
4557
4558  E("mvpCompletedSignupSignIn")?.addEventListener("click", () => {
4559    focusAccountFromGate("signin", "download");
4560  });
4561
4562  // The canonical Next actions section owns the real workflow controls. The
4563  // former preview-derived suggestion list is intentionally no longer rendered.
4564  const renderNextActions = () => {};
4565
4566  const renderTopIssues = (issues, emptyText = "No warnings or blocking errors.") => {
4567    const list = E("mvpTopIssues");
4568    if (!list) return;
4569    const panel = list.closest(".mvp-top-issues");
4570    const details = E("mvpReviewDetails");
4571    list.textContent = "";
4572    const hasIssues = Array.isArray(issues) && issues.length > 0;
4573    if (!hasIssues && !emptyText) {
4574      if (panel) panel.hidden = true;
4575      if (details) {
4576        details.hidden = true;
4577      }
4578      return;
4579    }
4580    if (panel) panel.hidden = false;
4581    if (details) {
4582      details.hidden = false;
4583    }
4584    const items = hasIssues ? issues : [emptyText];
4585    for (const issue of items.slice(0, 1)) {
4586      const item = document.createElement("li");
4587      item.textContent = String(issue || "").trim() || "Review the generated files.";
4588      list.appendChild(item);
4589    }
4590  };
4591
4592  const catalogOpportunityHiddenState = new Map();
4593  const renderCatalogOpportunityPreview = (data, requestMeta, viewModel) => {
4594    const discovery = viewModel.discovery || {};
4595    const potential = catalogPotentialFromResponse(data) || {};
4596    const collected = potential.dataCollected || potential.data_collected || {};
4597    const coverage = data?.customerSummary?.discovery_coverage || data?.discoveryCoverage || {};
4598    const candidates = Number(collected.relevantCandidatePages ?? collected.relevant_candidate_pages ?? discovery.candidatePagesFound ?? 0);
4599    const scanned = Number(collected.pagesScanned ?? collected.pages_scanned ?? discovery.pagesScanned ?? 0);
4600    const blocked = /blocked|403|429|needs_capture|capture[_ -]required/i.test([
4601      data?.status, data?.outcome, data?.customerSummary?.status,
4602      data?.reasonCode, data?.reason_code, data?.customerSummary?.reason_code,
4603      coverage.stopped_reason, coverage.stoppedReason, discovery.stoppedReason,
4604      potential.scanCoverage?.stoppedReason,
4605    ].join(" "));
4606    if (!requestMeta?.salesAgentClaimPreview || viewModel.productsCollected !== 0 || candidates < 5 || scanned < 1 || blocked) return false;
4607    if (stateTitle) stateTitle.textContent = "Catalog structure detected";
4608    if (resultStatus) { resultStatus.textContent = "Catalog diagnostic"; resultStatus.dataset.state = "ready"; }
4609    if (resultBanner) {
4610      resultBanner.textContent = `We found ${candidates} likely catalog or brand pages, but no public product records were available in this scan.`;
4611      resultBanner.hidden = false;
4612    }
4613    if (stateMessage) {
4614      stateMessage.textContent = "This often means detailed product data is supplied through another catalog, spreadsheet, PDF, supplier website, or B2B workflow.";
4615      stateMessage.hidden = false;
4616    }
4617    if (stateHint) {
4618      stateHint.textContent = "Send us one supplier catalog and we can show how it converts into structured ecommerce-ready product data.";
4619      stateHint.hidden = false;
4620    }
4621    for (const id of ["mvpPreviewDetails", "mvpNextActions", "mvpEnrichmentAction", "mvpEnrichmentStatusPanel", "mvpMarketingContentStep", "mvpMarketingContentWorkspace", "mvpCleanupPanel", "mvpDownloadsPanel", "mvpDatasetActions", "mvpDatasetRecommendation", "mvpProductDatasetContent", "mvpOverviewEnrichment"]) {
4622      const element = E(id);
4623      if (element) {
4624        catalogOpportunityHiddenState.set(element, element.hidden);
4625        element.hidden = true;
4626      }
4627    }
4628    return true;
4629  };
4630
4631  const renderCompletedPackage = (data, requestMeta, startedAt, completionEvent = "mvp_ingest_completed") => {
4632    if (!isForeground(requestMeta)) return;
4633    for (const [element, hidden] of catalogOpportunityHiddenState) element.hidden = hidden;
4634    catalogOpportunityHiddenState.clear();
4635    const renderStartedAt = performance.now();
4636    lastCompletedPackage = { data, requestMeta };
4637    if (!requestMeta?.salesAgentClaimPreview && !isSignedIn() && requestMeta?.sourceType === "url" && data?.runId && data?.ok !== false && data?.discoveryJob?.state !== "discovering") {
4638      anonymousCompletedRunId = String(data.runId);
4639      try {
4640        window.localStorage?.setItem(ANONYMOUS_COMPLETED_RUN_STORAGE_KEY, JSON.stringify({
4641          runId: anonymousCompletedRunId,
4642          sourceUrl: requestMeta?.url || data?.sourceUrl || "",
4643          host: requestMeta?.host || data?.host || "",
4644          status: "completed",
4645        }));
4646      } catch {}
4647    }
4648    setScanActive(["queued", "running", "discovering", "processing"].includes(String(data?.discoveryJob?.state || "").toLowerCase()), { keepIntentHidden: true });
4649    if (startAnotherScanButton) startAnotherScanButton.hidden = false;
4650    renderCheckoutRetentionMessage();
4651    const statusEl = E("mvpStatus");
4652    const metricProducts = E("mvpMetricProducts");
4653    const metricImportReady = E("mvpMetricImportReady");
4654    const metricReviewRows = E("mvpMetricReviewRows");
4655    const metricBlocked = E("mvpMetricBlocked");
4656    const summaryMetricImportReady = E("mvpSummaryMetricImportReady");
4657    const summaryMetricReviewRows = E("mvpSummaryMetricReviewRows");
4658    const summaryMetricBlocked = E("mvpSummaryMetricBlocked");
4659    const summaryMetricWarnings = E("mvpSummaryMetricWarnings");
4660    const summaryMetricRejected = E("mvpSummaryMetricRejected");
4661    const metricBlockingIssues = E("mvpMetricBlockingIssues");
4662    const metricWarnings = E("mvpMetricWarnings");
4663    const metricRejected = E("mvpMetricRejected");
4664    const summary = data?.summary || {};
4665    const customerSummary = data?.customerSummary || {};
4666    const report = data?.report || {};
4667    const viewModel = deriveMvpResultViewModel(data, requestMeta);
4668    const productsFound = viewModel.productsCollected;
4669    const importReadyProducts = viewModel.importableProducts;
4670    const blockedProducts = Number(summary.blockedProducts ?? customerSummary.blocked_products ?? Math.max(0, productsFound - importReadyProducts));
4671    const rejectedRows = Number(summary.rejectedRows ?? customerSummary.rejected_rows ?? 0);
4672    const warningCount = Number(summary.warningCount ?? customerSummary.warning_count ?? 0);
4673    const { missingImages, weakDescriptions } = resultContentMetrics(data);
4674    const rowsNeedingReview = viewModel.productsNeedingReview;
4675    const mainIssues = Array.isArray(summary?.topIssues)
4676      ? summary.topIssues
4677      : Array.isArray(customerSummary?.main_issues) ? customerSummary.main_issues : [];
4678    const hasSuccessfulExport = productsFound > 0 && importReadyProducts > 0;
4679    const hasBlockingErrors = blockedProducts > 0 || rejectedRows > 0;
4680    const blockingIssueCount = Number(summary.blockingIssueCount ?? customerSummary.blocking_issue_count ?? (hasBlockingErrors ? mainIssues.length : 0));
4681    const resultState = !hasSuccessfulExport
4682      ? "failure"
4683      : hasBlockingErrors
4684        ? "warning"
4685        : "success";
4686    setStateCopy(resultState);
4687    const summaryMessage = String(customerSummary?.recommendation || data?.message || "").trim();
4688    const hasReviewIssues = hasBlockingErrors || warningCount > 0 || mainIssues.length > 0;
4689    setResultSourceLabel(requestMeta);
4690    if (stateTitle && hasSuccessfulExport) stateTitle.textContent = "Product dataset ready for review";
4691    if (resultStatus) {
4692      resultStatus.textContent = blockedProducts > 0 ? "Rows require review" : "Dataset collected";
4693      resultStatus.dataset.state = blockedProducts > 0 ? "blocked" : "ready";
4694    }
4695    if (resultBanner) {
4696      resultBanner.textContent = !hasSuccessfulExport
4697        ? STATE_COPY.failure.banner
4698        : "";
4699      resultBanner.hidden = hasSuccessfulExport;
4700    }
4701    if (stateMessage) {
4702      stateMessage.textContent = !hasSuccessfulExport
4703        ? summaryMessage
4704        : blockedProducts > 0
4705          ? "Some records require review before using the current dataset for an ecommerce import."
4706          : "";
4707      stateMessage.hidden = !stateMessage.textContent;
4708    }
4709    if (issueSummaryTitle) issueSummaryTitle.textContent = hasBlockingErrors ? "Blocking issues" : "Content warnings";
4710    if (stateHint) {
4711      stateHint.textContent = "";
4712      stateHint.hidden = true;
4713    }
4714    if (statusEl) {
4715      statusEl.textContent = "";
4716      statusEl.hidden = true;
4717    }
4718    setImportQualityMetrics({ productsFound, importReadyProducts, warningCount, blockingIssueCount, blockedProducts });
4719    if (metricProducts) metricProducts.textContent = String(productsFound);
4720    if (metricImportReady) metricImportReady.textContent = String(importReadyProducts);
4721    if (metricReviewRows) metricReviewRows.textContent = String(rowsNeedingReview);
4722    if (metricBlocked) metricBlocked.textContent = String(blockedProducts);
4723    if (summaryMetricImportReady) summaryMetricImportReady.textContent = String(importReadyProducts);
4724    if (summaryMetricReviewRows) summaryMetricReviewRows.textContent = String(rowsNeedingReview);
4725    if (summaryMetricBlocked) summaryMetricBlocked.textContent = String(blockedProducts);
4726    if (metricBlockingIssues) metricBlockingIssues.textContent = String(blockingIssueCount);
4727    if (metricWarnings) metricWarnings.textContent = String(warningCount);
4728    setText("mvpMetricMissingImages", missingImages);
4729    setText("mvpMetricMissingSkus", viewModel.productsWithoutSkus);
4730    setText("mvpMetricWeakDescriptions", weakDescriptions);
4731    if (metricRejected) metricRejected.textContent = String(rejectedRows);
4732    if (summaryMetricWarnings) summaryMetricWarnings.textContent = String(warningCount);
4733    if (summaryMetricRejected) summaryMetricRejected.textContent = String(rejectedRows);
4734    renderTopIssues(
4735      mainIssues,
4736      hasReviewIssues ? (hasBlockingErrors ? "Blocking issues are in issues.csv." : "Content warnings are in issues.csv.") : ""
4737    );
4738    const discovery = viewModel.discovery;
4739    const discoveryInProgress = discovery?.enabled === true && discovery.pagesRemaining > 0;
4740    const authenticated = data?.auth?.authenticated === true || data?.usage?.authenticated === true || isSignedIn();
4741    renderPreview(Array.isArray(data?.products) && data.products.length ? data.products : data?.preview, productsFound, authenticated);
4742    renderNextActions(data?.preview);
4743    renderDiscoveryCoverage(discovery);
4744    renderExplorerPages(discovery);
4745    if (explorerMode) {
4746      if (reviewPreviewSlot && successPanel && reviewPreviewSlot.parentElement !== successPanel) successPanel.insertBefore(reviewPreviewSlot, explorerTabs?.nextSibling || null);
4747      if (discoveryCoveragePanel && successPanel && discoveryCoveragePanel.parentElement !== successPanel) successPanel.append(discoveryCoveragePanel);
4748      if (explorerTabs) explorerTabs.hidden = false;
4749      setExplorerTab("products");
4750    }
4751    if (continueScanProgress && data?.discoveryJob?.state === "discovering") {
4752      const durableCoverage = data?.customerSummary?.discovery_coverage || data?.discoveryCoverage || {};
4753      const pagesAttempted = firstFiniteCount(
4754        durableCoverage?.pages_attempted,
4755        durableCoverage?.pagesAttempted,
4756        data?.discoveryJob?.pagesAttempted,
4757      ) || 0;
4758      const pagesRemaining = firstFiniteCount(
4759        durableCoverage?.pages_remaining,
4760        durableCoverage?.pagesRemaining,
4761      ) || 0;
4762      const pagesDiscovered = firstFiniteCount(
4763        durableCoverage?.candidate_pages_found,
4764        durableCoverage?.pages_discovered,
4765        durableCoverage?.pagesDiscovered,
4766        data?.discoveryJob?.pagesDiscovered,
4767      ) || pagesAttempted + pagesRemaining;
4768      setScanProgress(pagesAttempted, pagesDiscovered);
4769      continueScanProgress.hidden = false;
4770      if (continueScanLoading) continueScanLoading.hidden = false;
4771    } else if (requestMeta?.sourceType === "url" && continueScanProgress && !continueScanProgress.hidden) {
4772      setScanProgress(1, 1);
4773      continueScanProgress.hidden = true;
4774      if (continueScanLoading) continueScanLoading.hidden = true;
4775      if (scanReassurance) scanReassurance.hidden = true;
4776    }
4777    const catalogPotential = catalogPotentialFromResponse(data);
4778    renderCatalogPotential(catalogPotential, discovery, viewModel);
4779    renderIncompleteScanWarning(discovery, productsFound);
4780    const showEnrichment = renderRecommendedActions({
4781      data,
4782      productsFound,
4783      viewModel,
4784    });
4785    storeLastMvpResult(data, requestMeta);
4786    log(completionEvent, { ...requestMeta, requestId: data?.requestId || null, summary, report, elapsedMs: Date.now() - startedAt });
4787    if (successPanel) successPanel.style.display = "grid";
4788    document.querySelector(".mvp-account-card")?.classList.add("mvp-account-card--result");
4789    setNoReportMessageVisible(false);
4790    renderDownloadLinks(data?.artifacts || {}, authenticated);
4791    setResultWorkflowStage({
4792      discovery,
4793      automaticFullCatalog: Boolean(data?.discoveryJob?.id),
4794      hasCatalogOpportunity: Boolean(catalogPotential),
4795      requestMeta,
4796      hasSuccessfulExport,
4797      hasPreview: Array.isArray(data?.preview) && data.preview.length > 0,
4798      showRecommended: blockedProducts > 0,
4799      showEnrichment,
4800      showReview: hasReviewIssues,
4801      finalTitle: stateTitle?.textContent || "",
4802      finalStatus: resultStatus?.textContent || "",
4803      finalStatusState: resultStatus?.dataset?.state || "",
4804      finalBanner: resultBanner?.textContent || "",
4805      finalMessage: stateMessage?.textContent || "",
4806    });
4807    renderCompletedScanSource(requestMeta);
4808    renderMvpResultViewModel(viewModel, data);
4809    renderResultAccessState(authenticated);
4810    renderCatalogOpportunityPreview(data, requestMeta, viewModel);
4811    setDetails(JSON.stringify({
4812      requestId: data?.requestId || null,
4813      status: data?.status,
4814      report,
4815      summary,
4816      customerSummary,
4817      artifacts: data?.artifacts || {},
4818      usage: data?.usage || null,
4819      message: data?.message || "",
4820    }, null, 2));
4821    const frontendRenderMs = Math.round(performance.now() - renderStartedAt);
4822    perfPhase("frontend_render", frontendRenderMs, { requestId: data?.requestId || null, runId: data?.runId || null });
4823    if (requestMeta?.perf) {
4824      requestMeta.perf.frontendRenderMs = frontendRenderMs;
4825      requestMeta.perf.uiRenderedAtMs = Date.now();
4826      log("mvp_run_complete", { requestId: data?.requestId || null, runId: data?.runId || null, ...requestMeta.perf, totalMs: Date.now() - startedAt });
4827    }
4828    if (continueScanStatus) continueScanStatus.hidden = true;
4829  };
4830
4831  function mvpDeliveryError(code, message) {
4832    const error = new Error(message);
4833    error.code = code;
4834    return error;
4835  }
4836
4837  async function waitForMvpUrlJob(jobId, requestMeta, startedAt, maxWaitMs = 20 * 60 * 1000, runId = "", statusUrl = "", requestId = "", authenticatedRequest = isSignedIn(), onStatus = null, showGlobalStatus = true) {
4838    const statusEl = E("mvpStatus");
4839    const configuredPollDelayMs = Number(globalThis.__MVP_JOB_POLL_DELAY_MS__);
4840    const pollDelayMs = Number.isFinite(configuredPollDelayMs) && configuredPollDelayMs >= 0
4841      ? configuredPollDelayMs
4842      : 2000;
4843    const requestTimeoutMs = 15_000;
4844    const maxConsecutiveReadFailures = 5;
4845    const startedPollingAt = Date.now();
4846    const resolvedStatusUrl = statusUrl || `/api/jobs/${encodeURIComponent(jobId)}`;
4847    const pollingFetch = authenticatedRequest
4848      ? authenticatedFetch
4849      : (url, options) => fetch(url, options);
4850    let consecutiveStatusReadFailures = 0;
4851    let consecutiveResultReadFailures = 0;
4852    let previousStatus = "";
4853    while (Date.now() - startedPollingAt < maxWaitMs) {
4854      assertForeground(requestMeta);
4855      let res;
4856      let payload;
4857      const controller = new AbortController();
4858      const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
4859      try {
4860        res = await pollingFetch(resolvedStatusUrl, {
4861          credentials: "same-origin",
4862          signal: controller.signal,
4863        }, { endpointType: "job_status", requestId, runId, jobId });
4864        payload = await parseResponsePayload(res);
4865      } catch (err) {
4866        consecutiveStatusReadFailures += 1;
4867        if (consecutiveStatusReadFailures >= maxConsecutiveReadFailures) {
4868          const failure = mvpDeliveryError("status_read_failed", err?.message || "The job finished remotely, but its status could not be read.");
4869          Object.assign(failure, { requestId, runId, jobId, endpointType: "job_status" });
4870          throw failure;
4871        }
4872        await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
4873        continue;
4874      } finally {
4875        clearTimeout(timeout);
4876      }
4877      if (!res.ok || payload?.ok === false) {
4878        if (res.status === 401) throw responseError(res, payload, "Job access is unavailable.", { endpointType: "job_status", requestId, runId, jobId, authenticatedRequest });
4879        consecutiveStatusReadFailures += 1;
4880        if (consecutiveStatusReadFailures >= maxConsecutiveReadFailures) {
4881          const failure = responseError(res, payload, `Job status request failed (${res.status}).`, { endpointType: "job_status", requestId, runId, jobId, reasonCode: "status_read_failed" });
4882          failure.code = "status_read_failed";
4883          throw failure;
4884        }
4885        await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
4886        continue;
4887      }
4888      consecutiveStatusReadFailures = 0;
4889      const job = payload?.job || {};
4890      const status = String(job.status || payload?.status || "").toLowerCase();
4891      if (status !== previousStatus) {
4892        const createdAtMs = Date.parse(job.createdAt || "");
4893        const jobStartedAtMs = Date.parse(job.startedAt || "");
4894        const jobFinishedAtMs = Date.parse(job.finishedAt || "");
4895        requestMeta.perf ||= {};
4896        if (status === "queued" && !requestMeta.perf.jobQueuedAtMs) requestMeta.perf.jobQueuedAtMs = Date.now();
4897        if (Number.isFinite(jobStartedAtMs)) {
4898          requestMeta.perf.jobStartedAtMs = jobStartedAtMs;
4899          if (Number.isFinite(createdAtMs)) requestMeta.perf.queueWaitMs = Math.max(0, jobStartedAtMs - createdAtMs);
4900        }
4901        if (Number.isFinite(jobFinishedAtMs)) {
4902          requestMeta.perf.jobCompletedAtMs = jobFinishedAtMs;
4903          if (Number.isFinite(jobStartedAtMs)) requestMeta.perf.workerExecutionMs = Math.max(0, jobFinishedAtMs - jobStartedAtMs);
4904        }
4905        log("job_status_change", { requestId, runId, jobId, status, queueWaitMs: requestMeta.perf.queueWaitMs, workerExecutionMs: requestMeta.perf.workerExecutionMs });
4906        previousStatus = status;
4907      }
4908      assertForeground(requestMeta);
4909      if (typeof onStatus === "function") await onStatus(status, job, payload);
4910      assertForeground(requestMeta);
4911      if (status === "completed" || status === "succeeded") {
4912        const result = job?.result?.stdoutJson || payload?.result?.stdoutJson;
4913        // Worker stdout can carry only a compact preview. The durable result has
4914        // the complete `products` array used by every user's product selector.
4915        const productsFound = Number(result?.summary?.productsFound || 0);
4916        const compactPreviewCanCoverCatalog = productsFound <= ANONYMOUS_PRODUCT_LIMIT;
4917        if (result?.ok === true && (Array.isArray(result?.products) || compactPreviewCanCoverCatalog)) return result;
4918        if (result?.ok === false && !result?._truncated) {
4919          const failure = new Error(result?.error || "The MVP run failed.");
4920          Object.assign(failure, { requestId, runId, jobId, endpointType: "job_status" });
4921          throw failure;
4922        }
4923        if (runId) {
4924          const resultFetchStartedAt = performance.now();
4925          let resultRes;
4926          let durableResult;
4927          const resultController = new AbortController();
4928          const resultTimeout = setTimeout(() => resultController.abort(), requestTimeoutMs);
4929          try {
4930            resultRes = await pollingFetch(`/api/mvp/runs/${encodeURIComponent(runId)}/result`, {
4931              credentials: "same-origin",
4932              signal: resultController.signal,
4933            }
4933, { endpointType: "run_result", requestId, runId, jobId });
4934            durableResult = await parseResponsePayload(resultRes);
4935          } catch (err) {
4936            consecutiveResultReadFailures += 1;
4937            if (consecutiveResultReadFailures >= maxConsecutiveReadFailures) {
4938              const failure = mvpDeliveryError("result_unavailable", err?.message || "The job completed, but its import package is temporarily unavailable.");
4939              Object.assign(failure, { requestId, runId, jobId, endpointType: "run_result" });
4940              throw failure;
4941            }
4942            await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
4943            continue;
4944          } finally {
4945            clearTimeout(resultTimeout);
4946          }
4947          if (resultRes.ok && durableResult?.ok) {
4948            requestMeta.perf ||= {};
4949            requestMeta.perf.resultFetchMs = Math.round(performance.now() - resultFetchStartedAt);
4950            requestMeta.perf.resultLoadedAtMs = Date.now();
4951            perfPhase("result_fetch", requestMeta.perf.resultFetchMs, { requestId, runId, jobId });
4952            return durableResult;
4953          }
4954          if (resultRes.status === 401) throw responseError(resultRes, durableResult, "Result access is unavailable.", { endpointType: "run_result", requestId, runId, jobId, authenticatedRequest });
4955          if (durableResult?.status === "failed" || durableResult?.reasonCode === "result_failed") {
4956            const failure = responseError(resultRes, durableResult, "The worker could not complete this import package.", { endpointType: "run_result", requestId, runId, jobId, reasonCode: "result_failed" });
4957            failure.code = "result_failed";
4958            throw failure;
4959          }
4960          if ((resultRes.status === 202 || resultRes.status === 409) && durableResult?.ok !== true) {
4961            consecutiveResultReadFailures = 0;
4962            if (showGlobalStatus && statusEl) {
4963              statusEl.textContent = "Finalizing your import package…";
4964              statusEl.hidden = false;
4965            }
4966            await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
4967            continue;
4968          }
4969          consecutiveResultReadFailures += 1;
4970          if (consecutiveResultReadFailures < maxConsecutiveReadFailures) {
4971            await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
4972            continue;
4973          }
4974          const failure = responseError(resultRes, durableResult, "The job completed, but its import package is temporarily unavailable.", { endpointType: "run_result", requestId, runId, jobId, reasonCode: "result_unavailable" });
4975          failure.code = "result_unavailable";
4976          throw failure;
4977        }
4978        const failure = mvpDeliveryError("result_unavailable", "The job completed, but its import package is unavailable.");
4979        Object.assign(failure, { requestId, runId, jobId, endpointType: "run_result" });
4980        throw failure;
4981      }
4982      if (["failed", "cancelled", "enqueue_failed", "stale", "error"].includes(status)) {
4983        const failure = new Error(job.error || payload?.error || `Job ${status}.`);
4984        Object.assign(failure, { requestId, runId, jobId, endpointType: "job_status", httpStatus: res.status });
4985        throw failure;
4986      }
4987      if (showGlobalStatus && statusEl) {
4988        const elapsed = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
4989        statusEl.textContent = `Generating import package in the background (${elapsed}s)…`;
4990        statusEl.hidden = false;
4991      }
4992      await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
4993    }
4994    log("mvp_ingest_job_timeout", { ...requestMeta, requestId, runId, jobId, endpointType: "job_status", elapsedMs: Date.now() - startedAt });
4995    const failure = mvpDeliveryError("poll_timeout", "Timed out waiting for the URL run to finish.");
4996    Object.assign(failure, { requestId, runId, jobId, endpointType: "job_status" });
4997    throw failure;
4998  }
4999
5000  const finishDiscoveryFromDurableCoverage = (data) => {
5001    const coverage = data?.customerSummary?.discovery_coverage || data?.discoveryCoverage || null;
5002    const pagesRemaining = firstFiniteCount(coverage?.pages_remaining, coverage?.pagesRemaining);
5003    if (data?.discoveryJob?.state !== "discovering" || pagesRemaining === null || pagesRemaining > 0) return data;
5004    return { ...data, discoveryJob: { ...data.discoveryJob, state: "complete", terminal: true } };
5005  };
5006
5007  const waitForFullCatalogChain = async (data, requestMeta, startedAt) => {
5008    assertForeground(requestMeta);
5009    let current = finishDiscoveryFromDurableCoverage(data);
5010    let lastPolledJobId = "";
5011    const configuredPollDelayMs = Number(globalThis.__MVP_JOB_POLL_DELAY_MS__);
5012    const pollDelayMs = Number.isFinite(configuredPollDelayMs) && configuredPollDelayMs >= 0
5013      ? configuredPollDelayMs
5014      : 2000;
5015    const readPersistedRun = async (runId, currentJobId) => {
5016      const resultFetch = isSignedIn()
5017        ? authenticatedFetch
5018        : (url, options) => fetch(url, options);
5019      const response = await resultFetch(`/api/mvp/runs/${encodeURIComponent(runId)}/result`, {
5020        credentials: "same-origin",
5021      }, { endpointType: "run_result", runId, jobId: currentJobId || "" });
5022      const result = await parseResponsePayload(response);
5023      assertForeground(requestMeta);
5024      if (!response.ok || result?.ok !== true) {
5025        throw responseError(response, result, "The current discovery run could not be refreshed.", {
5026          endpointType: "run_result", runId, jobId: currentJobId || "",
5027        });
5028      }
5029      return result;
5030    };
5031    const throwIfDiscoveryFailed = (result) => {
5032      if (result?.discoveryJob?.state !== "failed") return;
5033      clearActiveRunObserver(result?.runId || current?.runId || data?.runId);
5034      const failure = mvpDeliveryError("result_failed", result?.message || "Full-catalog discovery failed.");
5035      Object.assign(failure, {
5036        reasonCode: "result_failed",
5037        runId: result?.runId || current?.runId || "",
5038        jobId: result?.discoveryJob?.currentJobId || "",
5039        endpointType: "run_result",
5040      });
5041      throw failure;
5042    };
5043
5044    while (current?.discoveryJob) {
5045      assertForeground(requestMeta);
5046      const observedRunId = String(current?.runId || data?.runId || "");
5047      trackActiveRunObserver({
5048        runId: observedRunId,
5049        sourceUrl: requestMeta?.url || "",
5050        host: requestMeta?.host || "",
5051        state: "observing",
5052      });
5053      throwIfDiscoveryFailed(current);
5054      if (current.discoveryJob.state !== "discovering") break;
5055      const runId = observedRunId;
5056      renderCompletedPackage(current, requestMeta, startedAt, "mvp_discovery_partial_updated");
5057      if (continueScanLoadingStatus) continueScanLoadingStatus.textContent = "Discovering full catalog";
5058      if (!runId) break;
5059      const nextJobId = String(current.discoveryJob.currentJobId || "");
5060      if (!nextJobId || nextJobId === lastPolledJobId) {
5061        await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
5062        current = finishDiscoveryFromDurableCoverage(await readPersistedRun(runId, nextJobId));
5063        continue;
5064      }
5065
5066      lastPolledJobId = nextJobId;
5067      activeDiscoveryJobId = nextJobId;
5068      if (stopDiscoveryButton) {
5069        stopDiscoveryButton.hidden = false;
5070        stopDiscoveryButton.textContent = "Stop discovery";
5071      }
5072      try {
5073        await waitForMvpUrlJob(
5074          nextJobId, requestMeta, startedAt, Infinity, runId,
5075          `/api/jobs/${encodeURIComponent(nextJobId)}`, current.requestId || "", isSignedIn(),
5076          async () => {
5077            if (continueScanLoadingStatus) continueScanLoadingStatus.textContent = "Discovering full catalog";
5078            const durable = finishDiscoveryFromDurableCoverage(await readPersistedRun(runId, nextJobId));
5079            current = durable;
5080            renderCompletedPackage(durable, requestMeta, startedAt, "mvp_discovery_partial_updated");
5081          }, false
5082        );
5083      } catch (error) {
5084        assertForeground(requestMeta);
5085        // A job pointer is only a detail channel. The persisted run decides whether
5086        // discovery is still healthy or has actually failed.
5087        current = { ...current, discoveryJob: { ...current.discoveryJob, currentJobId: nextJobId } };
5088      }
5089    }
5090    assertForeground(requestMeta);
5091    activeDiscoveryJobId = "";
5092    const finalRunId = String(current?.runId || data?.runId || "");
5093    if (current?.discoveryJob?.state === "discovering") {
5094      clearActiveRunObserver(finalRunId);
5095    } else {
5096      clearActiveRunObserver(finalRunId);
5097      try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
5098    }
5099    return current;
5100  };
5101
5102  /* Legacy automatic restoration was intentionally removed. Recovery is now
5103     discovered below and only attaches or continues after an explicit click. */
5104  const unusedRestoreActiveInitialUrlRun = async () => {
5105    if (activeInitialScanJobId) return false;
5106    let localRun = null;
5107    try { localRun = JSON.parse(window.localStorage?.getItem(ACTIVE_DISCOVERY_STORAGE_KEY) || "null"); } catch {}
5108    if (!isSignedIn() && localRun?.runId) {
5109      const run = localRun;
5110      const restoredStartedAt = Number.isFinite(Date.parse(run.createdAt)) ? Date.parse(run.createdAt) : Date.now();
5111      const requestMeta = {
5112        sourceType: "url", host: run.host || "", url: run.sourceUrl || "", restored: true,
5113        perf: { requestSubmittedAtMs: restoredStartedAt },
5114      };
5115      activeInitialScanJobId = String(run.jobId || "");
5116      trackActiveRunObserver({ runId: run.runId, sourceUrl: run.sourceUrl, host: run.host, state: "restoring" });
5117      markActiveRunRecoveryReady();
5118      setNoReportMessageVisible(false);
5119      let loading = null;
5120      try {
5121        const response = await fetch(`/api/mvp/runs/${encodeURIComponent(run.runId)}/result`, { credentials: "same-origin" });
5122        const partial = finishDiscoveryFromDurableCoverage(await parseResponsePayload(response));
5123        if (response.status === 404 || response.status === 410) {
5124          clearActiveRunObserver(run.runId);
5125          try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
5126          return false;
5127        }
5128        if (!response.ok || partial?.ok !== true) {
5129          if (continueScanStatus) {
5130            continueScanStatus.hidden = false;
5131            continueScanStatus.textContent = "Discovery is continuing in the background. Reload later to resume live updates.";
5132          }
5133          return true;
5134        }
5135        if (partial?.discoveryJob?.state === "discovering") {
5136          setUrlGenerationLoading(true);
5137          loading = startInitialScanLoading(restoredStartedAt);
5138          loading.updateStatus("recovering");
5139        }
5140        renderCompletedPackage(partial, requestMeta, restoredStartedAt, "mvp_active_run_partial_restored");
5141        const data = partial?.discoveryJob?.state === "discovering"
5142          ? await waitForFullCatalogChain(partial, requestMeta, restoredStartedAt)
5143          : partial;
5144        renderCompletedPackage(data, requestMeta, restoredStartedAt, "mvp_active_run_restored");
5145        if (data?.discoveryJob?.state !== "discovering") {
5146          clearActiveRunObserver(run.runId);
5147          try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
5148        }
5149        return true;
5150      } catch {
5151        if (continueScanStatus) {
5152          continueScanStatus.hidden = false;
5153          continueScanStatus.textContent = "Discovery is continuing in the background. Reload later to resume live updates.";
5154        }
5155        return true;
5156      } finally {
5157        loading?.stop();
5158        if (loading) setUrlGenerationLoading(false);
5159        activeInitialScanJobId = "";
5160      }
5161    }
5162    const runs = isSignedIn() ? await loadRunHistory() : (localRun ? [localRun] : []);
5163    const run = runs.find((candidate) => {
5164      const createdAtMs = Date.parse(candidate?.createdAt || "");
5165      const recentlyCreated = Number.isFinite(createdAtMs) && Date.now() - createdAtMs <= 24 * 60 * 60 * 1000;
5166      return candidate?.jobId
5167        && (!candidate?.jobType || candidate.jobType === "mvp-url-run")
5168        && recentlyCreated
5169        && Number(candidate?.productCount || 0) === 0
5170        && ["queued", "running"].includes(String(candidate?.status || "").toLowerCase());
5171    });
5172    if (!run) return false;
5173
5174    const restoredStartedAt = Number.isFinite(Date.parse(run.createdAt)) ? Date.parse(run.createdAt) : Date.now();
5175    const requestMeta = {
5176      sourceType: "url",
5177      host: run.host || "",
5178      url: run.sourceUrl || "",
5179      restored: true,
5180      perf: { requestSubmittedAtMs: restoredStartedAt },
5181    };
5182    activeInitialScanJobId = run.jobId;
5183    trackActiveRunObserver({ runId: run.runId, sourceUrl: run.sourceUrl, host: run.host, state: "restoring" });
5184    markActiveRunRecoveryReady();
5185    setUrlGenerationLoading(true);
5186    setScanActive(true);
5187    setNoReportMessageVisible(false);
5188    const loading = startInitialScanLoading(restoredStartedAt);
5189    loading.updateStatus(run.status);
5190    try {
5191      let data = await waitForMvpUrlJob(
5192        run.jobId,
5193        requestMeta,
5194        restoredStartedAt,
5195        RECOVERY_POLL_MAX_MS,
5196        run.runId,
5197        `/api/jobs/${encodeURIComponent(run.jobId)}`,
5198        "",
5199        isSignedIn(),
5200        (status) => loading.updateStatus(status),
5201        false
5202      );
5203      data = finishDiscoveryFromDurableCoverage(data);
5204      if (data?.discoveryJob?.state === "discovering") {
5205        data = await waitForFullCatalogChain(data, requestMeta, restoredStartedAt);
5206      }
5207      renderCompletedPackage(data, requestMeta, restoredStartedAt, "mvp_active_run_restored");
5208      if (data?.discoveryJob?.state !== "discovering") {
5209        clearActiveRunObserver(run.runId);
5210        try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
5211      }
5212      if (isSignedIn()) await loadRunHistory();
5213      return true;
5214    } catch (err) {
5215      setScanActive(false, { keepIntentHidden: true });
5216      clearActiveRunObserver(run.runId);
5217      const deliveryCode = ["result_unavailable", "status_read_failed", "session_expired", "result_access_de
5217nied"].includes(err?.code)
5218        ? err.code
5219        : "";
5220      setResultSourceLabel(requestMeta);
5221      if (deliveryCode) {
5222        if (stateTitle) stateTitle.textContent = "Import package could not be loaded";
5223        if (stateMessage) stateMessage.textContent = err?.message || "Extraction may have completed. Your run remains available under My runs.";
5224      } else {
5225        setStateCopy("failure");
5226        try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
5227        if (stateMessage && err?.message) stateMessage.textContent = err.message;
5228      }
5229      renderTopIssues([err?.message || "The scan failed before the package was completed."]);
5230      if (successPanel) successPanel.style.display = "grid";
5231      setNoReportMessageVisible(false);
5232      hideDownloadLinks();
5233      setDetails(`Restored URL scan failed.\n${err?.message || err}`);
5234      return false;
5235    } finally {
5236      loading.stop();
5237      setUrlGenerationLoading(false);
5238      activeInitialScanJobId = "";
5239    }
5240  };
5241
5242  const unusedRestoreActiveDiscovery = async () => {
5243    if (!isSignedIn() || activeDiscoveryJobId) return false;
5244    const runs = await loadRunHistory();
5245    const run = runs.find((candidate) => candidate?.jobId
5246      && candidate?.jobType === "mvp-continue-scan"
5247      && ["queued", "running"].includes(String(candidate?.status || "").toLowerCase()));
5248    if (!run) return false;
5249    activeDiscoveryJobId = run.jobId;
5250    trackActiveRunObserver({ runId: run.runId, sourceUrl: run.sourceUrl, host: run.host, state: "restoring" });
5251    markActiveRunRecoveryReady();
5252    const startedAt = Number.isFinite(Date.parse(run.updatedAt || run.createdAt)) ? Date.parse(run.updatedAt || run.createdAt) : Date.now();
5253    let stopLoading = null;
5254    try {
5255      const partialResponse = await authenticatedFetch(`/api/mvp/runs/${encodeURIComponent(run.runId)}/result`, {
5256        credentials: "same-origin",
5257      }, { endpointType: "run_result", runId: run.runId, jobId: run.jobId });
5258      const partial = finishDiscoveryFromDurableCoverage(await parseResponsePayload(partialResponse));
5259      if (!partialResponse.ok || partial?.ok !== true) throw responseError(partialResponse, partial, "The current discovery run could not be restored.", { endpointType: "run_result", runId: run.runId, jobId: run.jobId });
5260      renderCompletedPackage(partial, { sourceType: "url", url: run.sourceUrl, host: run.host, restored: true }, startedAt, "mvp_discovery_partial_restored");
5261      if (partial?.discoveryJob?.state === "discovering") {
5262        stopLoading = startContinueScanLoading();
5263        if (continueScanLoadingStatus) continueScanLoadingStatus.textContent = "Discovering full catalog";
5264      }
5265      const result = partial?.discoveryJob?.state === "discovering"
5266        ? await waitForFullCatalogChain(partial, { sourceType: "url", url: run.sourceUrl, host: run.host, restored: true, perf: {} }, startedAt)
5267        : partial;
5268      renderCompletedPackage(result, { sourceType: "url", url: run.sourceUrl, host: run.host, restored: true }, startedAt, "mvp_discovery_restored");
5269      if (result?.discoveryJob?.state !== "discovering") {
5270        clearActiveRunObserver(run.runId);
5271        try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
5272      }
5273      return true;
5274    } catch (error) {
5275      clearActiveRunObserver(run.runId);
5276      if (continueScanStatus) {
5277        continueScanStatus.hidden = false;
5278        continueScanStatus.textContent = /cancel/i.test(String(error?.message || "")) ? "Cancelled" : `Failed: ${error?.message || "Discovery failed."}`;
5279      }
5280      return false;
5281    } finally {
5282      activeDiscoveryJobId = "";
5283      stopLoading?.();
5284    }
5285  };
5286
5287  const recoveryRequestMeta = (run) => ({
5288    sourceType: "url",
5289    url: run?.sourceUrl || "",
5290    host: run?.host || "",
5291    restored: true,
5292    perf: {},
5293  });
5294
5295  const hideRecoveryChoice = () => {
5296    if (recoveryPanel) recoveryPanel.hidden = true;
5297    if (recoveryStatus) { recoveryStatus.hidden = true; recoveryStatus.textContent = ""; }
5298    recoveryCandidate = null;
5299  };
5300
5301  const showRecoveryChoice = (run, durableResult = null) => {
5302    recoveryCandidate = { ...run, durableResult };
5303    const host = String(run?.host || durableResult?.customerSummary?.supplier_context?.host || "this site");
5304    if (recoveryTitle) recoveryTitle.textContent = `You have an unfinished extraction for ${host}`;
5305    const jobState = String(run?.status || "").toLowerCase();
5306    const discovery = durableResult?.discoveryJob || run?.discoveryJob || {};
5307    const pages = Number(discovery?.pagesAttempted || durableResult?.customerSummary?.discovery_coverage?.pages_attempted || 0);
5308    const products = Number(discovery?.productsFound || run?.productC
5308ount || durableResult?.summary?.productsFound || 0);
5309    if (recoveryProgress) {
5310      recoveryProgress.textContent = [
5311        ["queued", "running"].includes(jobState) || discovery?.state === "discovering" ? "Currently running in the background." : "Saved progress is ready to continue.",
5312        pages > 0 ? `${pages} pages checked.` : "",
5313        products > 0 ? `${products} products found.` : "",
5314      ].filter(Boolean).join(" ");
5315    }
5316    if (recoveryPanel) recoveryPanel.hidden = false;
5317  };
5318
5319  const reconnectSavedExtraction = async (run, durableResult = null) => {
5320    const startedAt = Date.now();
5321    const requestMeta = { ...recoveryRequestMeta(run), foregroundGeneration: ++foregroundGeneration };
5322    const jobId = String(durableResult?.discoveryJob?.currentJobId || run?.jobId || "");
5323    if (!jobId) throw mvpDeliveryError("recovery_job_missing", "The saved extraction is no longer active.");
5324
5325    setScanActive(true);
5326    setUrlGenerationLoading(true);
5327    setNoReportMessageVisible(false);
5328    const loading = startInitialScanLoading(startedAt);
5329    loading.updateStatus("recovering");
5330    try {
5331      // The job endpoint, rather than the browser's saved status, is the authority
5332      // for deciding whether this run is still active.
5333      trackActiveRunObserver({ runId: run.runId, sourceUrl: run.sourceUrl, host: run.host, state: "observing" });
5334      let data = await waitForMvpUrlJob(jobId, requestMeta, startedAt, RECOVERY_POLL_MAX_MS, run.runId,
5335        `/api/jobs/${encodeURIComponent(jobId)}`, "", isSignedIn(), (status) => loading.updateStatus(status), false);
5336      data = finishDiscoveryFromDurableCoverage(data);
5337      if (data?.discoveryJob?.state === "discovering") data = await waitForFullCatalogChain(data, requestMeta, startedAt);
5338      assertForeground(requestMeta);
5339      renderCompletedPackage(data, requestMeta, startedAt, "mvp_recovery_continued");
5340      hideRecoveryChoice();
5341      clearActiveRunObserver(run.runId);
5342      try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
5343      setScanActive(false);
5344      return true;
5345    } finally {
5346      loading.stop();
5347      setUrlGenerationLoading(false);
5348    }
5349  };
5350
5351  const checkForRecoveryChoice = async () => {
5352    const generation = foregroundGeneration;
5353    let localRun = null;
5354    try { localRun = JSON.parse(window.localStorage?.getItem(ACTIVE_DISCOVERY_STORAGE_KEY) || "null"); } catch {}
5355    const runs = isSignedIn() ? await loadRunHistory() : (localRun ? [localRun] : []);
5356    const run = runs.find((candidate) => {
5357      const status = String(candidate?.status || "").toLowerCase();
5358      const discoveryState = String(candidate?.discoveryJob?.state || "").toLowerCase();
5359      return candidate?.runId && ((candidate === localRun && candidate?.jobId) || ["queued", "running", "failed", "cancelled", "interrupted"].includes(status)
5360        || ["discovering", "stopped_at_limit", "cancelled", "failed", "interrupted"].includes(discoveryState));
5361    });
5362    if (!run) return false;
5363
5364    trackActiveRunObserver({ runId: run.runId, sourceUrl: run.sourceUrl, host: run.host, state: "restoring" });
5365    let durable = null;
5366    const recoveryController = new AbortController();
5367    const recoveryTimeout = setTimeout(() => recoveryController.abort(), 10_000);
5368    try {
5369      const resultFetch = isSignedIn() ? authenticatedFetch : (url, options) => fetch(url, options);
5370      const response = await resultFetch(`/api/mvp/runs/${encodeURIComponent(run.runId)}/result`, {
5371        credentials: "same-origin",
5372        signal: recoveryController.signal,
5373      }, { endpointType: "run_result", runId: run.runId, jobId: run.jobId || "" });
5374      const payload = finishDiscoveryFromDurableCoverage(await parseResponsePayload(response));
5375      if (response.ok && payload?.ok === true) durable = payload;
5376      if (response.status === 404) {
5377        clearActiveRunObserver(run.runId);
5378        try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
5379        return false;
5380      }
5381    } catch {} finally {
5382      clearTimeout(recoveryTimeout);
5383    }
5384
5385    if (generation !== foregroundGeneration) return false;
5386    const durableState = String(durable?.discoveryJob?.state || "").toLowerCase();
5387    const historyStatus = String(run?.status || "").toLowerCase();
5388    const completed = durable && (durableState === "complete"
5389      || (!durable?.discoveryJob && ["success", "completed"].includes(String(durable?.status || "").toLowerCase())));
5390    if (completed) {
5391      clearActiveRunObserver(run.runId);
5392      try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
5393      renderCompletedPackage(durable, recoveryRequestMeta(run), Date.now(), "mvp_recovery_completed");
5394      return true;
5395    }
5396    const durableActive = ["discovering", "queued", "running"].includes(durableState);
5397    const savedActive = ["queued", "running"].includes(historyStatus) || (run === localRun && run.jobId && !historyStatus);
5398    if (durableActive || savedActive) {
5399      try {
5400        await reconnectSavedExtraction(run, durable);
5401        return true;
5402      } catch (error) {
5403        if (generation !== foregroundGeneration - 1) return false;
5404        setScanActive(false);
5405        clearActiveRunObserver(run.runId);
5406      }
5407    } else {
5408      clearActiveRunObserver(run.runId);
5409    }
5410    showRecoveryChoice(run, durable);
5411    return true;
5412  };
5413
5414  recoveryContinue?.addEventListener("click", async () => {
5415    const run = recoveryCandidate;
5416    if (!run) return;
5417    recoveryContinue.disabled = true;
5418    recoveryStartNew.disabled = true;
5419    setScanActive(true);
5420    if (recoveryStatus) { recoveryStatus.hidden = false; recoveryStatus.textContent = "Connecting to the saved extraction…"; }
5421    const startedAt = Date.now();
5422    const requestMeta = { ...recoveryRequestMeta(run), foregroundGeneration: ++foregroundGeneration };
5423    try {
5424      let data = run.durableResult;
5425      const active = ["queued", "running"].includes(String(run.status || "").toLowerCase());
5426      if (active && run.jobId) {
5427        trackActiveRunObserver({ runId: run.runId, sourceUrl: run.sourceUrl, host: run.host, state: "observing" });
5428        data = await waitForMvpUrlJob(run.jobId, requestMeta, startedAt, RECOVERY_POLL_MAX_MS, run.runId,
5429          `/api/jobs/${encodeURIComponent(run.jobId)}`, "", isSignedIn(), (status) => {
5430            assertForeground(requestMeta);
5431            if (recoveryStatus) recoveryStatus.textContent = `Extraction ${status || "running"}…`;
5432          }, false);
5433      } else {
5434        const response = await authenticatedFetch(`/api/mvp/runs/${encodeURIComponent(run.runId)}/continue-scan`, {
5435          method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
5436        }, { endpointType: "continue_scan", runId: run.runId });
5437        const queued = await parseResponsePayload(response);
5438        if (!response.ok || queued?.ok !== true) throw responseError(response, queued, "The saved extraction could not be continued.");
5439        if (queued.jobId) data = await waitForMvpUrlJob(queued.jobId, requestMeta, startedAt, RECOVERY_POLL_MAX_MS,
5440          run.runId, `/api/jobs/${encodeURIComponent(queued.jobId)}`, queued.requestId || "", isSignedIn(), null, false);
5441        else data = queued;
5442      }
5443      assertForeground(requestMeta);
5444      data = finishDiscoveryFromDurableCoverage(data);
5445      if (data?.discoveryJob?.state === "discovering") data = await waitForFullCatalogChain(data, requestMeta, startedAt);
5446      assertForeground(requestMeta);
5447      renderCompletedPackage(data, requestMeta, startedAt, "mvp_recovery_continued");
5448      hideRecoveryChoice();
5449      clearActiveRunObserver(run.runId);
5450    } catch (error) {
5451      if (!isForeground(requestMeta)) return;
5452      setScanActive(false, { keepIntentHidden: true });
5453      clearActiveRunObserver(run.runId);
5454      if (recoveryStatus) recoveryStatus.textContent = error?.code === "poll_timeout"
5455        ? "Live updates stopped. The extraction may still be running in the background."
5456        : (error?.message || "The saved extraction could not be continued.");
5457    } finally {
5458      if (!isForeground(requestMeta)) return;
5459      recoveryContinue.disabled = false;
5460      recoveryStartNew.disabled = false;
5461    }
5462  });
5463
5464  recoveryStartNew?.addEventListener("click", () => {
5465    if (!recoveryCandidate || scanActive) return;
5466    clearActiveRunObserver(recoveryCandidate.runId);
5467    try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
5468    hideRecoveryChoice();
5469    setScanActive(false);
5470    if (urlInput) urlInput.value = "";
5471    urlInput?.focus?.();
5472  });
5473
5474  const openLinkedDiscoveryResult = async () => {
5475    let runId = "";
5476    let resume = "";
5477    let expires = "";
5478    try {
5479      const params = new URL(window.location.href).searchParams;
5480      runId = String(params.get("run") || ""); resume = String(params.get("resume") || ""); expires = String(params.get("expires") || "");
5481    } catch {}
5482    if (!runId || !/^[a-z0-9._-]+$/i.test(runId)) return false;
5483    if (!isSignedIn() && resume && expires) {
5484      const response = await fetch(`/api/mvp/runs/${encodeURIComponent(runId)}/result?resume=${encodeURIComponent(resume)}&expires=${encodeURIComponent(expires)}`, { credentials: "same-origin" });
5485      const data = await parseResponsePayload(response);
5486      if (!response.ok || !data?.ok) return false;
5487      renderCompletedPackage(data, { sourceType: "url", url: data?.customerSummary?.supplier_context?.source_url || "", host: data?.customerSummary?.supplier_context?.host || "" }, Date.now(), "mvp_discovery_email_opened");
5488      return true;
5489    }
5490    if (!isSignedIn()) return false;
5491    const runs = runHistoryLoaded ? cachedRunHistory : await loadRunHistory();
5492    const run = runs.find((candidate) => candidate?.runId === runId);
5493    if (!run || run.canOpenResult !== true) return false;
5494    return openHistoryRun(run, null);
5495  };
5496
5497  const startContinueScanLoading = () => {
5498    continuationScanActive = true;
5499    const globalStatus = E("mvpStatus");
5500    if (globalStatus) globalStatus.hidden = true;
5501    if (continueScanLoading) continueScanLoading.hidden = false;
5502    if (continueScanProgress) continueScanProgress.hidden = false;
5503    if (datasetActions) datasetActions.hidden = true;
5504    if (stopDiscoveryButton) {
5505      stopDiscoveryButton.hidden = false;
5506      stopDiscoveryButton.textContent = "Stop discovery";
5507    }
5508    if (currentDatasetState) {
5509      currentDatasetState.textContent = "Current dataset: Partial";
5510      currentDatasetState.hidden = false;
5511    }
5512    if (lastMvpResultViewModel) {
5513      setText("mvpContinueProductsCollected", lastMvpResultViewModel.productsCollected);
5514      setText("mvpContinuePagesScanned", lastMvpResultViewModel.pagesScanned);
5515      setText("mvpContinuePagesRemaining", lastMvpResultViewModel.pagesRemaining);
5516      setScanProgress(
5517        lastMvpResultViewModel.pagesScanned,
5518        lastMvpResultViewModel.pagesScanned + lastMvpResultViewModel.pagesRemaining,
5519      );
5520    }
5521    if (continueScanLoadingStatus) continueScanLoadingStatus.textContent = "Requesting the next catalog scan…";
5522    return () => {
5523      continuationScanActive = false;
5524      if (continueScanLoading) continueScanLoading.hidden = true;
5525      if (continueScanProgress) continueScanProgress.hidden = true;
5526      if (currentDatasetState) currentDatasetState.hidden = true;
5527      if (lastMvpResultViewModel && lastMvpResult) renderMvpResultViewModel(lastMvpResultViewModel, lastMvpResult);
5528      const hasIdleDiscoveryActions = lastMvpResultViewModel?.discovery?.enabled === true
5529        && lastMvpResultViewModel.pagesRemaining > 0
5530        && entitlementState().canContinueScan
5531        && resultWorkflowState?.stopDiscovery !== true;
5532      if (datasetActions) datasetActions.hidden = !hasIdleDiscoveryActions;
5533    };
5534  };
5535
5536  E("mvpDiscoveryClearProblemFilter")?.addEventListener("click", () => {
5537    if (!lastDiscoveryModel) return;
5538    renderDiscoveryMap(lastDiscoveryModel, { problemOnly: false, filter: "scanned" });
5539  });
5540
5541  document.querySelectorAll("[data-mvp-page-filter]").forEach((button) => {
5542    button.addEventListener("click", () => {
5543      if (!lastDiscoveryModel) return;
5544      const filter = button.dataset.mvpPageFilter || "needs_review";
5545      renderDiscoveryMap(lastDiscoveryModel, { problemOnly: filter === "needs_review", filter });
5546    });
5547  });
5548
5549  const openCurrentFiles = () => {
5550    if (freeFilesPanel && "open" in freeFilesPanel) freeFilesPanel.open = true;
5551    if (technicalSection && "open" in technicalSection) technicalSection.open = true;
5552    if (productReviewSection && "open" in productReviewSection) productReviewSection.open = true;
5553    if (currentDraftExport && "open" in currentDraftExport) currentDraftExport.open = true;
5554    freeFilesPanel?.scrollIntoView?.({ behavior: "smooth", block: "start" });
5555  };
5556
5557  E("mvpExportCurrentProducts")?.addEventListener("click", () => {
5558    const csvDownload = E("mvpCsvDownload");
5559    if (csvDownload?.dataset.mvpDownloadPath) return csvDownload.click();
5560    if (!isSignedIn()) return focusAccountFromGate("signin", "download");
5561    openCurrentFiles();
5562  });
5563  E("mvpViewTechnicalDetails")?.addEventListener("click", () => {
5564    if (technicalSection && "open" in technicalSection) technicalSection.open = true;
5565    technicalSection?.scrollIntoView?.({ behavior: "smooth", block: "start" });
5566  });
5567  E("mvpPrimaryRecommendation")?.addEventListener("click", () => {
5568    const action = E("mvpPrimaryRecommendation")?.dataset.action;
5569    if (action === "generate_files") return openCurrentFiles();
5570    if (action === "retry") return runButton?.click();
5571    const discoveryMap = E("mvpDiscoveryMap");
5572    if (lastDiscoveryModel && discoveryMap && discoveryProblemPages(lastDiscoveryModel).length > 0) {
5573      renderDiscoveryMap(lastDiscoveryModel, { problemOnly: true });
5574      discoveryMap.open = true;
5575      discoveryMap.scrollIntoView?.({ behavior: "smooth", block: "start" });
5576      return;
5577    }
5578    E("mvpReviewDetails")?.scrollIntoView?.({ behavior: "smooth", block: "start" });
5579  });
5580
5581  continueScanButton?.addEventListener("click", async () => {
5582    if (!isSignedIn()) {
5583      focusAccountFromGate("signin", "continue");
5584      return;
5585    }
5586    if (!lastMvpResult?.runId || !lastMvpResult?.host || !entitlementState().canContinueScan) return;
5587    const originalLabel = continueScanButton.textContent;
5588    const startedAt = Date.now();
5589    continueScanButton.disabled = true;
5590    continueScanButton.textContent = "Scanning…";
5591    if (continueScanStatus) {
5592      continueScanStatus.hidden = true;
5593      continueScanStatus.textContent = "";
5594    }
5595    const stopContinueScanLoading = startContinueScanLoading();
5596    try {
5597      const requestMeta = { sourceType: "url", url: lastMvpResult.url, host: lastMvpResult.host, perf: { requestSubmittedAtMs: Date.now() } };
5598      const res = await authenticatedFetch(`/api/mvp/runs/${encodeURIComponent(lastMvpResult.runId)}/continue-scan`, {
5599        method: "POST",
5600        headers: { "Content-Type": "application/json" },
5601        credentials: "same-origin",
5602        body: JSON.stringify({ host: lastMvpResult.host, ...discoveryNotificationPayload() }),
5603      }, { endpointType: "continue_scan", runId: lastMvpResult.runId });
5604      let data = await parseResponsePayload(res);
5605      if (data?.notificationError && discoveryNotificationStatus) {
5606        discoveryNotificationStatus.textContent = data.notificationError;
5607        discoveryNotificationStatus.hidden = false;
5608      }
5609      requestMeta.perf.jobAcceptedMs = Date.now() - requestMeta.perf.requestSubmittedAtMs;
5610      if (res.status === 402) {
5611        renderUsage(data?.usage);
5612        setCheckoutMessage(AUTHENTICATED_EXHAUSTED_MESSAGE);
5613      }
5614      if (!res.ok || !data?.ok) throw responseError(res, data, "Continue scan failed.", { endpointType: "continue_scan", runId: lastMvpResult.runId });
5615      if (res.status === 202 && data?.jobId) {
5616        activeDiscoveryJobId = data.jobId;
5617        data = await waitForMvpUrlJob(
5618          data.jobId,
5619          requestMeta,
5620          startedAt,
5621          35 * 60 * 1000,
5622          lastMvpResult.runId,
5623          data.statusUrl || "",
5624          data.requestId || lastMvpResult.requestId || "",
5625          isSignedIn(),
5626          (status) => {
5627            if (stateTitle) stateTitle.textContent = status === "queued"
5628              ? "Catalog scan queued"
5629              : "Scanning catalog";
5630            if (!continueScanLoadingStatus) return;
5631            continueScanLoadingStatus.textContent = status === "queued"
5632              ? "Waiting for the next scan batch to start…"
5633              : status === "completed" || status === "succeeded"
5634                ? "Finalizing the updated dataset…"
5635                : "Scanning the next supplier pages…";
5636          },
5637          false
5638        );
5639        data = await waitForFullCatalogChain(data, requestMeta, startedAt);
5640      }
5641      stopContinueScanLoading();
5642      renderUsage(data?.usage);
5643      renderCompletedPackage(data, requestMeta, startedAt, "mvp_continue_scan_completed");
5644      if (isSignedIn()) await loadRunHistory();
5645      if (continueScanStatus) continueScanStatus.textContent = "Scan continued. The current package has been updated.";
5646    } catch (err) {
5647      stopContinueScanLoading();
5648      if (continueScanStatus) {
5649        continueScanStatus.hidden = false;
5650        continueScanStatus.textContent = err?.message || "Continue scan failed.";
5651      }
5652      warn("mvp_continue_scan_failed", { runId: lastMvpResult?.runId || null, error: err?.message || String(err) });
5653    } finally {
5654      activeDiscoveryJobId = "";
5655      stopContinueScanLoading();
5656      continueScanButton.disabled = false;
5657      continueScanButton.textContent = originalLabel || "Continue building dataset";
5658    }
5659  });
5660
5661  enrichProductsButton?.addEventListener("click", async () => {
5662    if (!isSignedIn()) {
5663      focusAccountFromGate("signin", "enrich");
5664      return;
5665    }
5666    if (enrichProductsButton.dataset.requiresRuns === "true") {
5667      focusAccountFromGate("signin", "enrich");
5668      return;
5669    }
5670    if (!lastMvpResult?.runId || !lastMvpResult?.host || enrichProductsButton.disabled || enrichmentInProgress) return;
5671    const originalLabel = enrichProductsButton.textContent;
5672    const startedAt = Date.now();
5673    enrichmentInProgress = true;
5674    enrichProductsButton.disabled = true;
5675    enrichProductsButton.textContent = "Enriching products…";
5676    setEnrichmentState("starting", "Submitting the enrichment job.");
5677    try {
5678      const res = await authenticatedFetch(`/api/mvp/runs/${encodeURIComponent(lastMvpResult.runId)}/enrich-products`, {
5679        method: "POST",
5680        headers: { "Content-Type": "application/json" },
5681        credentials: "same-origin",
5682        body: JSON.stringify({ host: lastMvpResult.host }),
5683      }, { endpointType: "product_enrichment", runId: lastMvpResult.runId });
5684      let data = await parseResponsePayload(res);
5685      if (!res.ok || !data?.ok) throw responseError(res, data, "Product enrichment failed.", { endpointType: "product_enrichment", runId: lastMvpResult.runId });
5686      if (res.status === 202 && data?.jobId) {
5687        setEnrichmentState("queued", "The enrichment job is queued and waiting to start.");
5688        data = await waitForMvpUrlJob(
5689          data.jobId,
5690          { sourceType: "url", url: lastMvpResult.url, host: lastMvpResult.host },
5691          startedAt,
5692          35 * 60 * 1000,
5693          lastMvpResult.runId,
5694          data.statusUrl || "",
5695          data.requestId || lastMvpResult.requestId || "",
5696          true,
5697          (status) => {
5698            if (status === "queued") {
5699              setEnrichmentState("queued", "The enrichment job is queued and waiting to start.");
5700            } else {
5701              setEnrichmentState(
5702                "processing",
5703                status === "completed" || status === "succeeded"
5704                  ? "Product-page processing finished. Updating the product dataset."
5705                  : "Visiting eligible product pages and collecting available product data."
5706              );
5707            }
5708          }
5709        );
5710      }
5711      renderUsage(data?.usage);
5712      renderCompletedPackage(data, { sourceType: "url", url: lastMvpResult.url, host: lastMvpResult.host }, startedAt, "mvp_product_enrichment_completed");
5713      enrichmentInProgress = false;
5714      if (!data?.enrichment && !data?.summary?.enrichment && !data?.customerSummary?.enrichment) {
5715        setEnrichmentState("completed", "Enrichment completed and the product dataset was updated.");
5716      }
5717    } catch (err) {
5718      enrichmentInProgress = false;
5719      enrichProductsButton.hidden = false;
5720      enrichProductsButton.disabled = false;
5721      enrichProductsButton.textContent = originalLabel || "Enrich products";
5722      setEnrichmentState("failed", err?.message || "Product enrichment failed.");
5723      warn("mvp_product_enrichment_failed", { runId: lastMvpResult?.runId || null, error: err?.message || String(err) });
5724    } finally {
5725      if (enrichProductsButton.textContent === "Enriching products…") {
5726        enrichProductsButton.disabled = false;
5727        enrichProductsButton.textContent = originalLabel || "Enrich product data";
5728      }
5729    }
5730  });
5731
5732  stopDiscoveryButton?.addEventListener("click", async () => {
5733    if (!activeDiscoveryJobId) return openCurrentFiles();
5734    stopDiscoveryButton.disabled = true;
5735    try {
5736      const response = await authenticatedFetch(`/api/jobs/${encodeURIComponent(activeDiscoveryJobId)}/cancel`, {
5737        method: "POST", credentials: "same-origin",
5738      }, { endpointType: "continue_scan_cancel", runId: lastMvpResult?.runId || "", jobId: activeDiscoveryJobId });
5739      const data = await parseResponsePayload(response);
5740      if (!response.ok || data?.ok === false) throw new Error(data?.error || "Discovery could not be stopped.");
5741      if (continueScanStatus) {
5742        continueScanStatus.textContent = "Cancelled";
5743        continueScanStatus.hidden = false;
5744      }
5745    } catch (error) {
5746      if (continueScanStatus) {
5747        continueScanStatus.textContent = error?.message || "Discovery could not be stopped.";
5748        continueScanStatus.hidden = false;
5749      }
5750    } finally {
5751      stopDiscoveryButton.disabled = false;
5752    }
5753  });
5754
5755  E("mvpDownloadCurrentFiles")?.addEventListener("click", openCurrentFiles);
5756  const focusAccountFromGate = (mode, action = "continue") => {
5757    if (blockAuthDuringScan()) return;
5758    if (isSignedIn()) {
5759      setAddRunsExpanded(true, { remember: true });
5760    } else {
5761      setAuthMode(mode);
5762      const enrichmentAccountMessage = mode === "signup"
5763        ? t("mvp.enrichment.createMessage", null, "Create an account to save this dataset and enrich its product details. Only product enrichment consumes credits.")
5764        : t("mvp.enrichment.signInMessage", null, "Sign in to save this dataset and enrich its product details.");
5765      const storyAccountMessage = mode === "signup"
5766        ? t("mvp.marketing.createMessage", null, "Create an account to create an Instagram Story from this dataset. Only product enrichment consumes credits.")
5767        : t("mvp.marketing.signInMessage", null, "Sign in to create an Instagram Story from this dataset.");
5768      const catalogAccountMessage = mode === "signup"
5769        ? t("mvp.preview.createMessage", null, "Create an account to save and reopen this run.")
5770        : t("mvp.preview.signInMessage", null, "Sign in to save and reopen this run.");
5771      const downloadAccountMessage = mode === "signup"
5772        ? "Create an account to download this generated file. Downloads do not consume credits."
5773        : "Sign in to download this generated file. Downloads do not consume credits.";
5774      setAuthMessage(action === "enrich"
5775        ? enrichmentAccountMessage
5776        : action === "story"
5777          ? storyAccountMessage
5778          : action === "download"
5779            ? downloadAccountMessage
5780          : action === "catalog"
5781            ? catalogAccountMessage
5782          : mode === "signup"
5783            ? t("mvp.next.createMessage", null, "Create an account to save this dataset and continue scanning the remaining catalog pages. Only product enrichment consumes credits.")
5784            : t("mvp.next.signInMessage", null, "Sign in to save and continue this dataset."));
5785    }
5786    openAccountPanel(document.activeElement instanceof HTMLElement ? document.activeElement : null);
5787  };
5788
5789  E("mvpCreateAccountEnrich")?.addEventListener("click", () => {
5790    focusAccountFromGate("signup", "enrich");
5791  });
5792
5793  E("mvpSignInEnrich")?.addEventListener("click", () => {
5794    focusAccountFromGate("signin", "enrich");
5795  });
5796
5797  currentDraftExport?.addEventListener("toggle", () => {
5798    const discoveryInProgress = resultWorkflowState?.discovery?.enabled === true
5799      && resultWorkflowState.discovery.pagesRemaining > 0
5800      && !resultWorkflowState.stopDiscovery;
5801    if (discoveryInProgress && cleanupPanel) cleanupPanel.hidden = !currentDraftExport.open;
5802  });
5803
5804  cleanupRequestLink?.addEventListener("click", async () => {
5805    if (!authenticatedActionAllowed()) return;
5806    if (!lastMvpResult || !hasCleanupArtifacts(lastMvpResult)) {
5807      resetCleanupAction("Generate a report first to unlock this review action.");
5808      return;
5809    }
5810    const originalLabel = cleanupRequestLink.textContent;
5811    cleanupRequestLink.disabled = true;
5812    cleanupRequestLink.textContent = "Generating cleaned Shopify CSV…";
5813    if (cleanupStatus) {
5814      cleanupStatus.textContent = "Generating cleaned Shopify CSV…";
5815      cleanupStatus.hidden = false;
5816    }
5817    if (cleanupDownload) {
5818      cleanupDownload.hidden = true;
5819      cleanupDownload.removeAttribute("href");
5820    }
5821
5822    try {
5823      const res = await authenticatedFetch("/api/mvp/cleanup-ai", {
5824        method: "POST",
5825        headers: { "Content-Type": "application/json" },
5826        body: JSON.stringify({
5827          requestId: lastMvpResult.requestId,
5828          sourceType: lastMvpResult.sourceType,
5829          sourceUrl: lastMvpResult.url,
5830          fileName: lastMvpResult.fileName,
5831          artifacts: lastMvpResult.artifacts,
5832        }),
5833      });
5834      const data = await res.json();
5835      if (!res.ok || !data?.ok) throw new Error(data?.error || "Cleaned CSV generation failed.");
5836      const artifactPath = String(data?.artifact?.cleanedShopifyCsv || "");
5837      if (!artifactPath) throw new Error("Cleaned CSV artifact was not returned.");
5838      if (cleanupDownload) {
5839        const canDownload = isSignedIn();
5840        cleanupDownload.textContent = canDownload ? "Download prepared Shopify CSV" : "Sign in to download";
5841        cleanupDownload.dataset.mvpDownloadPath = canDownload ? artifactPath : "";
5842        cleanupDownload.removeAttribute("href");
5843        cleanupDownload.hidden = false;
5844      }
5845      if (cleanupPreview) cleanupPreview.hidden = false;
5846      const summary = data?.summary || {};
5847      if (cleanupTitle) cleanupTitle.textContent = "Cleaned Shopify CSV ready";
5848      if (cleanupDescription) cleanupDescription.hidden = true;
5849      if (cleanupStatus) {
5850        cleanupStatus.textContent = "";
5851        cleanupStatus.hidden = true;
5852      }
5853      cleanupRequestLink.hidden = true;
5854      log("mvp_cleanup_csv_generated", { requestId: lastMvpResult.requestId, artifactPath, summary });
5855    } catch (err) {
5856      if (cleanupStatus) {
5857        cleanupStatus.textContent = err?.message || "Cleaned CSV generation failed.";
5858        cleanupStatus.hidden = false;
5859      }
5860      warn("mvp_cleanup_csv_failed", { requestId: lastMvpResult?.requestId || null, error: err?.message || String(err) });
5861    } finally {
5862      cleanupRequestLink.textContent = originalLabel || "Prepare Shopify CSV";
5863      cleanupRequestLink.disabled = !hasCleanupArtifacts(lastMvpResult);
5864      renderActionAccessGates();
5865    }
5866  });
5867
5868  cleanupPreview?.addEventListener("click", () => {
5869    if (previewDetails) previewDetails.open = true;
5870    previewDetails?.scrollIntoView?.({ behavior: "smooth", block: "start" });
5871  });
5872
5873  const extractUrlFromHtml = (html) => {
5874    const text = String(html || "");
5875    const patterns = [
5876      /<link[^>]+rel=["'][^"']*canonical[^"']*["'][^>]+href=["']([^"']+)["']/i,
5877      /<meta[^>]+property=["']og:url["'][^>]+content=["']([^"']+)["']/i,
5878      /<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:url["']/i,
5879      /<base[^>]+href=["']([^"']+)["']/i,
5880    ];
5881    for (const pattern of patterns) {
5882      const match = text.match(pattern);
5883      if (match?.[1]) return match[1].trim();
5884    }
5885    return "";
5886  };
5887
5888  const normalizeSubmittedUrl = (value) => {
5889    const trimmed = String(value || "").trim();
5890    if (!trimmed || /^https?:\/\//i.test(trimmed)) return trimmed;
5891    try {
5892      return new URL(`https://${trimmed}`).href;
5893    } catch {
5894      return `https://${trimmed}`;
5895    }
5896  };
5897
5898  const comparableRunUrl = (value) => {
5899    try {
5900      const parsed = new URL(normalizeSubmittedUrl(value));
5901      parsed.hash = "";
5902      return parsed.href;
5903    } catch {
5904      return String(value || "").trim();
5905    }
5906  };
5907
5908  const trackActiveRunObserver = ({ runId, sourceUrl, host, state }) => {
5909    const id = String(runId || "");
5910    if (!id) return;
5911    activeRunObservers.set(id, {
5912      runId: id,
5913      sourceUrl: comparableRunUrl(sourceUrl),
5914      host: String(host || "").toLowerCase(),
5915      state: String(state || "observing"),
5916    });
5917  };
5918
5919  const clearActiveRunObserver = (runId) => {
5920    const id = String(runId || "");
5921    if (id) activeRunObservers.delete(id);
5922  };
5923
5924  const activeRunForUrl = (url) => {
5925    const sourceUrl = comparableRunUrl(url);
5926    return Array.from(activeRunObservers.values()).find((run) => run.sourceUrl === sourceUrl && ["observing", "submitting"].includes(run.state)) || null;
5927  };
5928
5929  const markActiveRunRecoveryReady = () => {
5930    if (!activeRunRecoveryPending) return;
5931    activeRunRecoveryPending = false;
5932    resolveActiveRunRecovery?.();
5933  };
5934
5935  const htmlFileInput = E("mvpHtmlFileInput");
5936  const htmlFileName = E("mvpHtmlFileName");
5937
5938  uploadTab?.addEventListener("click", () => setInputMode("upload"));
5939  urlTab?.addEventListener("click", () => setInputMode("url"));
5940  const focusCsvRecovery = () => {
5941    setInputMode("upload");
5942    uploadPanel?.scrollIntoView?.({ behavior: "smooth", block: "center" });
5943    csvFileInput?.focus?.();
5944  };
5945  const focusHtmlRecovery = () => {
5946    setInputMode("url");
5947    if (fallbackPanel) {
5948      fallbackPanel.hidden = false;
5949      fallbackPanel.open = true;
5950      fallbackPanel.scrollIntoView?.({ behavior: "smooth", block: "center" });
5951    }
5952    htmlFileInput?.focus?.();
5953  };
5954  E("mvpCreateAiJson")?.addEventListener("click", focusCsvRecovery);
5955  E("mvpUseCsvForJson")?.addEventListener("click", focusCsvRecovery);
5956  E("mvpUseHtmlForJson")?.addEventListener("click", focusHtmlRecovery);
5957  E("mvpZeroExtractionUseCsv")?.addEventListener("click", focusCsvRecovery);
5958  E("mvpZeroExtractionUseHtml")?.addEventListener("click", focusHtmlRecovery);
5959  E("mvpZeroExtractionTryAnother")?.addEventListener("click", () => {
5960    setInputMode("url");
5961    urlInput?.scrollIntoView?.({ behavior: "smooth", block: "center" });
5962    urlInput?.focus?.();
5963  });
5964  setInputMode("url");
5965
5966  htmlFileInput?.addEventListener("change", async () => {
5967    const file = htmlFileInput.files?.[0];
5968    if (!file) {
5969      if (htmlFileName) htmlFileName.textContent = "No file selected.";
5970      return;
5971    }
5972    if (htmlFileName) htmlFileName.textContent = `Selected: ${file.name}`;
5973    try {
5974      const text = await file.text();
5975      if (htmlInput) htmlInput.value = text;
5976      if (fallbackPanel) fallbackPanel.open = true;
5977      log("mvp_html_file_loaded", { fileName: file.name, size: text.length });
5978    } catch (err) {
5979      if (htmlFileName) htmlFileName.textContent = `Failed to read file: ${file.name}`;
5980      log("mvp_html_file_load_failed", { fileName: file.name, error: err?.message || String(err) });
5981    }
5982  });
5983
5984  inspectFileButton?.addEventListener("click", async () => {
5985    const statusEl = E("mvpStatus");
5986    const file = csvFileInput?.files?.[0];
5987    if (!file) {
5988      if (statusEl) {
5989        statusEl.hidden = false;
5990        statusEl.textContent = "Upload a CSV file to inspect.";
5991      }
5992      return;
5993    }
5994    if (statusEl) {
5995      statusEl.hidden = false;
5996      statusEl.textContent = "Inspecting supplier CSV and building import files...";
5997    }
5998    setImportQualityMetrics({ productsFound: 0, importReadyProducts: 0, warningCount: 0, blockingIssueCount: 0, blockedProducts: 0 });
5999    setPackageLoading(true, "upload");
6000    if (successPanel) successPanel.style.display = "none";
6001    setNoReportMessageVisible(false);
6002    setResultSourceLabel();
6003    hideDownloadLinks();
6004    resetCleanupAction();
6005    resetPreview();
6006    renderDiscoveryCoverage(null);
6007    renderNextActions([]);
6008    setDetails("CSV inspection started.");
6009    const startedAt = Date.now();
6010    const requestMeta = { sourceType: "upload", fileName: file.name, size: file.size };
6011    log("mvp_file_inspection_started", requestMeta);
6012    try {
6013      const form = new FormData();
6014      form.append("file", file);
6015      const res = await fetch("/api/mvp/inspect-file", {
6016        method: "POST",
6017        body: form,
6018      });
6019      const data = await res.json();
6020      if (!res.ok || !data?.ok) throw new Error(data?.error || "CSV inspection failed.");
6021      renderCompletedPackage(data, requestMeta, startedAt, "mvp_file_inspection_completed");
6022    } catch (err) {
6023      log("mvp_file_inspection_failed", { ...requestMeta, error: err?.message || String(err), elapsedMs: Date.now() - startedAt });
6024      setResultSourceLabel(requestMeta);
6025      setStateCopy("failure");
6026      if (stateMessage) stateMessage.textContent = err?.message || "CSV inspection failed.";
6027      if (issueSummaryTitle) issueSummaryTitle.textContent = "Blocking errors";
6028      setImportQualityMetrics({ productsFound: 0, importReadyProducts: 0, warningCount: 0, blockingIssueCount: 1, blockedProducts: 0 });
6029      if (E("mvpMetricProducts")) E("mvpMetricProducts").textContent = "0";
6030      if (E("mvpMetricImportReady")) E("mvpMetricImportReady").textContent = "0";
6031      if (E("mvpMetricReviewRows")) E("mvpMetricReviewRows").textContent = "0";
6032      if (E("mvpMetricBlocked")) E("mvpMetricBlocked").textContent = "0";
6033      if (E("mvpSummaryMetricImportReady")) E("mvpSummaryMetricImportReady").textContent = "0";
6034      if (E("mvpSummaryMetricReviewRows")) E("mvpSummaryMetricReviewRows").textContent = "0";
6035      if (E("mvpSummaryMetricBlocked")) E("mvpSummaryMetricBlocked").textContent = "0";
6036      if (E("mvpMetricBlockingIssues")) E("mvpMetricBlockingIssues").textContent = "0";
6037      if (E("mvpMetricWarnings")) E("mvpMetricWarnings").textContent = "0";
6038      if (E("mvpMetricRejected")) E("mvpMetricRejected").textContent = "0";
6039      if (E("mvpSummaryMetricWarnings")) E("mvpSummaryMetricWarnings").textContent = "0";
6040      if (E("mvpSummaryMetricRejected")) E("mvpSummaryMetricRejected").textContent = "0";
6041      renderTopIssues([err?.message || "CSV inspection failed."]);
6042      renderDiscoveryCoverage(null);
6043      renderNextActions([]);
6044      if (successPanel) successPanel.style.display = "grid";
6045      setNoReportMessageVisible(false);
6046      hideDownloadLinks();
6047      resetCleanupAction("Generate a report first to unlock this review action.");
6048      if (statusEl) {
6049        statusEl.textContent = "";
6050        statusEl.hidden = true;
6051      }
6052      setDetails(`CSV inspection failed.\n${err?.message || err}`);
6053    } finally {
6054      setPackageLoading(false, "upload");
6055    }
6056  });
6057
6058  runButton?.addEventListener("click", async () => {
6059    if (isSignedIn() && !entitlementState().canStartUrlExtraction) return;
6060    const url = normalizeSubmittedUrl(E("mvpUrlInput")?.value);
6061    const content = htmlInput?.value || "";
6062    const fallbackUsed = Boolean(fallbackPanel?.open);
6063    const sourceType = fallbackUsed ? "html" : "url";
6064    const pastedUrl = extractUrlFromHtml(content);
6065    const statusEl = E("mvpStatus");
6066      const metricProducts = E("mvpMetricProducts");
6067      const metricImportReady = E("mvpMetricImportReady");
6068      const metricReviewRows = E("mvpMetricReviewRows");
6069      const metricBlocked = E("mvpMetricBlocked");
6070      const summaryMetricImportReady = E("mvpSummaryMetricImportReady");
6071      const summaryMetricReviewRows = E("mvpSummaryMetricReviewRows");
6072      const summaryMetricBlocked = E("mvpSummaryMetricBlocked");
6073      const summaryMetricWarnings = E("mvpSummaryMetricWarnings");
6074      const summaryMetricRejected = E("mvpSummaryMetricRejected");
6075      const metricBlockingIssues = E("mvpMetricBlockingIssues");
6076      const metricWarnings = E("mvpMetricWarnings");
6077      const metricRejected = E("mvpMetricRejected");
6078    if (!url) {
6079      if (statusEl) {
6080        statusEl.hidden = false;
6081        statusEl.textContent = "Add a category URL to start.";
6082      }
6083      return;
6084    }
6085    let parsedUrl;
6086    try {
6087      parsedUrl = new URL(url);
6088      if (!/^https?:$/.test(parsedUrl.protocol) || !parsedUrl.hostname) throw new Error("Invalid URL");
6089    } catch {
6090      if (statusEl) {
6091        statusEl.hidden = false;
6092        statusEl.textContent = "Enter a valid HTTP or HTTPS URL.";
6093      }
6094      return;
6095    }
6096    const host = parsedUrl.hostname;
6097    if (fallbackUsed && !content.trim()) {
6098      if (statusEl) {
6099        statusEl.hidden = false;
6100        statusEl.textContent = "Upload or paste the saved HTML file to continue.";
6101      }
6102      return;
6103    }
6104    if (activeRunRecoveryPending) await activeRunRecoveryReady;
6105    const submissionUrlKey = comparableRunUrl(url);
6106    const duplicateRun = activeRunForUrl(url);
6107    if (activeSubmissionUrls.has(submissionUrlKey) || duplicateRun) {
6108      log("mvp_ingest_duplicate_prevented", {
6109        runId: duplicateRun?.runId || null,
6110        host: duplicateRun?.host || host,
6111        url: submissionUrlKey,
6112        observationState: duplicateRun?.state || "submitting",
6113      });
6114      return;
6115    }
6116    activeSubmissionUrls.add(submissionUrlKey);
6117    const generation = ++foregroundGeneration;
6118    // Job pointers belong to the foreground, unlike the URL duplicate registry.
6119    activeInitialScanJobId = "";
6120    activeDiscoveryJobId = "";
6121    hideRecoveryChoice();
6122    if (fallbackUsed && !pastedUrl) {
6123      log("mvp_html_url_missing", { hasUrl: Boolean(url), contentSize: String(content || "").length });
6124    } else if (fallbackUsed) {
6125      try {
6126        const enteredHost = parsedUrl.hostname.toLowerCase();
6127        const pastedHost = new URL(pastedUrl, url).hostname.toLowerCase();
6128        if (enteredHost !== pastedHost) {
6129          log("mvp_html_url_host_mismatch", { enteredHost, pastedHost, pastedUrl });
6130        }
6131      } catch {}
6132    }
6133    if (statusEl) {
6134      statusEl.textContent = "";
6135      statusEl.hidden = true;
6136    }
6137    setUrlGenerationLoading(true);
6138    setScanActive(true);
6139    setImportQualityMetrics({ productsFound: 0, importReadyProducts: 0, warningCount: 0, blockingIssueCount: 0, blockedProducts: 0 });
6140    if (successPanel && sourceType !== "url") successPanel.style.display = "none";
6141    setNoReportMessageVisible(false);
6142    setResultSourceLabel();
6143    hideDownloadLinks();
6144    resetCleanupAction();
6145    resetPreview();
6146    renderDiscoveryCoverage(null);
6147    renderNextActions([]);
6148    setDetails("Product import package generation started.");
6149    const startedAt = Date.now();
6150    const requestMeta = { foregroundGeneration: generation, sourceType, host, url, hasUrl: Boolean(url), contentSize: fallbackUsed ? String(content || "").length : 0, perf: { requestSubmittedAtMs: Date.now() } };
6151    activePreScanLoading?.stop();
6152    const initialScanLoading = sourceType === "url" ? startInitialScanLoading(startedAt) : null;
6153    activePreScanLoading = initialScanLoading;
6154    let submittedRunId = "";
6155    log("mvp_ingest_started", requestMeta);
6156    try {
6157      const body = fallbackUsed
6158        ? { sourceType, host, url, content }
6159        : { sourceType, host, url };
6160      Object.assign(body, discoveryNotificationPayload());
6161      const ingestOptions = {
6162        method: "POST",
6163        headers: { "Content-Type": "application/json" },
6164        body: JSON.stringify(body),
6165      };
6166      const res = isSignedIn()
6167        ? await authenticatedFetch("/api/mvp/ingest", ingestOptions, { endpointType: "queue" })
6168        : await fetch("/api/mvp/ingest", ingestOptions);
6169      let data = await parseResponsePayload(res);
6170      submittedRunId = String(data?.runId || "");
6171      if (!isForeground(requestMeta)) {
6172        if (res.status === 202 && data?.jobId) trackActiveRunObserver({ runId: data.runId, sourceUrl: url, host, state: "observing" });
6173        return;
6174      }
6175      if (data?.notificationError && discoveryNotificationStatus) {
6176        discoveryNotificationStatus.textContent = data.notificationError;
6177        discoveryNotificationStatus.hidden = false;
6178      }
6179      requestMeta.perf.jobAcceptedMs = Date.now() - requestMeta.perf.requestSubmittedAtMs;
6180      if (res.status === 202 && data?.jobId) {
6181        renderUsage(data?.usage);
6182        activeInitialScanJobId = data.jobId;
6183        trackActiveRunObserver({ runId: data.runId, sourceUrl: url, host, state: "submitting" });
6184        try {
6185          window.localStorage?.setItem(ACTIVE_DISCOVERY_STORAGE_KEY, JSON.stringify({
6186            jobId: data.jobId, runId: data.runId, status: data.status || "queued", createdAt: new Date().toISOString(),
6187            updatedAt: new Date().toISOString(), sourceUrl: url, host, productCount: 0, jobType: "mvp-url-run",
6188          }));
6189        } catch {}
6190        initialScanLoading.updateStatus(data.status || "queued");
6191        log("mvp_ingest_queued", { ...requestMeta, requestId: data.requestId || null, runId: data.runId || null, jobId: data.jobId, elapsedMs: Date.now() - startedAt });
6192        setDetails(JSON.stringify({
6193          requestId: data.requestId || null,
6194          runId: data.runId || null,
6195          jobId: data.jobId,
6196          statusUrl: data.statusUrl || `/api/jobs/${data.jobId}`,
6197          status: data.status || "queued",
6198        }, null, 2));
6199        data = await waitForMvpUrlJob(
6200          data.jobId,
6201          requestMeta,
6202          startedAt,
6203          20 * 60 * 1000,
6204          data.runId,
6205          data.statusUrl || "",
6206          data.requestId || "",
6207          data?.auth?.authenticated === true || data?.usage?.authenticated === true || isSignedIn(),
6208          (status, job, payload) => {
6209            assertForeground(requestMeta);
6210            initialScanLoading?.updateStatus(status);
6211            initialScanLoading?.updateDiscovery(job, payload);
6212          },
6213          false
6214        );
6215        data = await waitForFullCatalogChain(data, requestMeta, startedAt);
6216      }
6217      assertForeground(requestMeta);
6218      if (!res.ok || !data?.ok) throw new Error(data?.error || "Product extraction failed.");
6219      renderUsage(data?.usage);
6220      if (data?.discoveryJob?.state !== "discovering") {
6221        clearActiveRunObserver(submittedRunId);
6222        try { window.localStorage?.removeItem(ACTIVE_DISCOVERY_STORAGE_KEY); } catch {}
6223      }
6224      if (data?.status === "needs_capture") {
6225        const friendly = FRIENDLY_REASON[data?.reasonCode] || data?.message || STATE_COPY.capture.message;
6226        setResultSourceLabel(requestMeta);
6227        setStateCopy("capture");
6228        if (fallbackPanel) fallbackPanel.open = true;
6229        if (stateMessage) stateMessage.textContent = friendly;
6230        if (stateHint) {
6231          stateHint.textContent = STATE_COPY.capture.hint;
6232          stateHint.hidden = false;
6233        }
6234        const hasCatalogSources = Boolean((data?.customerSummary?.discovery_coverage || data?.discoveryCoverage)?.catalog_files?.length);
6235        const captureViewModel = deriveMvpResultViewModel(data, requestMeta);
6236        const captureProductsFound = hasCatalogSources ? captureViewModel.productsCollected : Number(data?.summary?.productsFound || 0);
6237        const captureImportReady = Number(data?.summary?.importReadyProducts || 0);
6238        const captureBlocked = Number(data?.summary?.blockedProducts || 0);
6239        const captureBlockingIssues = Number(data?.summary?.blockingIssueCount || 0);
6240        const captureWarnings = Number(data?.summary?.warningCount || 0);
6241        const captureRejected = Number(data?.summary?.rejectedRows || 0);
6242        setImportQualityMetrics({
6243          productsFound: captureProductsFound,
6244          importReadyProducts: captureImportReady,
6245          warningCount: captureWarnings,
6246          blockingIssueCount: captureBlockingIssues,
6247          blockedProducts: captureBlocked,
6248        });
6249        if (metricProducts) metricProducts.textContent = String(captureProductsFound);
6250        if (metricImportReady) metricImportReady.textContent = String(Number(data?.summary?.importReadyProducts || 0));
6251        if (metricReviewRows) metricReviewRows.textContent = "0";
6252        if (metricBlocked) metricBlocked.textContent = String(Number(data?.summary?.blockedProducts || 0));
6253        if (summaryMetricImportReady) summaryMetricImportReady.textContent = String(Number(data?.summary?.importReadyProducts || 0));
6254        if (summaryMetricReviewRows) summaryMetricReviewRows.textContent = "0";
6255        if (summaryMetricBlocked) summaryMetricBlocked.textContent = String(Number(data?.summary?.blockedProducts || 0));
6256        if (metricBlockingIssues) metricBlockingIssues.textContent = String(Number(data?.summary?.blockingIssueCount || 0));
6257        if (metricWarnings) metricWarnings.textContent = String(Number(data?.summary?.warningCount || 0));
6258        if (metricRejected) metricRejected.textContent = String(Number(data?.summary?.rejectedRows || 0));
6259        if (summaryMetricWarnings) summaryMetricWarnings.textContent = String(captureWarnings);
6260        if (summaryMetricRejected) summaryMetricRejected.textContent = String(captureRejected);
6261        renderTopIssues([friendly]);
6262        resetPreview();
6263        renderDiscoveryCoverage(discoveryModelFromResponse(data));
6264        if (hasCatalogSources) {
6265          renderZeroExtractionDiagnostic(deriveMvpResultViewModel(data, requestMeta), requestMeta.sourceType === "url", data);
6266          if (E("mvpZeroExtractionDiagnostic")?.hidden === false) {
6267            if (stateTitle) { stateTitle.textContent = ""; stateTitle.hidden = true; }
6268            if (stateMessage) { stateMessage.textContent = ""; stateMessage.hidden = true; }
6269            if (resultBanner) { resultBanner.textContent = ""; resultBanner.hidden = true; }
6270            if (stateHint) stateHint.hidden = true;
6271            if (fallbackPanel) fallbackPanel.open = false;
6272            renderTopIssues([]);
6273          }
6274        }
6275        renderNextActions([]);
6276        hideDownloadLinks();
6277        resetCleanupAction("Generate a report first to unlock this review action.");
6278        if (successPanel) successPanel.style.display = "grid";
6279        setNoReportMessageVisible(false);
6280        if (statusEl) {
6281          statusEl.textContent = "";
6282          statusEl.hidden = true;
6283        }
6284        setScanActive(false, { keepIntentHidden: true });
6285        log("mvp_ingest_needs_capture", { ...requestMeta, requestId: data.requestId || null, reasonCode: data.reasonCode || null, elapsedMs: Date.now() - startedAt });
6286        setDetails(JSON.stringify({
6287          requestId: data.requestId || null,
6288          status: hasCatalogSources ? "catalog_sources_detected" : data.status,
6289          reasonCode: data.reasonCode || "",
6290          summary: { ...data.summary, productsFound: captureProductsFound },
6291          message: hasCatalogSources ? "Website extraction produced limited usable product data. Review the catalog sources below." : data.message || friendly,
6292          nextAction: hasCatalogSources ? "review_catalog_sources" : data.nextAction || "",
6293        }, null, 2));
6294        return;
6295      }
6296      renderCompletedPackage(data, requestMeta, startedAt);
6297      if (sourceType === "url" && isSignedIn()) await loadRunHistory();
6298    } catch (err) {
6299      if (!isForeground(requestMeta)) {
6300        if (submittedRunId) trackActiveRunObserver({ runId: submittedRunId, sourceUrl: url, host, state: "observing" });
6301        return;
6302      }
6303      setScanActive(false, { keepIntentHidden: true });
6304      clearActiveRunObserver(submittedRunId);
6305      const deliveryCode = ["result_unavailable", "poll_timeout", "status_read_failed", "session_expired", "result_access_de
6305nied"].includes(err?.code)
6306        ? err.code
6307        : "";
6308      log("mvp_ingest_failed", {
6309        ...requestMeta,
6310        reasonCode: err?.reasonCode || deliveryCode || undefined,
6311        error: err?.message || String(err),
6312        httpStatus: err?.httpStatus || undefined,
6313        requestId: err?.requestId || null,
6314        runId: err?.runId || null,
6315        jobId: err?.jobId || null,
6316        endpointType: err?.endpointType || undefined,
6317        elapsedMs: Date.now() - startedAt,
6318      });
6319      setResultSourceLabel(requestMeta);
6320      if (deliveryCode) {
6321        if (stateTitle) stateTitle.textContent = "Import package could not be loaded";
6322        if (stateMessage) stateMessage.textContent = err?.message || "Extraction may have completed. Your run remains available under My runs.";
6323        if (resultBanner) resultBanner.textContent = err?.httpStatus === 401
6324          ? (err?.authenticatedRequest ? "Sign in again to load this package." : "This package belongs to a different browser session.")
6325          : "The package result is temporarily unavailable.";
6326      } else {
6327        setStateCopy("failure");
6328      }
6329      if (issueSummaryTitle) issueSummaryTitle.textContent = "Blocking errors";
6330      setImportQualityMetrics({ productsFound: 0, importReadyProducts: 0, warningCount: 0, blockingIssueCount: 1, blockedProducts: 0 });
6331      if (metricProducts) metricProducts.textContent = "0";
6332      if (metricImportReady) metricImportReady.textContent = "0";
6333      if (metricReviewRows) metricReviewRows.textContent = "0";
6334      if (metricBlocked) metricBlocked.textContent = "0";
6335      if (summaryMetricImportReady) summaryMetricImportReady.textContent = "0";
6336      if (summaryMetricReviewRows) summaryMetricReviewRows.textContent = "0";
6337      if (summaryMetricBlocked) summaryMetricBlocked.textContent = "0";
6338      if (metricBlockingIssues) metricBlockingIssues.textContent = "0";
6339      if (metricWarnings) metricWarnings.textContent = "0";
6340      if (metricRejected) metricRejected.textContent = "0";
6341      if (summaryMetricWarnings) summaryMetricWarnings.textContent = "0";
6342      if (summaryMetricRejected) summaryMetricRejected.textContent = "0";
6343      renderTopIssues([deliveryCode ? `${err.message} (${deliveryCode})` : "No usable products found."]);
6344      renderDiscoveryCoverage(null);
6345      renderNextActions([]);
6346      if (successPanel) successPanel.style.display = "grid";
6347      setNoReportMessageVisible(false);
6348      hideDownloadLinks();
6349      resetCleanupAction("Generate a report first to unlock this review action.");
6350      if (statusEl) {
6351        statusEl.textContent = "";
6352        statusEl.hidden = true;
6353      }
6354      setDetails(deliveryCode
6355        ? `Result delivery failed.\n${err?.message || err}\nError code: ${deliveryCode}`
6356        : `Product extraction failed.\n${err?.message || err}`);
6357    } finally {
6358      activeSubmissionUrls.delete(submissionUrlKey);
6359      if (!isForeground(requestMeta)) return;
6360      initialScanLoading?.stop();
6361      if (activePreScanLoading === initialScanLoading) activePreScanLoading = null;
6362      activeInitialScanJobId = "";
6363      setUrlGenerationLoading(false);
6364    }
6365  });
6366
6367  intentInputs.forEach((input) => {
6368    input.addEventListener("change", () => {
6369      if (input.checked) setMvpIntent(input.value);
6370    });
6371  });
6372
6373  const restoreCheckoutScan = () => {
6374    let checkout = "";
6375    try {
6376      checkout = new URL(window.location.href).searchParams.get("checkout") || "";
6377    } catch {}
6378    if (!checkout) return;
6379    try {
6380      const saved = JSON.parse(window.sessionStorage?.getItem("fsMvpCheckoutScan") || "null");
6381      window.sessionStorage?.removeItem("fsMvpCheckoutScan");
6382      if (!saved || typeof saved !== "object") return;
6383      if (urlInput && typeof saved.url === "string") urlInput.value = saved.url;
6384      if (saved.intent === "visibility" || saved.intent === "extraction") setMvpIntent(saved.intent, { renderResult: false });
6385      if (saved.inputMode === "upload" || saved.inputMode === "url") setInputMode(saved.inputMode);
6386      const completedPackage = saved.completedPackage;
6387      if (completedPackage?.data && completedPackage?.requestMeta) {
6388        renderCompletedPackage(completedPackage.data, completedPackage.requestMeta, Date.now(), "mvp_checkout_scan_restored");
6389      }
6390      renderCheckoutRetentionMessage();
6391    } catch {}
6392  };
6393
6394  appInitCompletedMs = Math.round(performance.now() - appInitStartedAt);
6395  setMvpIntent("extraction", { renderResult: false });
6396  prefillUrlFromLocation();
6397  if (!claimPreviewOnlyFromLocation()) restoreCheckoutScan();
6398  void initAuth().finally(async () => {
6399    try {
6400      if (!claimPreviewOnlyFromLocation()) {
6401        await claimCurrentAnonymousRun();
6402        await handleCheckoutReturn();
6403        await checkForRecoveryChoice();
6404        await openLinkedDiscoveryResult();
6405      }
6406      await loadUsage();
6407      await previewSalesAgentRun();
6408      if (!claimPreviewOnlyFromLocation()) await claimSalesAgentRun();
6409    } finally {
6410      markActiveRunRecoveryReady();
6411    }
6412  });
6413})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.