feat: add edge device endpoints (ingest, heartbeat, alert)
This commit is contained in:
parent
b520ffc578
commit
2ca6fb01f4
1 changed files with 185 additions and 0 deletions
|
|
@ -415,4 +415,189 @@ export async function environmentRoutes(fastify: FastifyInstance) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== EDGE DEVICE ENDPOINTS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /ingest
|
||||||
|
* Batch ingest readings from edge device
|
||||||
|
* This endpoint accepts API key auth (not JWT) for edge devices
|
||||||
|
*/
|
||||||
|
fastify.post('/ingest', {
|
||||||
|
config: { rawBody: true },
|
||||||
|
preHandler: async (request, reply) => {
|
||||||
|
// Allow API key auth for edge devices
|
||||||
|
const authHeader = request.headers.authorization;
|
||||||
|
if (authHeader?.startsWith('Bearer ')) {
|
||||||
|
const apiKey = authHeader.substring(7);
|
||||||
|
// TODO: Validate API key against facility configuration
|
||||||
|
// For now, accept any non-empty key
|
||||||
|
if (!apiKey) {
|
||||||
|
return reply.status(401).send({ error: 'Invalid API key' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const { facilityId, readings } = request.body as {
|
||||||
|
facilityId?: string;
|
||||||
|
readings: Array<{
|
||||||
|
roomId: string;
|
||||||
|
sensorId?: string;
|
||||||
|
temperature: number;
|
||||||
|
humidity: number;
|
||||||
|
dewpoint?: number;
|
||||||
|
vpd?: number;
|
||||||
|
timestamp: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!readings || !Array.isArray(readings) || readings.length === 0) {
|
||||||
|
return reply.status(400).send({ error: 'No readings provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store readings in batch
|
||||||
|
const created = await prisma.$transaction(
|
||||||
|
readings.map(r =>
|
||||||
|
prisma.sensorReading.create({
|
||||||
|
data: {
|
||||||
|
sensorId: r.sensorId || 'edge-default',
|
||||||
|
value: r.temperature, // Primary value is temperature
|
||||||
|
unit: '°F',
|
||||||
|
timestamp: new Date(r.timestamp),
|
||||||
|
metadata: {
|
||||||
|
humidity: r.humidity,
|
||||||
|
dewpoint: r.dewpoint,
|
||||||
|
vpd: r.vpd,
|
||||||
|
roomId: r.roomId,
|
||||||
|
facilityId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fastify.log.info(`Ingested ${created.length} readings from edge device`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
count: created.length,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
fastify.log.error(error);
|
||||||
|
return reply.status(500).send({ error: 'Failed to ingest readings' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /heartbeat
|
||||||
|
* Edge device heartbeat for monitoring
|
||||||
|
*/
|
||||||
|
fastify.post('/heartbeat', {
|
||||||
|
preHandler: async (request, reply) => {
|
||||||
|
const authHeader = request.headers.authorization;
|
||||||
|
if (!authHeader?.startsWith('Bearer ')) {
|
||||||
|
return reply.status(401).send({ error: 'API key required' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const data = request.body as {
|
||||||
|
facilityId: string;
|
||||||
|
edgeId: string;
|
||||||
|
status: 'ok' | 'degraded' | 'error';
|
||||||
|
sensorCount: number;
|
||||||
|
bufferSize: number;
|
||||||
|
lastReading?: string;
|
||||||
|
uptime: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log heartbeat (could store in Redis for real-time monitoring)
|
||||||
|
fastify.log.info({
|
||||||
|
event: 'edge_heartbeat',
|
||||||
|
...data,
|
||||||
|
receivedAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Store in Redis or create alert if status is not 'ok'
|
||||||
|
// For now, just acknowledge
|
||||||
|
if (data.status !== 'ok') {
|
||||||
|
fastify.log.warn(`Edge device ${data.edgeId} status: ${data.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ack: true,
|
||||||
|
serverTime: new Date().toISOString()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
fastify.log.error(error);
|
||||||
|
return reply.status(500).send({ error: 'Failed to process heartbeat' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /alert
|
||||||
|
* Edge device alert relay (for notification fan-out)
|
||||||
|
*/
|
||||||
|
fastify.post('/alert', {
|
||||||
|
preHandler: async (request, reply) => {
|
||||||
|
const authHeader = request.headers.authorization;
|
||||||
|
if (!authHeader?.startsWith('Bearer ')) {
|
||||||
|
return reply.status(401).send({ error: 'API key required' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const data = request.body as {
|
||||||
|
facilityId: string;
|
||||||
|
edgeId: string;
|
||||||
|
alertType: string;
|
||||||
|
sensorId: string;
|
||||||
|
sensorName: string;
|
||||||
|
currentValue: number;
|
||||||
|
threshold: number;
|
||||||
|
timestamp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create alert in database
|
||||||
|
const alert = await prisma.environmentAlert.create({
|
||||||
|
data: {
|
||||||
|
type: data.alertType,
|
||||||
|
severity: data.alertType.includes('HIGH') ? 'WARNING' : 'INFO',
|
||||||
|
message: `${data.sensorName}: ${data.alertType.replace('_', ' ')} (${data.currentValue} vs threshold ${data.threshold})`,
|
||||||
|
value: data.currentValue,
|
||||||
|
threshold: data.threshold,
|
||||||
|
metadata: {
|
||||||
|
facilityId: data.facilityId,
|
||||||
|
edgeId: data.edgeId,
|
||||||
|
sensorId: data.sensorId,
|
||||||
|
sensorName: data.sensorName,
|
||||||
|
edgeTimestamp: data.timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.log.warn({
|
||||||
|
event: 'edge_alert',
|
||||||
|
alertId: alert.id,
|
||||||
|
...data
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Fan out to notification channels (in-app notifications)
|
||||||
|
// This will integrate with the notification service for mobile push
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
alertId: alert.id,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
fastify.log.error(error);
|
||||||
|
return reply.status(500).send({ error: 'Failed to process alert' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue