generated from Dicken/dickendock
Reset-Button, Service-Reorder, Port/HTTPS-Spalten, Healthcheck in Sidebar, Kategorien-Sync
This commit is contained in:
@@ -50,14 +50,28 @@ export function createCategory(input: CategoryCreateInput): Category {
|
||||
return getCategory(id)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Benennt eine Kategorie um. Da Dienste ihre Kategorie als freier Text
|
||||
* speichern (kein Fremdschlüssel), werden alle Dienste mit dem alten Namen
|
||||
* automatisch auf den neuen Namen mit umgestellt, damit beides synchron bleibt.
|
||||
*/
|
||||
export function updateCategory(id: string, input: CategoryUpdateInput): Category | null {
|
||||
const existing = getCategory(id);
|
||||
if (!existing) return null;
|
||||
|
||||
const timestamp = nowIso();
|
||||
|
||||
if (input.name !== undefined && input.name !== existing.name) {
|
||||
db.update(services)
|
||||
.set({ category: input.name, updatedAt: timestamp })
|
||||
.where(eq(services.category, existing.name))
|
||||
.run();
|
||||
}
|
||||
|
||||
db.update(categories)
|
||||
.set({
|
||||
...(input.name !== undefined && { name: input.name }),
|
||||
updatedAt: nowIso(),
|
||||
updatedAt: timestamp,
|
||||
})
|
||||
.where(eq(categories.id, id))
|
||||
.run();
|
||||
@@ -99,3 +113,41 @@ export function deleteCategory(id: string): boolean {
|
||||
const result = db.delete(categories).where(eq(categories.id, id)).run();
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt eine Kategorie an, falls noch keine mit diesem Namen existiert
|
||||
* (Vergleich ohne Groß-/Kleinschreibung). Wird beim Scannen aufgerufen,
|
||||
* damit jede automatisch erkannte Kategorie auch im Adminbereich auftaucht.
|
||||
*/
|
||||
export function ensureCategory(name: string): Category {
|
||||
const trimmed = name.trim();
|
||||
const existing = listCategories().find(
|
||||
(c) => c.name.toLowerCase() === trimmed.toLowerCase()
|
||||
);
|
||||
if (existing) return existing;
|
||||
return createCategory({ name: trimmed });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gleicht eine Liste von Kategorienamen (z. B. alle distinct-Werte aus
|
||||
* services.category) mit der categories-Tabelle ab und legt fehlende an.
|
||||
* Wird beim Start aufgerufen, damit bereits vorhandene, aber nie über
|
||||
* ensureCategory() angelegte Kategorien (z. B. aus Scans vor diesem Fix)
|
||||
* im Adminbereich sichtbar werden. Gibt die Anzahl neu angelegter zurück.
|
||||
*/
|
||||
export function syncCategoriesFromServiceValues(categoryNames: string[]): number {
|
||||
const existingNames = new Set(listCategories().map((c) => c.name.toLowerCase()));
|
||||
let created = 0;
|
||||
|
||||
for (const rawName of categoryNames) {
|
||||
const trimmed = rawName.trim();
|
||||
if (!trimmed) continue;
|
||||
if (existingNames.has(trimmed.toLowerCase())) continue;
|
||||
|
||||
createCategory({ name: trimmed });
|
||||
existingNames.add(trimmed.toLowerCase());
|
||||
created++;
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,12 @@ export function deleteDevice(id: string): boolean {
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
/** Löscht ALLE Geräte (und per FK-Cascade alle zugehörigen Dienste). Für den Reset-Button im Adminbereich. */
|
||||
export function deleteAllDevices(): number {
|
||||
const result = db.delete(devices).run();
|
||||
return result.changes;
|
||||
}
|
||||
|
||||
export interface DeviceScanInput {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
|
||||
@@ -124,6 +124,23 @@ export function deleteService(id: string): boolean {
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Reihenfolge mehrerer Dienste in einer Transaktion neu
|
||||
* (z. B. nach Drag & Drop in der Admin-UI).
|
||||
*/
|
||||
export function reorderServices(input: { id: string; order: number }[]): Service[] {
|
||||
db.transaction((tx) => {
|
||||
for (const entry of input) {
|
||||
tx.update(services)
|
||||
.set({ order: entry.order, updatedAt: nowIso() })
|
||||
.where(eq(services.id, entry.id))
|
||||
.run();
|
||||
}
|
||||
});
|
||||
|
||||
return listServices();
|
||||
}
|
||||
|
||||
export interface ServiceScanInput {
|
||||
deviceId: string;
|
||||
hostname: string;
|
||||
|
||||
@@ -9,7 +9,10 @@ import { categoryRoutes } from "./routes/categories.js";
|
||||
import { scanRoutes } from "./routes/scan.js";
|
||||
import { logRoutes } from "./routes/logs.js";
|
||||
import { pluginRoutes } from "./routes/plugins.js";
|
||||
import { resetRoutes } from "./routes/reset.js";
|
||||
import { loadPlugins } from "./plugins/loader.js";
|
||||
import * as serviceRepo from "./db/repositories/services.js";
|
||||
import * as categoryRepo from "./db/repositories/categories.js";
|
||||
|
||||
const PORT = Number(process.env.PORT ?? 3001);
|
||||
const HOST = process.env.HOST ?? "0.0.0.0";
|
||||
@@ -31,6 +34,23 @@ async function main() {
|
||||
|
||||
ensureSchema();
|
||||
|
||||
// Kategorien, die bereits auf Diensten stehen (z. B. aus Scans vor diesem
|
||||
// Fix, oder von Plugins), aber noch nicht in der categories-Tabelle sind,
|
||||
// hier nachtragen. Damit zeigt Admin -> Kategorien immer das, was auch in
|
||||
// der Suche als Kategorie auftaucht.
|
||||
const existingCategoryNames = Array.from(
|
||||
new Set(
|
||||
serviceRepo
|
||||
.listServices()
|
||||
.map((s) => s.category)
|
||||
.filter((c): c is string => !!c)
|
||||
)
|
||||
);
|
||||
const syncedCount = categoryRepo.syncCategoriesFromServiceValues(existingCategoryNames);
|
||||
if (syncedCount > 0) {
|
||||
app.log.info(`${syncedCount} Kategorie(n) aus bestehenden Diensten nachgetragen`);
|
||||
}
|
||||
|
||||
const plugins = await loadPlugins();
|
||||
app.log.info(`${plugins.length} Plugin(s) geladen`);
|
||||
|
||||
@@ -41,6 +61,7 @@ async function main() {
|
||||
await app.register(scanRoutes);
|
||||
await app.register(logRoutes);
|
||||
await app.register(pluginRoutes);
|
||||
await app.register(resetRoutes);
|
||||
|
||||
app.get("/", async () => {
|
||||
return { name: "LaunchPad API", status: "running" };
|
||||
|
||||
34
apps/backend/src/routes/reset.ts
Normal file
34
apps/backend/src/routes/reset.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import * as deviceRepo from "../db/repositories/devices.js";
|
||||
import * as logRepo from "../db/repositories/logs.js";
|
||||
|
||||
/**
|
||||
* Löscht ALLE Geräte und (per FK-Cascade) alle zugehörigen Dienste.
|
||||
* Kategorien und Logs bleiben unangetastet.
|
||||
*
|
||||
* Erfordert { confirm: true } im Body – zusätzlich zur Bestätigung im
|
||||
* Frontend eine zweite Sicherung gegen versehentliche Aufrufe (z. B. per
|
||||
* Skript oder Browser-Erweiterung).
|
||||
*/
|
||||
export async function resetRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.post("/api/reset", async (request, reply) => {
|
||||
const body = request.body as { confirm?: boolean } | undefined;
|
||||
|
||||
if (body?.confirm !== true) {
|
||||
return reply.code(400).send({
|
||||
error: "Bestätigung erforderlich. Bitte { \"confirm\": true } im Body senden.",
|
||||
});
|
||||
}
|
||||
|
||||
const deviceCount = deviceRepo.listDevices().length;
|
||||
const deletedCount = deviceRepo.deleteAllDevices();
|
||||
|
||||
logRepo.logScan({
|
||||
type: "device",
|
||||
level: "info",
|
||||
message: `Reset: ${deletedCount} Gerät(e) und alle zugehörigen Dienste gelöscht`,
|
||||
});
|
||||
|
||||
return { deletedDevices: deletedCount, previousDeviceCount: deviceCount };
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import * as deviceRepo from "../db/repositories/devices.js";
|
||||
import * as serviceRepo from "../db/repositories/services.js";
|
||||
import * as categoryRepo from "../db/repositories/categories.js";
|
||||
import * as logRepo from "../db/repositories/logs.js";
|
||||
import { scanDeviceServices } from "../scanner/networkScanner.js";
|
||||
import { fetchFritzBoxHosts } from "../scanner/fritzbox.js";
|
||||
@@ -22,6 +23,13 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
try {
|
||||
const discovered = await scanDeviceServices(device);
|
||||
|
||||
// Jede erkannte Kategorie auch in der categories-Tabelle anlegen, damit
|
||||
// sie unter Admin -> Kategorien auftaucht und dort umbenannt/sortiert
|
||||
// werden kann (services.category ist reiner Freitext, kein Fremdschlüssel).
|
||||
for (const category of new Set(discovered.map((d) => d.category).filter((c): c is string => !!c))) {
|
||||
categoryRepo.ensureCategory(category);
|
||||
}
|
||||
|
||||
const results = discovered.map((found) =>
|
||||
serviceRepo.upsertServiceFromScan({
|
||||
deviceId: device.id,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { ServiceCreateSchema, ServiceUpdateSchema } from "@launchpad/shared";
|
||||
import { ServiceCreateSchema, ServiceReorderSchema, ServiceUpdateSchema } from "@launchpad/shared";
|
||||
import * as deviceRepo from "../db/repositories/devices.js";
|
||||
import * as serviceRepo from "../db/repositories/services.js";
|
||||
|
||||
@@ -43,6 +43,15 @@ export async function serviceRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.code(201).send(service);
|
||||
});
|
||||
|
||||
// Muss vor der /:id-Route stehen, damit "reorder" nicht als ID interpretiert wird.
|
||||
app.patch("/api/services/reorder", async (request, reply) => {
|
||||
const parsed = ServiceReorderSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
|
||||
}
|
||||
return serviceRepo.reorderServices(parsed.data);
|
||||
});
|
||||
|
||||
app.patch("/api/services/:id", async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const parsed = ServiceUpdateSchema.safeParse(request.body);
|
||||
|
||||
Reference in New Issue
Block a user