diff --git a/docs/EXPLORER_API_ACCESS.md b/docs/EXPLORER_API_ACCESS.md index 732d7d3..1d2cc9e 100644 --- a/docs/EXPLORER_API_ACCESS.md +++ b/docs/EXPLORER_API_ACCESS.md @@ -13,6 +13,21 @@ The frontend is reachable at **https://explorer.d-bis.org** (FQDN) or by **VM IP 2. **Same-origin /api** – When the site is served from the explorer host (FQDN `https://explorer.d-bis.org` or VM IP `http://192.168.11.140` / `https://192.168.11.140`), the frontend uses relative `/api` so all requests go through the same nginx proxy. If you open the frontend from elsewhere, the code falls back to the full Blockscout URL (CORS must allow it). - If the API returns **200** but the UI still shows no data, check the browser console for JavaScript errors (e.g. CSP or network errors). +### Frontend env contract + +For the Next frontend in `frontend/`, keep the runtime base URL at the **host origin**, not the `/api` subpath: + +```env +NEXT_PUBLIC_API_URL=https://explorer.d-bis.org +NEXT_PUBLIC_CHAIN_ID=138 +``` + +Why: + +- The Next pages now call live **Blockscout v2** endpoints under `${NEXT_PUBLIC_API_URL}/api/v2/*`. +- Setting `NEXT_PUBLIC_API_URL=https://explorer.d-bis.org/api` will incorrectly produce requests like `/api/api/v2/*`. +- Token aggregation remains under `/token-aggregation/api/v1/*` and is linked separately by the frontend. + --- ## CSP blocks eval / “script-src blocked” diff --git a/frontend/package.json b/frontend/package.json index 010527b..70dfe56 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,9 +6,11 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", + "build:check": "npm run lint && npm run type-check && npm run build", + "start": "PORT=${PORT:-3000} node .next/standalone/server.js", + "start:next": "next start", "lint": "next lint", - "type-check": "tsc --noEmit", + "type-check": "tsc --noEmit -p tsconfig.check.json", "test": "npm run lint && npm run type-check", "test:unit": "vitest run" }, diff --git a/frontend/public/explorer-spa.js b/frontend/public/explorer-spa.js index 9625736..ab4e806 100644 --- a/frontend/public/explorer-spa.js +++ b/frontend/public/explorer-spa.js @@ -18,7 +18,7 @@ banner.id = 'apiUnavailableBanner'; banner.setAttribute('role', 'alert'); banner.style.cssText = 'background: rgba(200,80,80,0.95); color: #fff; padding: 0.75rem 1rem; margin-bottom: 1rem; border-radius: 8px; font-size: 0.9rem;'; - banner.innerHTML = 'Explorer API temporarily unavailable (HTTP ' + status + '). Stats, blocks, and transactions cannot load until the backend is running. See docs.'; + banner.innerHTML = 'Explorer API temporarily unavailable (HTTP ' + status + '). Stats, blocks, and transactions cannot load until the backend is running. See docs.'; main.insertBefore(banner, main.firstChild); } (function() { @@ -2016,7 +2016,7 @@ async function renderHomeView() { showView('home'); - if ((window.location.pathname || '').replace(/^\//, '').replace(/\/$/, '') !== 'home') updatePath('/home'); + if ((window.location.pathname || '').replace(/\/$/, '') !== '') updatePath('/'); await loadStats(); await loadLatestBlocks(); await loadLatestTransactions(); @@ -2115,7 +2115,7 @@ } else if (fromHash) { route = fromHash; } - if (!route) { showHome(); updatePath('/home'); return; } + if (!route || route === 'home') { if (currentView !== 'home') showHome(); return; } var parts = route.split('/').filter(Boolean); var decode = function(s) { try { return decodeURIComponent(s); } catch (e) { return s; } }; if (parts[0] === 'block' && parts[1]) { var p1 = decode(parts[1]); var key = 'block:' + p1; if (currentDetailKey === key) return; currentDetailKey = key; setTimeout(function() { showBlockDetail(p1); }, 0); return; } @@ -2204,7 +2204,7 @@ // Update breadcrumb navigation function updateBreadcrumb(type, identifier, identifierExtra) { let breadcrumbContainer; - let breadcrumbHTML = 'Home'; + let breadcrumbHTML = 'Home'; switch (type) { case 'block': breadcrumbContainer = document.getElementById('blockDetailBreadcrumb'); @@ -2222,7 +2222,7 @@ break; case 'address': breadcrumbContainer = document.getElementById('addressDetailBreadcrumb'); - breadcrumbHTML += '/Address/' + escapeHtml(shortenHash(identifier)) + ''; + breadcrumbHTML += '/Addresses/' + escapeHtml(shortenHash(identifier)) + ''; break; case 'token': breadcrumbContainer = document.getElementById('tokenDetailBreadcrumb'); @@ -3716,7 +3716,7 @@ html += '

Public Explorer Access Points

'; html += '
'; endpointCards.forEach(function(card) { - html += ''; + html += ''; html += '
'; html += '
' + escapeHtml(card.title) + '
'; html += '' + escapeHtml(card.method) + ''; @@ -4091,19 +4091,19 @@
CCIPWETH10Bridge
- ${WETH10_BRIDGE_MAINNET} + ${WETH10_BRIDGE_MAINNET}
- View on Etherscan + View on Etherscan
@@ -4678,7 +4678,8 @@ throw new Error('Address not found'); } } catch (error) { - container.innerHTML = '
Failed to load address: ' + escapeHtml(error.message || 'Unknown error') + '.
'; + var retryAddress = String(address || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + container.innerHTML = '
Failed to load address: ' + escapeHtml(error.message || 'Unknown error') + '.
'; return; } } else { @@ -4694,26 +4695,29 @@ const balanceEth = formatEther(a.balance || '0'); const isContract = !!a.is_contract; const verifiedBadge = a.is_verified ? 'Verified' : ''; - const contractLink = isContract ? `View contract on Blockscout` : ''; + const encodedAddress = encodeURIComponent(address); + const escapedAddress = escapeHtml(address); + const addressForJs = address.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + const contractLink = isContract ? `View contract on Blockscout` : ''; const savedLabel = getAddressLabel(address); const inWatchlist = isInWatchlist(address); container.innerHTML = `
Address
-
${address}
+
${escapedAddress}
Label
-
+
Watchlist
-
+
Token approvals
- +
Balance
@@ -4956,7 +4960,7 @@ } el.dataset.loaded = '1'; if (!data) { - el.innerHTML = '

Contract source not indexed. Verify on Blockscout

'; + el.innerHTML = '

Contract source not indexed. Verify on Blockscout

'; return; } const abi = data.abi || data.abi_interface || []; @@ -4984,7 +4988,7 @@ html += ''; html += '
'; } - html += '

Read / Write contract on Blockscout

'; + html += '

Read / Write contract on Blockscout

'; el.innerHTML = html; if (viewFns.length > 0) { (function setupReadContract(contractAddr, abiJson, viewFunctions) { @@ -5279,7 +5283,7 @@ html += '
'; } } - html += '

View on Blockscout

'; + html += '

View on Blockscout

'; container.innerHTML = html; } catch (err) { container.innerHTML = '
Failed to load NFT: ' + escapeHtml(err.message || 'Unknown') + '
'; @@ -5441,7 +5445,7 @@ function getExplorerAIPageContext() { return { - path: (window.location && window.location.pathname) ? window.location.pathname : '/home', + path: (window.location && window.location.pathname) ? window.location.pathname : '/', view: currentView || 'home' }; } diff --git a/frontend/public/index.html b/frontend/public/index.html index ce09ccb..aeee489 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1007,7 +1007,7 @@