first commit
This commit is contained in:
73
app/api/locations/cleanup/route.ts
Normal file
73
app/api/locations/cleanup/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { locationDb } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* POST /api/locations/cleanup (ADMIN only)
|
||||
*
|
||||
* Delete old location records and optimize database
|
||||
*
|
||||
* Body:
|
||||
* {
|
||||
* "retentionHours": 168 // 7 days default
|
||||
* }
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Super admin only (username "admin")
|
||||
const session = await auth();
|
||||
const username = session?.user?.name || '';
|
||||
if (!session?.user || username !== 'admin') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
const body = await request.json();
|
||||
const retentionHours = body.retentionHours || 168; // Default: 7 days
|
||||
|
||||
// Validate retention period
|
||||
if (retentionHours <= 0 || retentionHours > 8760) { // Max 1 year
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid retention period. Must be between 1 and 8760 hours (1 year)' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get stats before cleanup
|
||||
const statsBefore = locationDb.getStats();
|
||||
|
||||
// Delete old records
|
||||
const deletedCount = locationDb.deleteOlderThan(retentionHours);
|
||||
|
||||
// Get stats after cleanup
|
||||
const statsAfter = locationDb.getStats();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
deleted: deletedCount,
|
||||
retentionHours,
|
||||
retentionDays: Math.round(retentionHours / 24),
|
||||
before: {
|
||||
total: statsBefore.total,
|
||||
sizeKB: statsBefore.sizeKB,
|
||||
oldest: statsBefore.oldest,
|
||||
newest: statsBefore.newest,
|
||||
},
|
||||
after: {
|
||||
total: statsAfter.total,
|
||||
sizeKB: statsAfter.sizeKB,
|
||||
oldest: statsAfter.oldest,
|
||||
newest: statsAfter.newest,
|
||||
},
|
||||
freedKB: statsBefore.sizeKB - statsAfter.sizeKB,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Cleanup error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to cleanup locations',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
101
app/api/locations/ingest/route.ts
Normal file
101
app/api/locations/ingest/route.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { locationDb, Location } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* POST /api/locations/ingest
|
||||
*
|
||||
* Endpoint for n8n to push location data to local SQLite cache.
|
||||
* This is called AFTER n8n stores the data in NocoDB.
|
||||
*
|
||||
* Expected payload (single location or array):
|
||||
* {
|
||||
* "latitude": 48.1351,
|
||||
* "longitude": 11.5820,
|
||||
* "timestamp": "2024-01-15T10:30:00Z",
|
||||
* "user_id": 0,
|
||||
* "username": "10",
|
||||
* "marker_label": "Device A",
|
||||
* "battery": 85,
|
||||
* "speed": 2.5,
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* Security: Add API key validation in production!
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Support both single location and array of locations
|
||||
const locations = Array.isArray(body) ? body : [body];
|
||||
|
||||
if (locations.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No location data provided' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Debug logging for speed and battery values
|
||||
console.log('[Ingest Debug] Received locations:', locations.map(loc => ({
|
||||
username: loc.username,
|
||||
speed: loc.speed,
|
||||
speed_type: typeof loc.speed,
|
||||
battery: loc.battery,
|
||||
battery_type: typeof loc.battery
|
||||
})));
|
||||
|
||||
// Validate required fields
|
||||
for (const loc of locations) {
|
||||
if (!loc.latitude || !loc.longitude || !loc.timestamp) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: latitude, longitude, timestamp' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert into SQLite
|
||||
let insertedCount = 0;
|
||||
if (locations.length === 1) {
|
||||
locationDb.create(locations[0] as Location);
|
||||
insertedCount = 1;
|
||||
} else {
|
||||
insertedCount = locationDb.createMany(locations as Location[]);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
inserted: insertedCount,
|
||||
message: `Successfully stored ${insertedCount} location(s)`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Location ingest error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to store location data',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/locations/ingest/stats
|
||||
*
|
||||
* Get database statistics (for debugging)
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
const stats = locationDb.getStats();
|
||||
return NextResponse.json(stats);
|
||||
} catch (error) {
|
||||
console.error('Stats error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get stats' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
62
app/api/locations/optimize/route.ts
Normal file
62
app/api/locations/optimize/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { getLocationsDb } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* POST /api/locations/optimize (ADMIN only)
|
||||
*
|
||||
* Optimize database by running VACUUM and ANALYZE
|
||||
* This reclaims unused space and updates query planner statistics
|
||||
*/
|
||||
export async function POST() {
|
||||
try {
|
||||
// Super admin only (username "admin")
|
||||
const session = await auth();
|
||||
const username = session?.user?.name || '';
|
||||
if (!session?.user || username !== 'admin') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
const db = getLocationsDb();
|
||||
|
||||
// Get size before optimization
|
||||
const sizeBefore = db.prepare(
|
||||
"SELECT page_count * page_size / 1024 / 1024.0 as sizeMB FROM pragma_page_count(), pragma_page_size()"
|
||||
).get() as { sizeMB: number };
|
||||
|
||||
// Run VACUUM to reclaim space
|
||||
db.exec('VACUUM');
|
||||
|
||||
// Run ANALYZE to update query planner statistics
|
||||
db.exec('ANALYZE');
|
||||
|
||||
// Get size after optimization
|
||||
const sizeAfter = db.prepare(
|
||||
"SELECT page_count * page_size / 1024 / 1024.0 as sizeMB FROM pragma_page_count(), pragma_page_size()"
|
||||
).get() as { sizeMB: number };
|
||||
|
||||
db.close();
|
||||
|
||||
const freedMB = sizeBefore.sizeMB - sizeAfter.sizeMB;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
before: {
|
||||
sizeMB: Math.round(sizeBefore.sizeMB * 100) / 100,
|
||||
},
|
||||
after: {
|
||||
sizeMB: Math.round(sizeAfter.sizeMB * 100) / 100,
|
||||
},
|
||||
freedMB: Math.round(freedMB * 100) / 100,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Optimize error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to optimize database',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
230
app/api/locations/route.ts
Normal file
230
app/api/locations/route.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import type { LocationResponse } from "@/types/location";
|
||||
import { locationDb, Location, deviceDb, userDb } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
const N8N_API_URL = process.env.N8N_API_URL || "https://n8n.example.com/webhook/location";
|
||||
|
||||
/**
|
||||
* GET /api/locations
|
||||
*
|
||||
* Hybrid approach:
|
||||
* 1. Fetch fresh data from n8n webhook
|
||||
* 2. Store new locations in local SQLite cache
|
||||
* 3. Return filtered data from SQLite (enables 24h+ history)
|
||||
*
|
||||
* Query parameters:
|
||||
* - username: Filter by device tracker ID
|
||||
* - timeRangeHours: Filter by time range (e.g., 1, 3, 6, 12, 24)
|
||||
* - startTime: Custom range start (ISO string)
|
||||
* - endTime: Custom range end (ISO string)
|
||||
* - limit: Maximum number of records (default: 1000)
|
||||
* - sync: Set to 'false' to skip n8n fetch and read only from cache
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user's allowed device IDs for filtering locations
|
||||
const userId = (session.user as any).id;
|
||||
const role = (session.user as any).role;
|
||||
const sessionUsername = session.user.name || '';
|
||||
|
||||
// Get list of device IDs the user is allowed to access
|
||||
const userDeviceIds = userDb.getAllowedDeviceIds(userId, role, sessionUsername);
|
||||
|
||||
// If user has no devices, return empty response
|
||||
if (userDeviceIds.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
current: null,
|
||||
history: [],
|
||||
total_points: 0,
|
||||
last_updated: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const username = searchParams.get('username') || undefined;
|
||||
const timeRangeHours = searchParams.get('timeRangeHours')
|
||||
? parseInt(searchParams.get('timeRangeHours')!, 10)
|
||||
: undefined;
|
||||
const startTime = searchParams.get('startTime') || undefined;
|
||||
const endTime = searchParams.get('endTime') || undefined;
|
||||
const limit = searchParams.get('limit')
|
||||
? parseInt(searchParams.get('limit')!, 10)
|
||||
: 1000;
|
||||
const sync = searchParams.get('sync') !== 'false'; // Default: true
|
||||
|
||||
// Variable to store n8n data as fallback
|
||||
let n8nData: LocationResponse | null = null;
|
||||
|
||||
// Step 1: Optionally fetch and sync from n8n
|
||||
if (sync) {
|
||||
try {
|
||||
const response = await fetch(N8N_API_URL, {
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(3000), // 3 second timeout
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data: LocationResponse = await response.json();
|
||||
|
||||
// Debug: Log first location from n8n
|
||||
if (data.history && data.history.length > 0) {
|
||||
console.log('[N8N Debug] First location from n8n:', {
|
||||
username: data.history[0].username,
|
||||
speed: data.history[0].speed,
|
||||
speed_type: typeof data.history[0].speed,
|
||||
speed_exists: 'speed' in data.history[0],
|
||||
battery: data.history[0].battery,
|
||||
battery_type: typeof data.history[0].battery,
|
||||
battery_exists: 'battery' in data.history[0]
|
||||
});
|
||||
}
|
||||
|
||||
// Normalize data: Ensure speed and battery fields exist (treat 0 explicitly)
|
||||
if (data.history && Array.isArray(data.history)) {
|
||||
data.history = data.history.map(loc => {
|
||||
// Generate display_time in German locale (Europe/Berlin timezone)
|
||||
const displayTime = new Date(loc.timestamp).toLocaleString('de-DE', {
|
||||
timeZone: 'Europe/Berlin',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
|
||||
return {
|
||||
...loc,
|
||||
display_time: displayTime,
|
||||
// Explicit handling: 0 is valid, only undefined/null → null
|
||||
speed: typeof loc.speed === 'number' ? loc.speed : (loc.speed !== undefined && loc.speed !== null ? Number(loc.speed) : null),
|
||||
battery: typeof loc.battery === 'number' ? loc.battery : (loc.battery !== undefined && loc.battery !== null ? Number(loc.battery) : null),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Store n8n data for fallback
|
||||
n8nData = data;
|
||||
|
||||
// Store new locations in SQLite
|
||||
if (data.history && Array.isArray(data.history) && data.history.length > 0) {
|
||||
// Get latest timestamp from our DB
|
||||
const stats = locationDb.getStats();
|
||||
const lastLocalTimestamp = stats.newest || '1970-01-01T00:00:00Z';
|
||||
|
||||
// Filter for only newer locations
|
||||
const newLocations = data.history.filter(loc =>
|
||||
loc.timestamp > lastLocalTimestamp
|
||||
);
|
||||
|
||||
if (newLocations.length > 0) {
|
||||
const inserted = locationDb.createMany(newLocations as Location[]);
|
||||
console.log(`[Location Sync] Inserted ${inserted} new locations from n8n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (syncError) {
|
||||
// n8n not reachable - that's ok, we'll use cached data
|
||||
console.warn('[Location Sync] n8n webhook not reachable, using cache only:',
|
||||
syncError instanceof Error ? syncError.message : 'Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Read from local SQLite with filters
|
||||
let locations = locationDb.findMany({
|
||||
user_id: 0, // Always filter for MQTT devices
|
||||
username,
|
||||
timeRangeHours,
|
||||
startTime,
|
||||
endTime,
|
||||
limit,
|
||||
});
|
||||
|
||||
// Filter locations to only include user's devices
|
||||
locations = locations.filter(loc => loc.username && userDeviceIds.includes(loc.username));
|
||||
|
||||
// Step 3: If DB is empty, use n8n data as fallback
|
||||
if (locations.length === 0 && n8nData && n8nData.history) {
|
||||
console.log('[API] DB empty, using n8n data as fallback');
|
||||
// Filter n8n data if needed
|
||||
let filteredHistory = n8nData.history;
|
||||
|
||||
// Filter by user's devices
|
||||
filteredHistory = filteredHistory.filter(loc => loc.username && userDeviceIds.includes(loc.username));
|
||||
|
||||
if (username) {
|
||||
filteredHistory = filteredHistory.filter(loc => loc.username === username);
|
||||
}
|
||||
|
||||
// Apply time filters
|
||||
if (startTime && endTime) {
|
||||
// Custom range
|
||||
filteredHistory = filteredHistory.filter(loc =>
|
||||
loc.timestamp >= startTime && loc.timestamp <= endTime
|
||||
);
|
||||
} else if (timeRangeHours) {
|
||||
// Quick filter
|
||||
const cutoffTime = new Date(Date.now() - timeRangeHours * 60 * 60 * 1000).toISOString();
|
||||
filteredHistory = filteredHistory.filter(loc => loc.timestamp >= cutoffTime);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...n8nData,
|
||||
history: filteredHistory,
|
||||
total_points: filteredHistory.length,
|
||||
});
|
||||
}
|
||||
|
||||
// Normalize locations: Ensure speed, battery, and display_time are correct
|
||||
locations = locations.map(loc => {
|
||||
// Generate display_time if missing or regenerate from timestamp
|
||||
const displayTime = loc.display_time || new Date(loc.timestamp).toLocaleString('de-DE', {
|
||||
timeZone: 'Europe/Berlin',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
|
||||
return {
|
||||
...loc,
|
||||
display_time: displayTime,
|
||||
speed: loc.speed !== undefined && loc.speed !== null ? Number(loc.speed) : null,
|
||||
battery: loc.battery !== undefined && loc.battery !== null ? Number(loc.battery) : null,
|
||||
};
|
||||
});
|
||||
|
||||
// Get actual total count from database (not limited by 'limit' parameter)
|
||||
const stats = locationDb.getStats();
|
||||
|
||||
// Step 4: Return data in n8n-compatible format
|
||||
const response: LocationResponse = {
|
||||
success: true,
|
||||
current: locations.length > 0 ? locations[0] : null,
|
||||
history: locations,
|
||||
total_points: stats.total, // Use actual total from DB, not limited results
|
||||
last_updated: locations.length > 0 ? locations[0].timestamp : new Date().toISOString(),
|
||||
};
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching locations:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Failed to fetch locations",
|
||||
details: error instanceof Error ? error.message : "Unknown error"
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
77
app/api/locations/stats/route.ts
Normal file
77
app/api/locations/stats/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getLocationsDb } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* GET /api/locations/stats
|
||||
*
|
||||
* Get detailed database statistics
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
const db = getLocationsDb();
|
||||
|
||||
// Overall stats
|
||||
const totalCount = db.prepare('SELECT COUNT(*) as count FROM Location').get() as { count: number };
|
||||
|
||||
// Time range
|
||||
const timeRange = db.prepare(
|
||||
'SELECT MIN(timestamp) as oldest, MAX(timestamp) as newest FROM Location'
|
||||
).get() as { oldest: string | null; newest: string | null };
|
||||
|
||||
// Database size
|
||||
const dbSize = db.prepare(
|
||||
"SELECT page_count * page_size / 1024 / 1024.0 as sizeMB FROM pragma_page_count(), pragma_page_size()"
|
||||
).get() as { sizeMB: number };
|
||||
|
||||
// WAL mode check
|
||||
const walMode = db.prepare("PRAGMA journal_mode").get() as { journal_mode: string };
|
||||
|
||||
// Locations per device
|
||||
const perDevice = db.prepare(`
|
||||
SELECT username, COUNT(*) as count
|
||||
FROM Location
|
||||
WHERE user_id = 0
|
||||
GROUP BY username
|
||||
ORDER BY count DESC
|
||||
`).all() as Array<{ username: string; count: number }>;
|
||||
|
||||
// Locations per day (last 7 days)
|
||||
const perDay = db.prepare(`
|
||||
SELECT
|
||||
DATE(timestamp) as date,
|
||||
COUNT(*) as count
|
||||
FROM Location
|
||||
WHERE timestamp >= datetime('now', '-7 days')
|
||||
GROUP BY DATE(timestamp)
|
||||
ORDER BY date DESC
|
||||
`).all() as Array<{ date: string; count: number }>;
|
||||
|
||||
// Average locations per day
|
||||
const avgPerDay = perDay.length > 0
|
||||
? Math.round(perDay.reduce((sum, day) => sum + day.count, 0) / perDay.length)
|
||||
: 0;
|
||||
|
||||
db.close();
|
||||
|
||||
return NextResponse.json({
|
||||
total: totalCount.count,
|
||||
oldest: timeRange.oldest,
|
||||
newest: timeRange.newest,
|
||||
sizeMB: Math.round(dbSize.sizeMB * 100) / 100,
|
||||
walMode: walMode.journal_mode,
|
||||
perDevice,
|
||||
perDay,
|
||||
avgPerDay,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Stats error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to get database stats',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
86
app/api/locations/sync/route.ts
Normal file
86
app/api/locations/sync/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { locationDb, Location } from '@/lib/db';
|
||||
import type { LocationResponse } from "@/types/location";
|
||||
|
||||
const N8N_API_URL = process.env.N8N_API_URL || "https://n8n.example.com/webhook/location";
|
||||
|
||||
/**
|
||||
* POST /api/locations/sync (ADMIN only)
|
||||
*
|
||||
* Manually sync location data from n8n webhook to local SQLite cache.
|
||||
* This fetches all available data from n8n and stores only new records.
|
||||
*
|
||||
* Useful for:
|
||||
* - Initial database population
|
||||
* - Recovery after downtime
|
||||
* - Manual refresh
|
||||
*/
|
||||
export async function POST() {
|
||||
try {
|
||||
// ADMIN only
|
||||
const session = await auth();
|
||||
if (!session?.user || (session.user as any).role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
// Get stats before sync
|
||||
const statsBefore = locationDb.getStats();
|
||||
|
||||
// Fetch from n8n webhook
|
||||
const response = await fetch(N8N_API_URL, {
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(10000), // 10 second timeout for manual sync
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`n8n webhook returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data: LocationResponse = await response.json();
|
||||
|
||||
let insertedCount = 0;
|
||||
|
||||
// Store new locations in SQLite
|
||||
if (data.history && Array.isArray(data.history) && data.history.length > 0) {
|
||||
// Get latest timestamp from our DB
|
||||
const lastLocalTimestamp = statsBefore.newest || '1970-01-01T00:00:00Z';
|
||||
|
||||
// Filter for only newer locations
|
||||
const newLocations = data.history.filter(loc =>
|
||||
loc.timestamp > lastLocalTimestamp
|
||||
);
|
||||
|
||||
if (newLocations.length > 0) {
|
||||
insertedCount = locationDb.createMany(newLocations as Location[]);
|
||||
console.log(`[Manual Sync] Inserted ${insertedCount} new locations from n8n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get stats after sync
|
||||
const statsAfter = locationDb.getStats();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
synced: insertedCount,
|
||||
n8nTotal: data.total_points || data.history.length,
|
||||
before: {
|
||||
total: statsBefore.total,
|
||||
newest: statsBefore.newest,
|
||||
},
|
||||
after: {
|
||||
total: statsAfter.total,
|
||||
newest: statsAfter.newest,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Sync error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to sync locations',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
91
app/api/locations/test/route.ts
Normal file
91
app/api/locations/test/route.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { locationDb } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* POST /api/locations/test
|
||||
*
|
||||
* Create a test location entry (for development/testing)
|
||||
* Body: { username, latitude, longitude, speed?, battery? }
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { username, latitude, longitude, speed, battery } = body;
|
||||
|
||||
// Validation
|
||||
if (!username || latitude === undefined || longitude === undefined) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: username, latitude, longitude' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const lat = parseFloat(latitude);
|
||||
const lon = parseFloat(longitude);
|
||||
|
||||
if (isNaN(lat) || isNaN(lon)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid latitude or longitude' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (lat < -90 || lat > 90) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Latitude must be between -90 and 90' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (lon < -180 || lon > 180) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Longitude must be between -180 and 180' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create location
|
||||
const now = new Date();
|
||||
const location = locationDb.create({
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
timestamp: now.toISOString(),
|
||||
user_id: 0,
|
||||
username: String(username),
|
||||
display_time: now.toLocaleString('de-DE', {
|
||||
timeZone: 'Europe/Berlin',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}),
|
||||
chat_id: 0,
|
||||
first_name: null,
|
||||
last_name: null,
|
||||
marker_label: null,
|
||||
battery: battery !== undefined ? Number(battery) : null,
|
||||
speed: speed !== undefined ? Number(speed) : null,
|
||||
});
|
||||
|
||||
if (!location) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create location (possibly duplicate)' },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
location,
|
||||
message: 'Test location created successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Test location creation error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create test location' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user