mirror of
https://github.com/MrUnknownDE/utools.git
synced 2026-04-25 17:13:47 +02:00
set cache to 7 days
This commit is contained in:
@@ -37,8 +37,13 @@ COPY ./data ./data
|
|||||||
|
|
||||||
# Create a non-root user and group
|
# Create a non-root user and group
|
||||||
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
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
|
# Switch to the non-root user
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// ─── Filesystem Cache (24h TTL) ───────────────────────────────────────────────
|
// ─── 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');
|
const CACHE_DIR = process.env.ASN_CACHE_DIR || path.join(__dirname, '..', 'data', 'asn-cache');
|
||||||
|
|
||||||
// Ensure cache directory exists
|
// Ensure cache directory exists
|
||||||
@@ -57,7 +57,7 @@ function fetchJson(url) {
|
|||||||
'User-Agent': 'uTools-Network-Suite/1.0 (https://github.com/MrUnknownDE/utools)',
|
'User-Agent': 'uTools-Network-Suite/1.0 (https://github.com/MrUnknownDE/utools)',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
timeout: 8000,
|
timeout: 15000,
|
||||||
}, (res) => {
|
}, (res) => {
|
||||||
let raw = '';
|
let raw = '';
|
||||||
res.on('data', (chunk) => { raw += chunk; });
|
res.on('data', (chunk) => { raw += chunk; });
|
||||||
@@ -185,14 +185,23 @@ router.get('/', async (req, res, next) => {
|
|||||||
logger.info({ requestIp, asn }, 'ASN lookup request');
|
logger.info({ requestIp, asn }, 'ASN lookup request');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Level 1 + Level 2: fetch all base data in parallel
|
// Level 1 + Level 2: fetch all base data in parallel (allSettled = one failure won't crash everything)
|
||||||
const [overview, neighbours, prefixes, peeringdb] = await Promise.all([
|
const [overviewResult, neighboursResult, prefixesResult, peeringdbResult] = await Promise.allSettled([
|
||||||
fetchOverview(asn),
|
fetchOverview(asn),
|
||||||
fetchNeighbours(asn),
|
fetchNeighbours(asn),
|
||||||
fetchPrefixes(asn),
|
fetchPrefixes(asn),
|
||||||
fetchPeeringDb(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
|
// Split neighbours
|
||||||
const upstreams = neighbours.filter(n => n.type === 'left').sort((a, b) => b.power - a.power).slice(0, 10);
|
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);
|
const downstreams = neighbours.filter(n => n.type === 'right').sort((a, b) => b.power - a.power).slice(0, 10);
|
||||||
|
|||||||
@@ -302,7 +302,14 @@
|
|||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
<div id="loading-section" class="hidden flex flex-col items-center gap-4 py-16">
|
<div id="loading-section" class="hidden flex flex-col items-center gap-4 py-16">
|
||||||
<div class="loader" style="width:40px;height:40px;border-width:5px;"></div>
|
<div class="loader" style="width:40px;height:40px;border-width:5px;"></div>
|
||||||
<p class="text-gray-400 text-sm" id="loading-msg">Fetching AS data…</p>
|
<p class="text-gray-400 text-sm" id="loading-msg">Querying RIPE Stat & PeeringDB…</p>
|
||||||
|
<!-- Shown after 3s for slow lookups -->
|
||||||
|
<div id="loading-hint" class="hidden mt-2 max-w-sm text-center">
|
||||||
|
<p class="text-xs text-amber-400/80 bg-amber-400/10 border border-amber-400/20 rounded-lg px-4 py-2">
|
||||||
|
⏳ Large ASes (like Cloudflare, Google, Tier-1 carriers) can take up to 15 seconds on the first
|
||||||
|
lookup — subsequent lookups are cached for 24h.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
|
|||||||
@@ -21,14 +21,22 @@ function showError(msg) {
|
|||||||
errorBox.classList.remove('hidden');
|
errorBox.classList.remove('hidden');
|
||||||
loadingSection.classList.add('hidden');
|
loadingSection.classList.add('hidden');
|
||||||
resultsSection.classList.add('hidden');
|
resultsSection.classList.add('hidden');
|
||||||
|
if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer);
|
||||||
}
|
}
|
||||||
function hideError() { errorBox.classList.add('hidden'); }
|
function hideError() { errorBox.classList.add('hidden'); }
|
||||||
|
|
||||||
function setLoading(msg = 'Fetching AS data…') {
|
function setLoading(msg = 'Querying RIPE Stat & PeeringDB…') {
|
||||||
hideError();
|
hideError();
|
||||||
loadingMsg.textContent = msg;
|
loadingMsg.textContent = msg;
|
||||||
loadingSection.classList.remove('hidden');
|
loadingSection.classList.remove('hidden');
|
||||||
resultsSection.classList.add('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) {
|
function updateUrlParam(asn) {
|
||||||
@@ -69,6 +77,10 @@ async function doLookup(rawAsn) {
|
|||||||
function renderResults(data) {
|
function renderResults(data) {
|
||||||
loadingSection.classList.add('hidden');
|
loadingSection.classList.add('hidden');
|
||||||
resultsSection.classList.remove('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
|
// Header
|
||||||
document.getElementById('res-asn').textContent = `AS${data.asn}`;
|
document.getElementById('res-asn').textContent = `AS${data.asn}`;
|
||||||
@@ -290,13 +302,15 @@ function renderGraph(graph) {
|
|||||||
.attr('fill', '#e5e7eb')
|
.attr('fill', '#e5e7eb')
|
||||||
.text(d => `AS${d.asn}`);
|
.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('dy', d => nodeRadius[d.role] + 23)
|
||||||
.attr('font-size', 8)
|
.attr('font-size', d => d.role === 'tier1' ? 7 : 8)
|
||||||
.attr('fill', '#9ca3af')
|
.attr('fill', d => d.role === 'tier1' ? '#6b7280' : '#9ca3af')
|
||||||
.text(d => {
|
.text(d => {
|
||||||
const max = d.role === 'center' ? 22 : 16;
|
if (!d.name) return '';
|
||||||
return d.name && d.name.length > max ? d.name.slice(0, max) + '…' : (d.name || '');
|
const max = d.role === 'center' ? 22 : d.role === 'tier1' ? 12 : 16;
|
||||||
|
return d.name.length > max ? d.name.slice(0, max) + '…' : d.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Tick ──────────────────────────────────────────────────────────────
|
// ── Tick ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user