feat: Add real-time visitor map with IP geolocation
- Add latitude/longitude columns to visitor_logs model - Update visitor_service to fetch and store coordinates from ip-api.com - Add /admin/map-data API endpoint for map visualization - Create VisitorMap component using Leaflet/OpenStreetMap - Integrate map into visitor-stats admin page - 30-second auto-refresh with animation for new visitors - Color-coded markers (red: active, blue: recent) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -332,3 +332,102 @@ def trigger_aggregation(
|
||||
if result:
|
||||
return {"status": "success", "date": date_str}
|
||||
return {"status": "no_data", "date": date_str}
|
||||
|
||||
|
||||
@router.get("/admin/map-data")
|
||||
def get_visitor_map_data(
|
||||
minutes: int = 30,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""
|
||||
Get visitor locations for map display
|
||||
Returns recent visitors with lat/lng coordinates
|
||||
"""
|
||||
start_time = datetime.utcnow() - timedelta(minutes=minutes)
|
||||
|
||||
# Get recent visitors with coordinates
|
||||
visitors = db.query(
|
||||
VisitorLog.latitude,
|
||||
VisitorLog.longitude,
|
||||
VisitorLog.country,
|
||||
VisitorLog.country_code,
|
||||
VisitorLog.city,
|
||||
VisitorLog.visited_at,
|
||||
VisitorLog.page_path,
|
||||
VisitorLog.device_type,
|
||||
).filter(
|
||||
and_(
|
||||
VisitorLog.visited_at >= start_time,
|
||||
VisitorLog.latitude.isnot(None),
|
||||
VisitorLog.longitude.isnot(None),
|
||||
)
|
||||
).order_by(
|
||||
desc(VisitorLog.visited_at)
|
||||
).limit(100).all()
|
||||
|
||||
# Group by unique location for clustering
|
||||
locations = []
|
||||
location_set = set()
|
||||
|
||||
for v in visitors:
|
||||
# Round coordinates for grouping (roughly city-level)
|
||||
lat_key = round(v.latitude, 2) if v.latitude else 0
|
||||
lng_key = round(v.longitude, 2) if v.longitude else 0
|
||||
location_key = f"{lat_key}:{lng_key}"
|
||||
|
||||
if location_key not in location_set:
|
||||
location_set.add(location_key)
|
||||
locations.append({
|
||||
"lat": v.latitude,
|
||||
"lng": v.longitude,
|
||||
"country": v.country,
|
||||
"country_code": v.country_code,
|
||||
"city": v.city,
|
||||
"last_visit": v.visited_at.isoformat() if v.visited_at else None,
|
||||
"page": v.page_path,
|
||||
"device": v.device_type,
|
||||
"count": 1, # Will be aggregated
|
||||
})
|
||||
else:
|
||||
# Increment count for existing location
|
||||
for loc in locations:
|
||||
if round(loc["lat"], 2) == lat_key and round(loc["lng"], 2) == lng_key:
|
||||
loc["count"] += 1
|
||||
break
|
||||
|
||||
# Get recent activity for animation (last 5 minutes)
|
||||
recent_start = datetime.utcnow() - timedelta(minutes=5)
|
||||
recent_visitors = db.query(
|
||||
VisitorLog.latitude,
|
||||
VisitorLog.longitude,
|
||||
VisitorLog.country,
|
||||
VisitorLog.city,
|
||||
VisitorLog.visited_at,
|
||||
).filter(
|
||||
and_(
|
||||
VisitorLog.visited_at >= recent_start,
|
||||
VisitorLog.latitude.isnot(None),
|
||||
VisitorLog.longitude.isnot(None),
|
||||
)
|
||||
).order_by(
|
||||
desc(VisitorLog.visited_at)
|
||||
).limit(20).all()
|
||||
|
||||
recent = [
|
||||
{
|
||||
"lat": v.latitude,
|
||||
"lng": v.longitude,
|
||||
"country": v.country,
|
||||
"city": v.city,
|
||||
"time": v.visited_at.isoformat() if v.visited_at else None,
|
||||
}
|
||||
for v in recent_visitors
|
||||
]
|
||||
|
||||
return {
|
||||
"locations": locations,
|
||||
"recent": recent,
|
||||
"total_with_coords": len(visitors),
|
||||
"minutes": minutes,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user