From 7f4f232c32da3921f66421a441b5cb59c00a114a Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Wed, 21 Jan 2026 12:38:36 +0100 Subject: [PATCH] Added Health Ednpoint for Devices Added Offline Sync for times --- backend/db/schema/devices.ts | 7 +- backend/src/index.ts | 4 +- backend/src/routes/devices/management.ts | 58 +++++++++++++++++ backend/src/routes/devices/rfid.ts | 81 ++++++++++++------------ 4 files changed, 109 insertions(+), 41 deletions(-) create mode 100644 backend/src/routes/devices/management.ts diff --git a/backend/db/schema/devices.ts b/backend/db/schema/devices.ts index 4825826..a2543e3 100644 --- a/backend/db/schema/devices.ts +++ b/backend/db/schema/devices.ts @@ -3,7 +3,7 @@ import { uuid, timestamp, text, - bigint, + bigint, jsonb, } from "drizzle-orm/pg-core" import { tenants } from "./tenants" @@ -23,6 +23,11 @@ export const devices = pgTable("devices", { password: text("password"), externalId: text("externalId"), + + lastSeen: timestamp("last_seen", { withTimezone: true }), + + // Hier speichern wir den ganzen Payload (RSSI, Heap, IP, etc.) + lastDebugInfo: jsonb("last_debug_info"), }) export type Device = typeof devices.$inferSelect diff --git a/backend/src/index.ts b/backend/src/index.ts index 069ca8e..7bff700 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -45,6 +45,7 @@ import staffTimeRoutesInternal from "./routes/internal/time"; //Devices import devicesRFIDRoutes from "./routes/devices/rfid"; +import devicesManagementRoutes from "./routes/devices/management"; import {sendMail} from "./utils/mailer"; @@ -71,7 +72,6 @@ async function main() { // Plugins Global verfügbar await app.register(swaggerPlugin); - await app.register(corsPlugin); await app.register(supabasePlugin); await app.register(tenantPlugin); await app.register(dayjsPlugin); @@ -116,8 +116,10 @@ async function main() { await app.register(async (devicesApp) => { await devicesApp.register(devicesRFIDRoutes) + await devicesApp.register(devicesManagementRoutes) },{prefix: "/devices"}) + await app.register(corsPlugin); //Geschützte Routes diff --git a/backend/src/routes/devices/management.ts b/backend/src/routes/devices/management.ts new file mode 100644 index 0000000..6c98e64 --- /dev/null +++ b/backend/src/routes/devices/management.ts @@ -0,0 +1,58 @@ +import { FastifyInstance } from "fastify"; +import { eq } from "drizzle-orm"; +import { db } from "../../../db"; // <--- PFAD ZUR DB INSTANZ ANPASSEN +import { devices } from "../../../db/schema"; + +// Definition, was wir vom ESP32 erwarten +interface HealthBody { + terminal_id: string; + ip_address?: string; + wifi_rssi?: number; + uptime_seconds?: number; + heap_free?: number; + [key: string]: any; // Erlaubt weitere Felder +} + +export default async function devicesManagementRoutes(server: FastifyInstance) { + server.post<{ Body: HealthBody }>( + "/health", + async (req, reply) => { + try { + const data = req.body; + + // 1. Validierung: Haben wir eine ID? + if (!data.terminal_id) { + console.warn("Health Check ohne terminal_id empfangen:", data); + return reply.code(400).send({ error: "terminal_id missing" }); + } + + console.log(`Health Ping von Device ${data.terminal_id}`, data); + + // 2. Datenbank Update + // Wir suchen das Gerät mit der passenden externalId + const result = await db + .update(devices) + .set({ + lastSeen: new Date(), // Setzt Zeit auf JETZT + lastDebugInfo: data // Speichert das ganze JSON + }) + .where(eq(devices.externalId, data.terminal_id)) + .returning({ id: devices.id }); // Gibt ID zurück, falls gefunden + + // 3. Checken ob Gerät gefunden wurde + if (result.length === 0) { + console.warn(`Unbekanntes Terminal versucht Health Check: ${data.terminal_id}`); + // Optional: 404 senden oder ignorieren (Sicherheit) + return reply.code(404).send({ error: "Device not found" }); + } + + // Alles OK + return reply.code(200).send({ status: "ok" }); + + } catch (err: any) { + console.error("Health Check Error:", err); + return reply.code(500).send({ error: err.message }); + } + } + ); +} \ No newline at end of file diff --git a/backend/src/routes/devices/rfid.ts b/backend/src/routes/devices/rfid.ts index 2931c07..d3c3d45 100644 --- a/backend/src/routes/devices/rfid.ts +++ b/backend/src/routes/devices/rfid.ts @@ -1,37 +1,39 @@ import { FastifyInstance } from "fastify"; -import {and, desc, eq} from "drizzle-orm"; -import {authProfiles, devices, stafftimeevents} from "../../../db/schema"; +import { and, desc, eq } from "drizzle-orm"; +import { authProfiles, devices, stafftimeevents } from "../../../db/schema"; export default async function devicesRFIDRoutes(server: FastifyInstance) { server.post( "/rfid/createevent/:terminal_id", async (req, reply) => { try { + // 1. Timestamp aus dem Body holen (optional) + const { rfid_id, timestamp } = req.body as { + rfid_id: string, + timestamp?: number // Kann undefined sein (Live) oder Zahl (Offline) + }; - const {rfid_id} = req.body as {rfid_id: string}; - const {terminal_id} = req.params as {terminal_id: string}; + const { terminal_id } = req.params as { terminal_id: string }; - if(!rfid_id ||!terminal_id) { + if (!rfid_id || !terminal_id) { console.log(`Missing Params`); - return reply.code(400).send(`Missing Params`) + return reply.code(400).send(`Missing Params`); } + // 2. Gerät suchen const device = await server.db .select() .from(devices) - .where( - eq(devices.externalId, terminal_id) - - ) + .where(eq(devices.externalId, terminal_id)) .limit(1) .then(rows => rows[0]); - if(!device) { + if (!device) { console.log(`Device ${terminal_id} not found`); - return reply.code(400).send(`Device ${terminal_id} not found`) - + return reply.code(400).send(`Device ${terminal_id} not found`); } + // 3. User-Profil suchen const profile = await server.db .select() .from(authProfiles) @@ -44,55 +46,56 @@ export default async function devicesRFIDRoutes(server: FastifyInstance) { .limit(1) .then(rows => rows[0]); - if(!profile) { + if (!profile) { console.log(`Profile for Token ${rfid_id} not found`); - return reply.code(400).send(`Profile for Token ${rfid_id} not found`) - + return reply.code(400).send(`Profile for Token ${rfid_id} not found`); } + // 4. Letztes Event suchen (für Status-Toggle Work Start/End) const lastEvent = await server.db .select() .from(stafftimeevents) - .where( - eq(stafftimeevents.user_id, profile.user_id) - ) - .orderBy(desc(stafftimeevents.eventtime)) // <-- Sortierung: Neuestes zuerst + .where(eq(stafftimeevents.user_id, profile.user_id)) + .orderBy(desc(stafftimeevents.eventtime)) .limit(1) .then(rows => rows[0]); - console.log(lastEvent) + // 5. Zeitstempel Logik (WICHTIG!) + // Der ESP32 sendet Unix-Timestamp in SEKUNDEN. JS braucht MILLISEKUNDEN. + // Wenn kein Timestamp kommt (0 oder undefined), nehmen wir JETZT. + const actualEventTime = (timestamp && timestamp > 0) + ? new Date(timestamp * 1000) + : new Date(); + // 6. Event Typ bestimmen (Toggle Logik) + // Falls noch nie gestempelt wurde (lastEvent undefined), fangen wir mit start an. + const nextEventType = (lastEvent?.eventtype === "work_start") + ? "work_end" + : "work_start"; const dataToInsert = { tenant_id: device.tenant, user_id: profile.user_id, actortype: "system", - eventtime: new Date(), - eventtype: lastEvent.eventtype === "work_start" ? "work_end" : "work_start", - source: "WEB" - } + eventtime: actualEventTime, // Hier nutzen wir die berechnete Zeit + eventtype: nextEventType, + source: "TERMINAL" // Habe ich von WEB auf TERMINAL geändert (optional) + }; - console.log(dataToInsert) + console.log(`New Event for ${profile.user_id}: ${nextEventType} @ ${actualEventTime.toISOString()}`); const [created] = await server.db .insert(stafftimeevents) //@ts-ignore .values(dataToInsert) - .returning() + .returning(); + + return created; - return created } catch (err: any) { - console.error(err) - return reply.code(400).send({ error: err.message }) + console.error(err); + return reply.code(400).send({ error: err.message }); } - - - - console.log(req.body) - - return - - } ); -} +} \ No newline at end of file