feat(globe): live traffic arcs from real visitors via /api/visitor/recent-arcs + ARCS counter HUD
This commit is contained in:
parent
2336285644
commit
df53d85d01
3 changed files with 116 additions and 34 deletions
|
|
@ -1,6 +1,23 @@
|
|||
{
|
||||
"site": "jaeswift.xyz",
|
||||
"entries": [
|
||||
{
|
||||
"version": "1.38.0",
|
||||
"date": "20/04/2026",
|
||||
"category": "FEATURE",
|
||||
"title": "Globe — Live Traffic Arcs from Real Visitors",
|
||||
"changes": [
|
||||
"Homepage 3D globe (globe.gl) now draws glowing arcs from real visitor country centroids into Manchester HQ (53.48°N, -2.24°W) instead of random fake cities",
|
||||
"Data sourced live from /api/visitor/recent-arcs — tails last 20k lines of nginx access.log, GeoIPs unique IPs, maps country_code → centroid (130+ countries in api/data/country_centroids.json)",
|
||||
"Frontend polls endpoint every 30s; tactical green arcs (rgba(0,255,60,.85)) fade to turquoise Solana (#14F195) tail for depth, 2.2s dash animation, 0.38 altitude auto-scale",
|
||||
"Per-arc interactive labels on hover: \"COUNTRY → MCR · Xm ago · /page_path\"",
|
||||
"New ARCS LIVE counter badge added to globe overlay HUD (turquoise accent), updates with each refresh",
|
||||
"Sub-degree lat/lon jitter applied so multiple visitors from same country no longer stack on a single line",
|
||||
"Graceful fallback: if endpoint returns empty/errors, shows 8 demo city arcs (New York/Tokyo/Paris/Sydney/Moscow/Singapore/Berlin/LA) so the globe is never bare",
|
||||
"Timestamp parser handles nginx common-log format (DD/Mon/YYYY:HH:MM:SS ±ZZZZ) for accurate relative times",
|
||||
"Backend endpoint cache: 5-min TTL, deduped by IP, capped at 50 most-recent unique visitors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.37.2",
|
||||
"date": "20/04/2026",
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@
|
|||
<span class="coord-label">LONG</span>
|
||||
<span class="coord-value">-2.24°</span>
|
||||
</div>
|
||||
<div class="globe-coord-block">
|
||||
<span class="coord-label">ARCS</span>
|
||||
<span class="coord-value" style="color: var(--accent-solana, #14F195);"><span id="globeArcCount">0</span> LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
127
js/globe.js
127
js/globe.js
|
|
@ -117,51 +117,112 @@
|
|||
// Fallback: no country data, globe still works
|
||||
});
|
||||
|
||||
// Simulated visitor arcs (random locations connecting to Manchester)
|
||||
function generateRandomArcs() {
|
||||
var cities = [
|
||||
{ lat: 40.71, lng: -74.01 }, // New York
|
||||
{ lat: 35.68, lng: 139.69 }, // Tokyo
|
||||
{ lat: 48.86, lng: 2.35 }, // Paris
|
||||
{ lat: -33.87, lng: 151.21 }, // Sydney
|
||||
{ lat: 55.76, lng: 37.62 }, // Moscow
|
||||
{ lat: 1.35, lng: 103.82 }, // Singapore
|
||||
{ lat: 37.57, lng: 126.98 }, // Seoul
|
||||
{ lat: 19.43, lng: -99.13 }, // Mexico City
|
||||
{ lat: 52.52, lng: 13.41 }, // Berlin
|
||||
{ lat: -23.55, lng: -46.63 }, // São Paulo
|
||||
{ lat: 28.61, lng: 77.21 }, // Delhi
|
||||
{ lat: 34.05, lng: -118.24 }, // LA
|
||||
];
|
||||
// ─── LIVE TRAFFIC ARCS — fetched from /api/visitor/recent-arcs ───
|
||||
var currentArcs = [];
|
||||
|
||||
var arcs = [];
|
||||
var count = 2 + Math.floor(Math.random() * 3);
|
||||
for (var i = 0; i < count; i++) {
|
||||
var city = cities[Math.floor(Math.random() * cities.length)];
|
||||
arcs.push({
|
||||
startLat: city.lat,
|
||||
startLng: city.lng,
|
||||
function parseNginxTs(ts) {
|
||||
// '20/Apr/2026:03:35:38 +0200' → Date
|
||||
if (!ts) return null;
|
||||
try {
|
||||
var parts = ts.match(/(\d+)\/(\w+)\/(\d+):(\d+):(\d+):(\d+)\s*([+-]\d{4})?/);
|
||||
if (!parts) return new Date(ts);
|
||||
var months = { Jan:0,Feb:1,Mar:2,Apr:3,May:4,Jun:5,Jul:6,Aug:7,Sep:8,Oct:9,Nov:10,Dec:11 };
|
||||
return new Date(Date.UTC(
|
||||
parseInt(parts[3]),
|
||||
months[parts[2]] || 0,
|
||||
parseInt(parts[1]),
|
||||
parseInt(parts[4]),
|
||||
parseInt(parts[5]),
|
||||
parseInt(parts[6])
|
||||
));
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
function timeAgo(ts) {
|
||||
var d = parseNginxTs(ts);
|
||||
if (!d) return '—';
|
||||
var s = Math.floor((Date.now() - d.getTime()) / 1000);
|
||||
if (s < 60) return s + 's ago';
|
||||
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
||||
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
||||
return Math.floor(s / 86400) + 'd ago';
|
||||
}
|
||||
|
||||
function buildArcsFromVisitors(visitors) {
|
||||
return (visitors || []).map(function (v) {
|
||||
// jitter lat/lon slightly so duplicates from same country don't overlap perfectly
|
||||
var jlat = (Math.random() - 0.5) * 1.2;
|
||||
var jlon = (Math.random() - 0.5) * 1.2;
|
||||
return {
|
||||
startLat: v.lat + jlat,
|
||||
startLng: v.lon + jlon,
|
||||
endLat: SERVER_LAT,
|
||||
endLng: SERVER_LNG,
|
||||
color: ['rgba(0, 255, 60, 0.7)', 'rgba(0, 255, 60, 0.08)']
|
||||
color: ['rgba(0, 255, 60, 0.85)', 'rgba(20, 241, 149, 0.15)'],
|
||||
country_code: v.country_code,
|
||||
country_name: v.country_name,
|
||||
page_viewed: v.page_viewed,
|
||||
timestamp: v.timestamp,
|
||||
label: (v.country_name || v.country_code || 'Unknown') + ' → MCR · ' + timeAgo(v.timestamp) + ' · ' + (v.page_viewed || '/')
|
||||
};
|
||||
});
|
||||
}
|
||||
return arcs;
|
||||
|
||||
function fallbackArcs() {
|
||||
var cities = [
|
||||
{ lat: 40.71, lng: -74.01, name: 'New York' },
|
||||
{ lat: 35.68, lng: 139.69, name: 'Tokyo' },
|
||||
{ lat: 48.86, lng: 2.35, name: 'Paris' },
|
||||
{ lat: -33.87, lng: 151.21, name: 'Sydney' },
|
||||
{ lat: 55.76, lng: 37.62, name: 'Moscow' },
|
||||
{ lat: 1.35, lng: 103.82, name: 'Singapore' },
|
||||
{ lat: 52.52, lng: 13.41, name: 'Berlin' },
|
||||
{ lat: 34.05, lng: -118.24, name: 'LA' }
|
||||
];
|
||||
return cities.map(function (c) {
|
||||
return {
|
||||
startLat: c.lat, startLng: c.lng,
|
||||
endLat: SERVER_LAT, endLng: SERVER_LNG,
|
||||
color: ['rgba(0, 255, 60, 0.55)', 'rgba(0, 255, 60, 0.05)'],
|
||||
label: c.name + ' → MCR (demo)'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function refreshArcs() {
|
||||
fetch('/api/visitor/recent-arcs', { cache: 'no-store' })
|
||||
.then(function (r) { return r.ok ? r.json() : []; })
|
||||
.then(function (visitors) {
|
||||
if (!Array.isArray(visitors) || !visitors.length) {
|
||||
currentArcs = fallbackArcs();
|
||||
} else {
|
||||
currentArcs = buildArcsFromVisitors(visitors.slice(0, 50));
|
||||
}
|
||||
globe.arcsData(currentArcs);
|
||||
var countEl = document.getElementById('globeArcCount');
|
||||
if (countEl) countEl.textContent = String(currentArcs.length);
|
||||
})
|
||||
.catch(function () {
|
||||
if (!currentArcs.length) {
|
||||
currentArcs = fallbackArcs();
|
||||
globe.arcsData(currentArcs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
globe
|
||||
.arcsData(generateRandomArcs())
|
||||
.arcsData([])
|
||||
.arcColor('color')
|
||||
.arcDashLength(0.5)
|
||||
.arcDashGap(0.2)
|
||||
.arcDashAnimateTime(2000)
|
||||
.arcStroke(0.4)
|
||||
.arcAltitudeAutoScale(0.3);
|
||||
.arcDashInitialGap(function () { return Math.random(); })
|
||||
.arcDashAnimateTime(2200)
|
||||
.arcStroke(0.45)
|
||||
.arcAltitudeAutoScale(0.38)
|
||||
.arcLabel('label');
|
||||
|
||||
// Refresh arcs periodically
|
||||
setInterval(function () {
|
||||
globe.arcsData(generateRandomArcs());
|
||||
}, 5000);
|
||||
refreshArcs();
|
||||
setInterval(refreshArcs, 30000);
|
||||
|
||||
// Handle resize
|
||||
var resizeTimer;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue