import { NextResponse } from "next/server"; import { and, isNotNull } from "drizzle-orm"; import { db } from "@/lib/db"; import { claws } from "@/lib/db/schema"; import { getCacheHeatmap, setCacheHeatmap } from "@/lib/redis"; import { applyDeterministicOffset } from "@/lib/geo/offset"; export async function GET() { try { const cached = await getCacheHeatmap(); if (cached) { return NextResponse.json(JSON.parse(cached)); } const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); // Fetch all claws with base coordinates const clawDetails = await db .select({ id: claws.id, name: claws.name, city: claws.city, country: claws.country, latitude: claws.latitude, longitude: claws.longitude, lastHeartbeat: claws.lastHeartbeat, }) .from(claws) .where( and( isNotNull(claws.latitude), isNotNull(claws.longitude) ) ); // Apply deterministic offset to each claw and group by unique coordinates const pointMap = new Map; onlineCount: number; }>(); for (const claw of clawDetails) { if (!claw.latitude || !claw.longitude) continue; const baseLat = Number(claw.latitude); const baseLng = Number(claw.longitude); // Apply deterministic offset based on claw ID const offset = applyDeterministicOffset(baseLat, baseLng, claw.id); const isOnline = claw.lastHeartbeat ? claw.lastHeartbeat >= fiveMinutesAgo : false; // Use rounded coordinates as key for grouping (within ~100m) const key = `${offset.lat.toFixed(4)}:${offset.lng.toFixed(4)}`; const existing = pointMap.get(key); if (existing) { existing.claws.push({ id: claw.id, name: claw.name, isOnline }); if (isOnline) existing.onlineCount++; } else { pointMap.set(key, { lat: offset.lat, lng: offset.lng, city: claw.city, country: claw.country, claws: [{ id: claw.id, name: claw.name, isOnline }], onlineCount: isOnline ? 1 : 0, }); } } // Convert map to array of points const points = Array.from(pointMap.values()).map((point) => ({ lat: point.lat, lng: point.lng, weight: point.claws.length, clawCount: point.claws.length, onlineCount: point.onlineCount, city: point.city, country: point.country, claws: point.claws.slice(0, 20), // Limit to 20 claws per point for display })); const lastUpdated = new Date().toISOString(); const responseData = { points, lastUpdated }; await setCacheHeatmap(JSON.stringify(responseData)); return NextResponse.json(responseData); } catch (error) { console.error("Heatmap error:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 } ); } }