// SVG icons for status const iconCheck = `` const iconCross = `` const iconInfo = `` window.addEventListener('load', function () { initFormValues(new URL(window.location)) const plausible = window.plausible || function() { window.plausible = window.plausible || { q: [] }; try { window.plausible.q.push(arguments); } catch (e) { // Silent fallback - analytics shouldn't break the app } } const queryForm = document.getElementById('queryForm') if (!queryForm) { console.error('Query form not found') return } let countdownInterval = null // Clear results when CID field value changes const cidInput = document.getElementById('cid') if (cidInput) { cidInput.addEventListener('change', function() { showOutput('') // clear out previous results showRawOutput('') // clear out previous results }) } queryForm.addEventListener('submit', async function (e) { e.preventDefault() // dont do a browser form post showOutput('') // clear out previous results showRawOutput('') // clear out previous results const formData = new FormData(queryForm) const backendURL = getBackendUrl(formData) const inputMaddr = formData.get('multiaddr') // Start countdown timer const timeoutSeconds = parseInt(formData.get('timeoutSeconds')) || 30 startCountdown(timeoutSeconds) plausible('IPFS Check Run', { props: { withMultiaddr: inputMaddr !== '' }, }) showInQuery(formData) // add `cid` and `multiaddr` to local url query to make it shareable toggleSubmitButton() try { const res = await fetch(backendURL, { method: 'POST' }) if (res.ok) { const respObj = await res.json() showRawOutput(JSON.stringify(respObj, null, 2)) if(inputMaddr === '') { const output = formatJustCidOutput(respObj) showOutput(output) } else { const output = formatMaddrOutput(inputMaddr, respObj) showOutput(output) } } else { const resText = await res.text() showOutput(`⚠️ backend returned an error: ${res.status} ${resText}`) } } catch (e) { console.log(e) showOutput(`⚠️ backend error: ${e}`) } finally { stopCountdown() toggleSubmitButton() } }) function startCountdown(seconds) { // Clear any existing countdown stopCountdown() const buttonText = document.getElementById('button-text') let remaining = seconds // Update button text immediately if (buttonText) { buttonText.textContent = `Testing: ${remaining}s` } // Update every second countdownInterval = setInterval(() => { remaining-- if (remaining <= 0) { stopCountdown() } else if (buttonText) { buttonText.textContent = `Testing: ${remaining}s` } }, 1000) } function stopCountdown() { if (countdownInterval) { clearInterval(countdownInterval) countdownInterval = null } // Restore original button text const buttonText = document.getElementById('button-text') if (buttonText) { buttonText.textContent = 'Run Test' } } }) function initFormValues (url) { for (const [key, val] of url.searchParams) { document.getElementById(key)?.setAttribute('value', val) } const timeoutSlider = document.getElementById('timeoutSeconds') const timeoutValue = document.getElementById('timeoutValue') if (timeoutSlider && timeoutValue) { timeoutSlider.addEventListener('input', function() { timeoutValue.textContent = this.value }) // set initial value timeoutValue.textContent = timeoutSlider.value } } function showInQuery (formData) { const backendURLElement = document.getElementById('backendURL') const defaultBackendUrl = backendURLElement ? backendURLElement.getAttribute('placeholder') : null const params = new URLSearchParams(formData) // skip showing default value our shareable url if (defaultBackendUrl && params.get('backendURL') === defaultBackendUrl) { params.delete('backendURL') } const url = new URL('?' + params, window.location) history.replaceState(null, "", url) } function getBackendUrl (formData) { const params = new URLSearchParams(formData) // dont send backendURL to the backend! params.delete('backendURL') // backendURL is the base, params are appended as query string try { return new URL('/check?' + params, formData.get('backendURL')) } catch (e) { console.error('Invalid backend URL:', e) // Fallback to current origin return new URL('/check?' + params, window.location.origin) } } function showOutput (output) { const outObj = document.getElementById('output') if (!outObj) { console.error('Output element not found') return } outObj.innerHTML = output // Show/hide raw output details based on whether there's output const rawOutputDetails = document.querySelector('details:has(#raw-output)') if (rawOutputDetails) { if (output && output.trim()) { rawOutputDetails.style.display = 'block' } else { rawOutputDetails.style.display = 'none' } } } function showRawOutput (output) { const outObj = document.getElementById('raw-output') if (!outObj) { console.error('Raw output element not found') return } outObj.textContent = output } function toggleSubmitButton() { const button = document.getElementById('submit') if (button) { button.toggleAttribute('disabled') } const spinner = document.getElementById('loading-spinner') if (spinner) { // Toggle spinner visibility spinner.classList.toggle('hidden') } } function formatMutableResolution(mutableRes) { if (!mutableRes) return '' // Extract domain from diagnostic URL for display (without hash) let diagnosticSite = '' if (mutableRes.DiagnosticURL) { try { const url = new URL(mutableRes.DiagnosticURL) diagnosticSite = url.hostname } catch (e) { diagnosticSite = 'diagnostic tool' } } let html = '
' // Show warning if input is mutable or legacy format if (mutableRes.IsMutableInput) { html += `
⚠️ Input is not an immutable CID, assuming mutable pointer
` } if (mutableRes.Error) { // Resolution failed html += `${iconCross} Mutable pointer resolution failed` html += ` for ${mutableRes.InputPath || 'unknown'}` html += `
Error: ${mutableRes.Error}
` if (mutableRes.DiagnosticURL) { html += `
More details at ${diagnosticSite}
` } } else { // Resolution succeeded html += `${iconCheck} Mutable pointer resolved successfully` html += '
' // Add details box similar to provider results html += '
' html += '
Input: ' + (mutableRes.InputPath || 'unknown') + '
' html += '
Resolved to: ' + (mutableRes.ResolvedPath || 'unknown') + '
' if (mutableRes.DiagnosticURL) { html += '
More details at ' + diagnosticSite + '
' } html += '
' html += '
' } html += '
' return html } function formatMaddrOutput (multiaddr, respObj) { const peerIDStartIndex = multiaddr.lastIndexOf("/p2p/") const peerID = multiaddr.slice(peerIDStartIndex + 5); const addrPart = multiaddr.slice(0, peerIDStartIndex); let outHtml = `
` // Show resolution info if present (at top level) outHtml += formatMutableResolution(respObj.MutableResolution) // Extract actual peer check result (might be wrapped) const peerResult = respObj.Result || respObj // Connection status if (peerResult.ConnectionError !== "") { outHtml += `
${iconCross}Could not connect to multiaddr: ${peerResult.ConnectionError}
` } else { const madrs = peerResult?.ConnectionMaddrs outHtml += `
${iconCheck}Successfully connected to multiaddr${madrs?.length > 1 ? 's' : '' }:
${madrs.join('
')}
` } // DHT status if (multiaddr.indexOf("/p2p/") === 0 && multiaddr.lastIndexOf("/") === 4) { // only peer id passed with /p2p/PeerID if (Object.keys(peerResult.PeerFoundInDHT).length === 0) { outHtml += `
${iconCross}Could not find any multiaddrs in the DHT
` } else { outHtml += `
${iconCheck}Found multiaddrs advertised in the DHT:
${Object.keys(peerResult.PeerFoundInDHT).join('
')}
` } } else { // a proper maddr with an IP was passed let foundAddr = false for (const key in peerResult.PeerFoundInDHT) { if (key === addrPart) { foundAddr = true outHtml += `
${iconCheck}Found multiaddr with ${peerResult.PeerFoundInDHT[key]} DHT peers
` break } } if (!foundAddr) { let alt = '' if (Object.keys(peerResult.PeerFoundInDHT).length > 0) { alt = `
Instead found:
${Object.keys(peerResult.PeerFoundInDHT).join('
')}
` } else { alt = '
No other addresses were found.' } outHtml += `
${iconCross}Could not find the given multiaddr in the DHT.${alt}
` } } // Provider record if (peerResult.ProviderRecordFromPeerInDHT === true || peerResult.ProviderRecordFromPeerInIPNI === true) { outHtml += `
${iconCheck}Found multihash advertised in ${peerResult.ProviderRecordFromPeerInDHT ? 'DHT' : 'IPNI'}
` } else { outHtml += `
${iconCross}Could not find the multihash in DHT or IPNI
` } // Bitswap if (peerResult.DataAvailableOverBitswap?.Enabled === true) { if (peerResult.DataAvailableOverBitswap?.Error !== "") { outHtml += `
${iconCross}There was an error downloading the data for the CID from the peer via Bitswap: ${peerResult.DataAvailableOverBitswap.Error}
` } else if (peerResult.DataAvailableOverBitswap?.Responded !== true) { outHtml += `
${iconCross}The peer did not quickly respond if it had the data for the CID over Bitswap
` } else if (peerResult.DataAvailableOverBitswap?.Found === true) { outHtml += `
${iconCheck}The peer responded that it has the data for the CID over Bitswap
` } else { outHtml += `
${iconCross}The peer responded that it does not have the data for the CID over Bitswap
` } } // HTTP if (peerResult.DataAvailableOverHTTP?.Enabled === true) { if (peerResult.DataAvailableOverHTTP?.Error !== "") { outHtml += `
${iconCross}There was an error downloading the data for the CID via HTTP: ${peerResult.DataAvailableOverHTTP.Error}
` } if (peerResult.DataAvailableOverHTTP?.Connected !== true) { outHtml += `
${iconCross}HTTP connection was unsuccessful to the HTTP endpoint
` } else if (peerResult.DataAvailableOverHTTP?.Found === true) { outHtml += `
${iconCheck}The HTTP endpoint responded that it has the data for the CID
` } else { outHtml += `
${iconCross}The HTTP endpoint responded that it does not have the data for the CID
` } } outHtml += '
' return outHtml } function formatJustCidOutput (resp) { let outHtml = '' // Show resolution info if present (at top level) if (resp.MutableResolution) { outHtml += formatMutableResolution(resp.MutableResolution) // Handle resolution-only response (resolution failed, no providers) if (!resp.Providers) { return outHtml } } // Extract providers array (might be at top level or under Providers key) const providers = resp.Providers || resp if (!Array.isArray(providers) || providers.length === 0) { return outHtml + `
${iconCross}No providers found for the given CID
` } const successfulProviders = providers.reduce((acc, provider) => { if(provider.ConnectionError === '' && (provider.DataAvailableOverBitswap?.Found === true || provider.DataAvailableOverHTTP?.Found === true)) { acc++ } return acc }, 0) // Show providers with the data first, followed by reachable providers, then by those with addresses providers.sort((a, b) => { const aHasData = a.DataAvailableOverBitswap?.Found || a.DataAvailableOverHTTP?.Found const bHasData = b.DataAvailableOverBitswap?.Found || b.DataAvailableOverHTTP?.Found // First order by data availability if (aHasData && !bHasData) return -1 if (!aHasData && bHasData) return 1 const aHasBitswap = a.DataAvailableOverBitswap?.Enabled const bHasBitswap = b.DataAvailableOverBitswap?.Enabled // Then order HTTP first if (aHasBitswap && !bHasBitswap) return 1 if (!aHasBitswap && bHasBitswap) return -1 const aSource = a.Source const bSource = b.Source // Then order Amino DHT first if (aSource === 'IPNI' && bSource !== 'IPNI') return 1 if (aSource !== 'IPNI' && bSource === 'IPNI') return -1 // Then order by connection errors if (a.ConnectionError === '' && b.ConnectionError !== '') { return -1; } else if (a.ConnectionError !== '' && b.ConnectionError === '') { return 1; } // Finally, show providers with addresses first if(a.Addrs?.length > 0 && b.Addrs?.length === 0) { return -1 } else if(a.Addrs?.length === 0 && b.Addrs?.length > 0) { return 1 } return 0 }) outHtml += `
${successfulProviders > 0 ? iconCheck : iconCross} Found ${successfulProviders} working providers (out of ${providers.length} provider records sampled from Amino DHT and IPNI) that could be connected to and had the CID available over Bitswap:
` outHtml += `
` for (const provider of providers) { const couldConnect = provider.ConnectionError === '' const hasBitswap = provider.DataAvailableOverBitswap?.Enabled === true const hasHTTP = provider.DataAvailableOverHTTP?.Enabled === true const foundBitswap = provider.DataAvailableOverBitswap?.Found const foundHTTP = provider.DataAvailableOverHTTP?.Found const isUnsuccessful = !couldConnect || (!foundBitswap && !foundHTTP) const cardBg = isUnsuccessful ? 'bg-red-50 border-red-200' : 'bg-white border-gray-200' outHtml += `
` outHtml += `
` outHtml += `${provider.ID}` if (hasBitswap) outHtml += `Bitswap` if (hasHTTP) outHtml += `HTTP` if (provider?.Source != null) { const bgColor = provider.Source === 'IPNI' ? 'bg-orange-100 text-orange-700' : 'bg-purple-100 text-purple-700' outHtml += `${provider.Source}` } outHtml += `
` if (hasBitswap) { outHtml += `
${couldConnect ? iconCheck : iconCross}Libp2p connected: ${couldConnect ? 'Yes' : provider.ConnectionError.replaceAll('\n', '
')}
` if (couldConnect) { if (provider.AgentVersion) { outHtml += `
${iconCheck}Agent Version: ${provider.AgentVersion}
` } const foundText = provider.DataAvailableOverBitswap.Found ? 'Found' : 'Not found' outHtml += `
${provider.DataAvailableOverBitswap.Found ? iconCheck : iconCross}Bitswap Check: ${foundText} ${provider.DataAvailableOverBitswap.Error || ''}
` } } if (hasHTTP) { const httpRes = provider.DataAvailableOverHTTP outHtml += `
${httpRes?.Connected ? iconCheck : iconCross}HTTP Connected: ${httpRes?.Connected ? 'Yes' : 'No'}
` outHtml += `
${httpRes?.Requested ? iconCheck : iconCross}HTTP request: ${httpRes?.Requested ? 'Yes' : 'No'}
` outHtml += `
${httpRes?.Found ? iconCheck : iconCross}HTTP Found: ${httpRes?.Found ? 'Yes' : 'No'} ${httpRes?.Error ? '(' + httpRes.Error + ')' : ''}
` } outHtml += (couldConnect && provider.ConnectionMaddrs) ? `
${(hasHTTP && !provider.DataAvailableOverHTTP?.Found) ? 'Attempted' : 'Successful'} Connection Multiaddr${provider.ConnectionMaddrs.length > 1 ? 's' : ''}:
${provider.ConnectionMaddrs?.join('
') || ''}
` : '' outHtml += (provider.Addrs?.length > 0) ? `
Peer Multiaddrs:
${provider.Addrs.join('
') || ''}
` : '' outHtml += `
` } outHtml += `
` return outHtml } /** * ---------------------------------------------------------------------------------------------------------- * If included in an iframe, we need to allow consumers/parent frames to know the size of the iframe content. * e.g. ipfs-webui's /#/diagnostics/check page, where ipfs-check is embedded * ---------------------------------------------------------------------------------------------------------- */ if (window.self !== window.top) { let rafId = null; let lastH = -1; function measuredHeight() { const sentinel = document.getElementById('__iframe_sentinel'); if (!sentinel) { // Fallback to document height if sentinel is missing return document.documentElement.getBoundingClientRect().height; } const rect = sentinel.getBoundingClientRect(); const bottom = rect.bottom + window.scrollY; // page Y of sentinel bottom const documentElHeight = document.documentElement.getBoundingClientRect().height return Math.min(bottom, documentElHeight); } function postSize() { if (rafId != null) return; rafId = requestAnimationFrame(() => { rafId = null; const h = measuredHeight(); if (h !== lastH) { lastH = h; try { parent.postMessage({ type: 'iframe-size:report', width: document.documentElement.scrollWidth, height: h, scrollHeight: document.documentElement.scrollHeight, scrollWidth: document.documentElement.scrollWidth }, '*'); } catch (error) { // Silently fail - iframe might be sandboxed or parent might not exist console.debug('Failed to post size to parent:', error); } } }); } // triggers window.addEventListener('load', postSize); window.addEventListener('resize', postSize); window.visualViewport?.addEventListener('resize', postSize); document.addEventListener('transitionend', postSize); document.fonts?.addEventListener?.('loadingdone', postSize); // Store observers for cleanup const resizeObserver = new ResizeObserver(postSize); const mutationObserver = new MutationObserver(postSize); resizeObserver.observe(document.body); mutationObserver.observe(document.body, { childList: true, subtree: true }); // Cleanup on page unload window.addEventListener('beforeunload', () => { resizeObserver.disconnect(); mutationObserver.disconnect(); if (rafId != null) { cancelAnimationFrame(rafId); } }); }