generated from Dicken/dickendock
Commit 6: Adminbereich (TanStack Router, 8 Menüpunkte, Scan-Logs)
This commit is contained in:
@@ -66,5 +66,14 @@ export function ensureSchema(): void {
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scan_logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
target_id TEXT,
|
||||
level TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
52
apps/backend/src/db/repositories/logs.ts
Normal file
52
apps/backend/src/db/repositories/logs.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { desc } from "drizzle-orm";
|
||||
import type { ScanLogEntry } from "@launchpad/shared";
|
||||
import { db } from "../client.js";
|
||||
import { scanLogs } from "../schema.js";
|
||||
|
||||
function mapRow(row: typeof scanLogs.$inferSelect): ScanLogEntry {
|
||||
return {
|
||||
id: row.id,
|
||||
type: row.type as ScanLogEntry["type"],
|
||||
targetId: row.targetId,
|
||||
level: row.level as ScanLogEntry["level"],
|
||||
message: row.message,
|
||||
createdAt: row.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
export interface LogScanInput {
|
||||
type: ScanLogEntry["type"];
|
||||
targetId?: string | null;
|
||||
level: ScanLogEntry["level"];
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** Schreibt einen Eintrag für einen Scan-Versuch (Erfolg oder Fehler). */
|
||||
export function logScan(input: LogScanInput): ScanLogEntry {
|
||||
const id = randomUUID();
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
db.insert(scanLogs)
|
||||
.values({
|
||||
id,
|
||||
type: input.type,
|
||||
targetId: input.targetId ?? null,
|
||||
level: input.level,
|
||||
message: input.message,
|
||||
createdAt,
|
||||
})
|
||||
.run();
|
||||
|
||||
return { id, type: input.type, targetId: input.targetId ?? null, level: input.level, message: input.message, createdAt };
|
||||
}
|
||||
|
||||
export function listLogs(limit = 100): ScanLogEntry[] {
|
||||
return db
|
||||
.select()
|
||||
.from(scanLogs)
|
||||
.orderBy(desc(scanLogs.createdAt))
|
||||
.limit(limit)
|
||||
.all()
|
||||
.map(mapRow);
|
||||
}
|
||||
@@ -58,3 +58,16 @@ export const categories = sqliteTable("categories", {
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Protokoll jedes Scan-Versuchs (Geräte-Netzwerk-Scan oder FritzBox-Scan),
|
||||
* angezeigt im Adminbereich unter "Logs".
|
||||
*/
|
||||
export const scanLogs = sqliteTable("scan_logs", {
|
||||
id: text("id").primaryKey(),
|
||||
type: text("type").notNull(), // "device" | "fritzbox"
|
||||
targetId: text("target_id"), // Geräte-ID bei type "device", sonst null
|
||||
level: text("level").notNull(), // "info" | "error"
|
||||
message: text("message").notNull(),
|
||||
createdAt: text("created_at").notNull(),
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { deviceRoutes } from "./routes/devices.js";
|
||||
import { serviceRoutes } from "./routes/services.js";
|
||||
import { categoryRoutes } from "./routes/categories.js";
|
||||
import { scanRoutes } from "./routes/scan.js";
|
||||
import { logRoutes } from "./routes/logs.js";
|
||||
|
||||
const PORT = Number(process.env.PORT ?? 3001);
|
||||
const HOST = process.env.HOST ?? "0.0.0.0";
|
||||
@@ -33,6 +34,7 @@ async function main() {
|
||||
await app.register(serviceRoutes);
|
||||
await app.register(categoryRoutes);
|
||||
await app.register(scanRoutes);
|
||||
await app.register(logRoutes);
|
||||
|
||||
app.get("/", async () => {
|
||||
return { name: "LaunchPad API", status: "running" };
|
||||
|
||||
10
apps/backend/src/routes/logs.ts
Normal file
10
apps/backend/src/routes/logs.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import * as logRepo from "../db/repositories/logs.js";
|
||||
|
||||
export async function logRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.get("/api/logs", async (request) => {
|
||||
const query = request.query as { limit?: string };
|
||||
const limit = query.limit ? Math.min(Number(query.limit), 500) : 100;
|
||||
return logRepo.listLogs(limit);
|
||||
});
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import * as deviceRepo from "../db/repositories/devices.js";
|
||||
import * as serviceRepo from "../db/repositories/services.js";
|
||||
import * as logRepo from "../db/repositories/logs.js";
|
||||
import { scanDeviceServices } from "../scanner/networkScanner.js";
|
||||
import { fetchFritzBoxHosts } from "../scanner/fritzbox.js";
|
||||
|
||||
/**
|
||||
* Scan-Endpunkte. Werden ausschließlich manuell per Knopfdruck ("Jetzt
|
||||
* scannen") aus der Admin-UI (Commit 6) ausgelöst – es gibt keinerlei
|
||||
* automatischen/zeitgesteuerten Scan.
|
||||
* scannen") aus der Admin-UI ausgelöst – es gibt keinerlei automatischen/
|
||||
* zeitgesteuerten Scan. Jeder Versuch (Erfolg oder Fehler) wird in scan_logs
|
||||
* protokolliert und ist unter Admin -> Logs einsehbar.
|
||||
*/
|
||||
export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
// Netzwerk-Scan für ein einzelnes, bereits bekanntes Gerät: DNS-Kandidaten,
|
||||
// Port 80/443 + typische Ports, Titel/Favicon, Softwareerkennung.
|
||||
app.post("/api/scan/devices/:id", async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const device = deviceRepo.getDevice(id);
|
||||
@@ -19,39 +19,61 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.code(404).send({ error: "Gerät nicht gefunden" });
|
||||
}
|
||||
|
||||
const discovered = await scanDeviceServices(device);
|
||||
try {
|
||||
const discovered = await scanDeviceServices(device);
|
||||
|
||||
const results = discovered.map((found) =>
|
||||
serviceRepo.upsertServiceFromScan({
|
||||
const results = discovered.map((found) =>
|
||||
serviceRepo.upsertServiceFromScan({
|
||||
deviceId: device.id,
|
||||
hostname: found.hostname,
|
||||
url: found.url,
|
||||
https: found.https,
|
||||
port: found.port,
|
||||
favicon: found.favicon,
|
||||
description: found.description,
|
||||
suggestedDisplayName: found.suggestedDisplayName,
|
||||
suggestedCategory: found.category,
|
||||
})
|
||||
);
|
||||
|
||||
deviceRepo.upsertDeviceFromScan({
|
||||
hostname: device.hostname,
|
||||
ip: device.ip,
|
||||
mac: device.mac,
|
||||
manufacturer: device.manufacturer,
|
||||
model: device.model,
|
||||
online: discovered.length > 0,
|
||||
source: device.source,
|
||||
});
|
||||
|
||||
const created = results.filter((r) => r.created).length;
|
||||
const updated = results.filter((r) => !r.created).length;
|
||||
|
||||
logRepo.logScan({
|
||||
type: "device",
|
||||
targetId: device.id,
|
||||
level: "info",
|
||||
message: `${device.hostname} (${device.ip}): ${discovered.length} Dienst(e) gefunden, ${created} neu, ${updated} aktualisiert`,
|
||||
});
|
||||
|
||||
return {
|
||||
deviceId: device.id,
|
||||
hostname: found.hostname,
|
||||
url: found.url,
|
||||
https: found.https,
|
||||
port: found.port,
|
||||
favicon: found.favicon,
|
||||
description: found.description,
|
||||
suggestedDisplayName: found.suggestedDisplayName,
|
||||
suggestedCategory: found.category,
|
||||
})
|
||||
);
|
||||
|
||||
deviceRepo.upsertDeviceFromScan({
|
||||
hostname: device.hostname,
|
||||
ip: device.ip,
|
||||
mac: device.mac,
|
||||
manufacturer: device.manufacturer,
|
||||
model: device.model,
|
||||
online: discovered.length > 0,
|
||||
source: device.source,
|
||||
});
|
||||
|
||||
return {
|
||||
deviceId: device.id,
|
||||
scannedPorts: discovered.length,
|
||||
created: results.filter((r) => r.created).length,
|
||||
updated: results.filter((r) => !r.created).length,
|
||||
services: results.map((r) => r.service),
|
||||
};
|
||||
scannedPorts: discovered.length,
|
||||
created,
|
||||
updated,
|
||||
services: results.map((r) => r.service),
|
||||
};
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
logRepo.logScan({
|
||||
type: "device",
|
||||
targetId: device.id,
|
||||
level: "error",
|
||||
message: `${device.hostname} (${device.ip}): Scan fehlgeschlagen – ${detail}`,
|
||||
});
|
||||
request.log.error(err);
|
||||
return reply.code(500).send({ error: "Geräte-Scan fehlgeschlagen", detail });
|
||||
}
|
||||
});
|
||||
|
||||
// FritzBox-Scan: liest die Geräteliste per TR-064 und legt/aktualisiert Geräte.
|
||||
@@ -82,13 +104,23 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
source: "fritzbox",
|
||||
})
|
||||
);
|
||||
|
||||
logRepo.logScan({
|
||||
type: "fritzbox",
|
||||
level: "info",
|
||||
message: `FritzBox-Scan: ${hosts.length} Gerät(e) gefunden`,
|
||||
});
|
||||
|
||||
return { found: hosts.length, devices };
|
||||
} catch (err) {
|
||||
request.log.error(err);
|
||||
return reply.code(502).send({
|
||||
error: "FritzBox-Scan fehlgeschlagen",
|
||||
detail: err instanceof Error ? err.message : String(err),
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
logRepo.logScan({
|
||||
type: "fritzbox",
|
||||
level: "error",
|
||||
message: `FritzBox-Scan fehlgeschlagen – ${detail}`,
|
||||
});
|
||||
request.log.error(err);
|
||||
return reply.code(502).send({ error: "FritzBox-Scan fehlgeschlagen", detail });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user