diff --git a/backend/Dockerfile b/backend/Dockerfile index a4b2319..48ae9cc 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -37,8 +37,13 @@ COPY ./data ./data # Create a non-root user and group RUN addgroup -S appgroup && adduser -S appuser -G appgroup -# Optional: Change ownership of app files to the new user -# RUN chown -R appuser:appgroup /app + +# Create ASN cache directory and set correct ownership BEFORE switching user +# This ensures the Docker volume mount is writable by appuser +RUN mkdir -p /app/asn-cache && chown -R appuser:appgroup /app/asn-cache + +# Change ownership of all app files to the new user +RUN chown -R appuser:appgroup /app # Switch to the non-root user USER appuser diff --git a/backend/routes/asnLookup.js b/backend/routes/asnLookup.js index 17f3dfe..cf3cc54 100644 --- a/backend/routes/asnLookup.js +++ b/backend/routes/asnLookup.js @@ -10,7 +10,7 @@ const logger = pino({ level: process.env.LOG_LEVEL || 'info' }); const router = express.Router(); // ─── Filesystem Cache (24h TTL) ─────────────────────────────────────────────── -const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours +const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days const CACHE_DIR = process.env.ASN_CACHE_DIR || path.join(__dirname, '..', 'data', 'asn-cache'); // Ensure cache directory exists @@ -57,7 +57,7 @@ function fetchJson(url) { 'User-Agent': 'uTools-Network-Suite/1.0 (https://github.com/MrUnknownDE/utools)', 'Accept': 'application/json', }, - timeout: 8000, + timeout: 15000, }, (res) => { let raw = ''; res.on('data', (chunk) => { raw += chunk; }); @@ -185,14 +185,23 @@ router.get('/', async (req, res, next) => { logger.info({ requestIp, asn }, 'ASN lookup request'); try { - // Level 1 + Level 2: fetch all base data in parallel - const [overview, neighbours, prefixes, peeringdb] = await Promise.all([ + // Level 1 + Level 2: fetch all base data in parallel (allSettled = one failure won't crash everything) + const [overviewResult, neighboursResult, prefixesResult, peeringdbResult] = await Promise.allSettled([ fetchOverview(asn), fetchNeighbours(asn), fetchPrefixes(asn), fetchPeeringDb(asn), ]); + const overview = overviewResult.status === 'fulfilled' ? overviewResult.value : { asn, name: null, announced: false, type: null }; + const neighbours = neighboursResult.status === 'fulfilled' ? neighboursResult.value : []; + const prefixes = prefixesResult.status === 'fulfilled' ? prefixesResult.value : []; + const peeringdb = peeringdbResult.status === 'fulfilled' ? peeringdbResult.value : null; + + if (overviewResult.status === 'rejected') logger.warn({ asn, error: overviewResult.reason?.message }, 'Overview fetch failed, continuing with partial data'); + if (neighboursResult.status === 'rejected') logger.warn({ asn, error: neighboursResult.reason?.message }, 'Neighbours fetch failed, continuing with partial data'); + if (prefixesResult.status === 'rejected') logger.warn({ asn, error: prefixesResult.reason?.message }, 'Prefixes fetch failed, continuing with partial data'); + // Split neighbours const upstreams = neighbours.filter(n => n.type === 'left').sort((a, b) => b.power - a.power).slice(0, 10); const downstreams = neighbours.filter(n => n.type === 'right').sort((a, b) => b.power - a.power).slice(0, 10); diff --git a/frontend/app/asn-lookup.html b/frontend/app/asn-lookup.html index 0d7be9f..a522369 100644 --- a/frontend/app/asn-lookup.html +++ b/frontend/app/asn-lookup.html @@ -302,7 +302,14 @@ diff --git a/frontend/app/asn-lookup.js b/frontend/app/asn-lookup.js index 4e4ff46..0dfb711 100644 --- a/frontend/app/asn-lookup.js +++ b/frontend/app/asn-lookup.js @@ -21,14 +21,22 @@ function showError(msg) { errorBox.classList.remove('hidden'); loadingSection.classList.add('hidden'); resultsSection.classList.add('hidden'); + if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer); } function hideError() { errorBox.classList.add('hidden'); } -function setLoading(msg = 'Fetching AS data…') { +function setLoading(msg = 'Querying RIPE Stat & PeeringDB…') { hideError(); loadingMsg.textContent = msg; loadingSection.classList.remove('hidden'); resultsSection.classList.add('hidden'); + + // After 3s show a hint that large ASes can be slow + if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer); + window._loadingHintTimer = setTimeout(() => { + const hint = document.getElementById('loading-hint'); + if (hint) hint.classList.remove('hidden'); + }, 3000); } function updateUrlParam(asn) { @@ -69,6 +77,10 @@ async function doLookup(rawAsn) { function renderResults(data) { loadingSection.classList.add('hidden'); resultsSection.classList.remove('hidden'); + // Reset loading hint for next lookup + if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer); + const hint = document.getElementById('loading-hint'); + if (hint) hint.classList.add('hidden'); // Header document.getElementById('res-asn').textContent = `AS${data.asn}`; @@ -290,13 +302,15 @@ function renderGraph(graph) { .attr('fill', '#e5e7eb') .text(d => `AS${d.asn}`); - node.filter(d => d.role !== 'tier1').append('text') + // Name label for ALL roles (tier1 gets shorter truncation) + node.append('text') .attr('dy', d => nodeRadius[d.role] + 23) - .attr('font-size', 8) - .attr('fill', '#9ca3af') + .attr('font-size', d => d.role === 'tier1' ? 7 : 8) + .attr('fill', d => d.role === 'tier1' ? '#6b7280' : '#9ca3af') .text(d => { - const max = d.role === 'center' ? 22 : 16; - return d.name && d.name.length > max ? d.name.slice(0, max) + '…' : (d.name || ''); + if (!d.name) return ''; + const max = d.role === 'center' ? 22 : d.role === 'tier1' ? 12 : 16; + return d.name.length > max ? d.name.slice(0, max) + '…' : d.name; }); // ── Tick ──────────────────────────────────────────────────────────────