// 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 += `
'
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}
`
}
// 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}
${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: