Commit 8: Plugin-System (Scanner erweitern, Geräte importieren, Icons)

This commit is contained in:
2026-07-19 10:14:43 +02:00
parent 6a7eb3dfe5
commit 986de50963
20 changed files with 517 additions and 20 deletions

View File

@@ -80,11 +80,15 @@ Dieser erste Commit liefert ein lauffähiges Grundgerüst:
- ✅ PWA: installierbar (Manifest + Icons für Android/iOS/Desktop), Service
Worker mit App-Shell-Precaching, `/api/*` läuft offline über den letzten
Cache-Stand (NetworkFirst, 3s-Timeout)
- ✅ Plugin-System: echte, ladbare Plugins unter `apps/backend/plugins/*`
(Bind-Mount, kein Rebuild nötig). Plugins können die Softwareerkennung des
Scanners erweitern (inkl. eigenem Icon) und eigene Geräte-Importquellen
bereitstellen. Zwei funktionierende Beispiel-Plugins liegen bei.
Noch **nicht** enthalten (folgt in den nächsten Commits):
Noch **nicht** enthalten:
- Plugin-System (Scanner registrieren, Geräte importieren, Menüs erweitern, Icons)
- shadcn/ui, React Hook Form (aktuell einfache kontrollierte Formulare)
- Eigene Admin-Routen/Menüpunkte pro Plugin (aktuell gesammelt auf einer Seite)
### Umgebungsvariablen (.env)
@@ -143,6 +147,9 @@ POST /api/scan/fritzbox Liest Geräteliste der FritzBox per TR-064
(erfordert FRITZBOX_HOST/USERNAME/PASSWORD)
GET /api/logs optional ?limit= (Default 100, Max 500)
GET /api/plugins geladene Plugins mit Capabilities
POST /api/plugins/:name/import löst importDevices() eines Plugins aus
```
## Frontend-Routen

View File

@@ -0,0 +1,78 @@
# LaunchPad-Plugins
Jeder Unterordner hier ist ein Plugin. Erkannt wird alles, was eine
`index.js` mit einem passenden Default-Export enthält kein Rebuild des
Docker-Images nötig, dieser Ordner wird zur Laufzeit als Volume eingebunden
(siehe `docker-compose.yml`). Nach dem Hinzufügen/Ändern eines Plugins
reicht ein Neustart des Backend-Containers:
```bash
docker compose restart backend
```
## Minimalstruktur
```
apps/backend/plugins/mein-plugin/
└── index.js
```
```js
export default {
name: "mein-plugin",
version: "1.0.0",
description: "Kurze Beschreibung, erscheint unter Admin -> Plugins",
// optional: Scanner erweitern
setup(ctx) {
ctx.registerSoftwareSignature({
name: "Meine Software",
category: "Sonstiges",
icon: "🔧", // Emoji oder Icon-URL
matches: (input) => !!input.body && /meine-software/i.test(input.body),
});
},
// optional: eigene Geräte-Importquelle
async importDevices() {
return [
{ hostname: "beispiel", ip: "192.168.1.99", source: "plugin" },
];
},
};
```
Beide Funktionen (`setup`, `importDevices`) sind optional ein Plugin kann
nur die eine, nur die andere, oder beide implementieren. Reines JavaScript
(ESM), kein Build-Schritt nötig.
## Was Plugins aktuell können
- **Scanner registrieren**: `ctx.registerSoftwareSignature(...)` in `setup()`
fügt der automatischen Softwareerkennung (siehe `apps/backend/src/scanner/softwareDetection.ts`)
eine zusätzliche Signatur hinzu, inklusive eigenem Icon. Wird bei jedem
Geräte-Scan berücksichtigt.
- **Geräte importieren**: `importDevices()` liefert eine Liste von Geräten,
die per Knopfdruck unter Admin -> Plugins importiert werden
(`POST /api/plugins/:name/import`). Abgeglichen wird wie bei Scans über
MAC/IP ein erneuter Import überschreibt keine Gerätefelder, die der
Nutzer zwischenzeitlich geändert hat, außer den scan-eigenen (siehe
`upsertDeviceFromScan`).
- **Icons bereitstellen**: über `icon` an einer registrierten Signatur. Wird
als Startwert für neu angelegte Dienste übernommen (wie `category`
niemals nachträglich überschrieben, siehe README Hauptprojekt).
## Was (noch) nicht geht
- Eigene Admin-Menüpunkte/Unterseiten (aktuell erscheinen alle Plugins
gesammelt auf einer Seite unter Admin -> Plugins, keine eigene Route pro
Plugin).
- Kein Sandboxing: Plugin-Code läuft mit vollem Zugriff im Backend-Prozess.
Nur Plugins aus vertrauenswürdiger Quelle einbinden.
## Beispiel-Plugins
- `example-signatures/` registriert zwei zusätzliche Software-Signaturen
(Homebridge, Uptime Kuma).
- `static-import/` importiert Geräte aus einer lokalen `devices.json`
(standardmäßig leer, siehe `devices.example.json` für das Format).

View File

@@ -0,0 +1,29 @@
/**
* Beispiel-Plugin: registriert zwei zusätzliche Softwareerkennungen, die
* nicht in der eingebauten Liste enthalten sind, inklusive eigenem Icon.
*
* Zeigt das setup()-Pattern: ein Plugin bekommt beim Laden einen Kontext mit
* registerSoftwareSignature() und kann darüber den Scanner erweitern.
*/
export default {
name: "example-signatures",
version: "1.0.0",
description:
"Erkennt Homebridge und Uptime Kuma zusätzlich zu den eingebauten Signaturen.",
setup(ctx) {
ctx.registerSoftwareSignature({
name: "Homebridge",
category: "Smart Home",
icon: "🏠",
matches: (input) => !!input.body && /homebridge/i.test(input.body),
});
ctx.registerSoftwareSignature({
name: "Uptime Kuma",
category: "Monitoring",
icon: "📈",
matches: (input) => !!input.body && /uptime\s*kuma/i.test(input.body),
});
},
};

View File

@@ -0,0 +1,9 @@
[
{
"hostname": "nas",
"ip": "192.168.1.20",
"mac": "AA:BB:CC:DD:EE:FF",
"manufacturer": "Synology",
"model": "DS920+"
}
]

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,40 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const currentDir = dirname(fileURLToPath(import.meta.url));
/**
* Beispiel-Plugin: importiert Geräte aus einer lokalen devices.json neben
* diesem Plugin. Zeigt das importDevices()-Pattern für eigene Datenquellen
* (z. B. ein Inventar-Export oder eine externe API) im Unterschied zu den
* Netzwerk-Scannern rein datengetrieben, kein eigener Netzwerkzugriff nötig.
*
* Standardmäßig ist devices.json leer, damit beim ersten Start keine
* Fantasiegeräte auftauchen. Zum Ausprobieren einfach Einträge ergänzen und
* unter Admin -> Plugins auf "Jetzt importieren" klicken.
*/
export default {
name: "static-import",
version: "1.0.0",
description: "Importiert Geräte aus plugins/static-import/devices.json.",
async importDevices() {
try {
const raw = readFileSync(join(currentDir, "devices.json"), "utf-8");
const entries = JSON.parse(raw);
return entries.map((entry) => ({
hostname: entry.hostname,
ip: entry.ip,
mac: entry.mac ?? undefined,
manufacturer: entry.manufacturer ?? undefined,
model: entry.model ?? undefined,
online: false,
source: "plugin",
}));
} catch {
return [];
}
},
};

View File

@@ -132,6 +132,8 @@ export interface ServiceScanInput {
suggestedDisplayName: string;
/** Nur relevant, wenn dabei ein NEUER Dienst angelegt wird. */
suggestedCategory?: string | null;
/** Nur relevant, wenn dabei ein NEUER Dienst angelegt wird (z. B. von einem Plugin bereitgestellt). */
suggestedIcon?: string | null;
}
export interface ScanUpsertResult {
@@ -180,7 +182,7 @@ export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult
favorite: false,
order: 0,
alias: "[]",
icon: null,
icon: input.suggestedIcon ?? null,
hostname: input.hostname,
url: input.url,
https: input.https,

View File

@@ -8,6 +8,8 @@ import { serviceRoutes } from "./routes/services.js";
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 { loadPlugins } from "./plugins/loader.js";
const PORT = Number(process.env.PORT ?? 3001);
const HOST = process.env.HOST ?? "0.0.0.0";
@@ -29,12 +31,16 @@ async function main() {
ensureSchema();
const plugins = await loadPlugins();
app.log.info(`${plugins.length} Plugin(s) geladen`);
await app.register(healthRoutes);
await app.register(deviceRoutes);
await app.register(serviceRoutes);
await app.register(categoryRoutes);
await app.register(scanRoutes);
await app.register(logRoutes);
await app.register(pluginRoutes);
app.get("/", async () => {
return { name: "LaunchPad API", status: "running" };

View File

@@ -0,0 +1,71 @@
import { existsSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { registerSoftwareSignature } from "../scanner/softwareDetection.js";
import type { LaunchPadPlugin } from "./types.js";
const currentDir = path.dirname(fileURLToPath(import.meta.url));
// apps/backend/src/plugins/loader.ts (bzw. dist/plugins/loader.js) -> das
// Plugin-Verzeichnis liegt direkt neben src/dist. Im Docker-Image entspricht
// das /app/plugins (siehe docker-compose.yml Bind-Mount), lokal
// apps/backend/plugins Plugins lassen sich damit ohne Rebuild hinzufügen.
const PLUGINS_DIR = path.resolve(currentDir, "../../plugins");
export interface LoadedPlugin {
plugin: LaunchPadPlugin;
capabilities: string[];
}
const loadedPlugins: LoadedPlugin[] = [];
export async function loadPlugins(): Promise<LoadedPlugin[]> {
loadedPlugins.length = 0;
if (!existsSync(PLUGINS_DIR)) {
return loadedPlugins;
}
const entries = readdirSync(PLUGINS_DIR).filter((entry) => {
const full = path.join(PLUGINS_DIR, entry);
return statSync(full).isDirectory();
});
for (const entry of entries) {
const entryPoint = path.join(PLUGINS_DIR, entry, "index.js");
if (!existsSync(entryPoint)) continue;
try {
const imported = await import(pathToFileURL(entryPoint).href);
const plugin: LaunchPadPlugin | undefined = imported.default ?? imported.plugin;
if (!plugin || !plugin.name || !plugin.version) {
console.warn(`Plugin-Ordner "${entry}" liefert kein gültiges Plugin-Objekt, wird übersprungen.`);
continue;
}
if (plugin.setup) {
await plugin.setup({ registerSoftwareSignature });
}
const capabilities: string[] = [];
if (plugin.setup) capabilities.push("scanner");
if (plugin.importDevices) capabilities.push("device-import");
loadedPlugins.push({ plugin, capabilities });
console.log(`Plugin geladen: ${plugin.name}@${plugin.version} [${capabilities.join(", ") || "keine Capabilities"}]`);
} catch (err) {
console.error(`Plugin "${entry}" konnte nicht geladen werden:`, err);
}
}
return loadedPlugins;
}
export function getLoadedPlugins(): LoadedPlugin[] {
return loadedPlugins;
}
export function getPlugin(name: string): LoadedPlugin | undefined {
return loadedPlugins.find((p) => p.plugin.name === name);
}

View File

@@ -0,0 +1,22 @@
import type { DeviceCreateInput } from "@launchpad/shared";
import type { SoftwareSignature } from "../scanner/softwareDetection.js";
export interface PluginContext {
/** Erweitert die Softwareerkennung des Scanners um eine eigene Signatur. */
registerSoftwareSignature: (signature: SoftwareSignature) => void;
}
/**
* Vertrag, den jedes Plugin erfüllt. Ein Plugin ist ein Ordner unter
* apps/backend/plugins/<name>/ mit einer index.js, deren Default-Export
* dieses Objekt ist. Siehe apps/backend/plugins/README.md.
*/
export interface LaunchPadPlugin {
name: string;
version: string;
description?: string;
/** Wird einmal beim Start aufgerufen, z. B. um Scanner-Signaturen zu registrieren. */
setup?: (ctx: PluginContext) => void | Promise<void>;
/** Optional: eigene Geräte-Importquelle (z. B. Inventarliste, externe API). */
importDevices?: () => Promise<DeviceCreateInput[]>;
}

View File

@@ -0,0 +1,61 @@
import type { FastifyInstance } from "fastify";
import * as deviceRepo from "../db/repositories/devices.js";
import * as logRepo from "../db/repositories/logs.js";
import { getLoadedPlugins, getPlugin } from "../plugins/loader.js";
export async function pluginRoutes(app: FastifyInstance): Promise<void> {
app.get("/api/plugins", async () => {
return getLoadedPlugins().map(({ plugin, capabilities }) => ({
name: plugin.name,
version: plugin.version,
description: plugin.description,
capabilities,
}));
});
// Manueller Geräte-Import über ein Plugin wie Scans nie automatisch,
// nur per Knopfdruck (Admin -> Plugins).
app.post("/api/plugins/:name/import", async (request, reply) => {
const { name } = request.params as { name: string };
const loaded = getPlugin(name);
if (!loaded) {
return reply.code(404).send({ error: "Plugin nicht gefunden" });
}
if (!loaded.plugin.importDevices) {
return reply.code(400).send({ error: "Dieses Plugin unterstützt keinen Geräte-Import" });
}
try {
const devices = await loaded.plugin.importDevices();
const imported = devices.map((d) =>
deviceRepo.upsertDeviceFromScan({
hostname: d.hostname,
ip: d.ip,
mac: d.mac,
manufacturer: d.manufacturer,
model: d.model,
online: d.online ?? false,
source: "plugin",
})
);
logRepo.logScan({
type: "device",
level: "info",
message: `Plugin "${name}": ${imported.length} Gerät(e) importiert`,
});
return { imported: imported.length, devices: imported };
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
logRepo.logScan({
type: "device",
level: "error",
message: `Plugin "${name}": Import fehlgeschlagen ${detail}`,
});
request.log.error(err);
return reply.code(500).send({ error: "Import fehlgeschlagen", detail });
}
});
}

View File

@@ -33,6 +33,7 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
description: found.description,
suggestedDisplayName: found.suggestedDisplayName,
suggestedCategory: found.category,
suggestedIcon: found.icon,
})
);

View File

@@ -17,6 +17,7 @@ export interface DiscoveredService {
description?: string;
suggestedDisplayName: string;
category?: string;
icon?: string;
}
/**
@@ -59,6 +60,7 @@ export async function scanDeviceServices(
description: software ? `${software.name} (automatisch erkannt)` : probe.title,
suggestedDisplayName: software?.name ?? probe.title ?? `${address}:${port}`,
category: software?.category,
icon: software?.icon,
});
}

View File

@@ -8,6 +8,8 @@ export interface SoftwareSignature {
name: string;
category: string;
matches: (input: SoftwareSignatureInput) => boolean;
/** Optional: Emoji oder Icon-URL, von Plugins bereitgestellt (siehe apps/backend/plugins). */
icon?: string;
}
function bodyContains(input: SoftwareSignatureInput, pattern: RegExp): boolean {
@@ -44,6 +46,21 @@ export const SOFTWARE_SIGNATURES: SoftwareSignature[] = [
{ name: "UniFi Network", category: "Netzwerk", matches: (i) => bodyContains(i, /unifi/i) },
];
export function detectSoftware(input: SoftwareSignatureInput): SoftwareSignature | null {
return SOFTWARE_SIGNATURES.find((signature) => signature.matches(input)) ?? null;
/**
* Zusätzliche Signaturen, die von Plugins zur Laufzeit registriert werden
* (siehe apps/backend/src/plugins/loader.ts). Eingebaute Signaturen haben
* Vorrang, falls beide zufällig auf denselben Dienst passen.
*/
const pluginSignatures: SoftwareSignature[] = [];
export function registerSoftwareSignature(signature: SoftwareSignature): void {
pluginSignatures.push(signature);
}
export function detectSoftware(input: SoftwareSignatureInput): SoftwareSignature | null {
return (
SOFTWARE_SIGNATURES.find((signature) => signature.matches(input)) ??
pluginSignatures.find((signature) => signature.matches(input)) ??
null
);
}

View File

@@ -0,0 +1,17 @@
import { useQuery } from "@tanstack/react-query";
import type { PluginInfo } from "@launchpad/shared";
async function fetchPlugins(): Promise<PluginInfo[]> {
const res = await fetch("/api/plugins");
if (!res.ok) {
throw new Error(`Plugins konnten nicht geladen werden (HTTP ${res.status})`);
}
return res.json();
}
export function usePlugins() {
return useQuery({
queryKey: ["plugins"],
queryFn: fetchPlugins,
});
}

View File

@@ -1,18 +1,109 @@
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@launchpad/ui";
import type { PluginInfo } from "@launchpad/shared";
import { usePlugins } from "../../hooks/usePlugins.js";
import { AdminPageHeader } from "./AdminPageHeader.js";
export function PluginsPage() {
const CAPABILITY_LABELS: Record<string, string> = {
scanner: "Erweitert die Softwareerkennung",
"device-import": "Kann Geräte importieren",
};
async function importFromPlugin(name: string) {
const res = await fetch(`/api/plugins/${name}/import`, { method: "POST" });
const body = await res.json();
if (!res.ok) {
throw new Error(body.detail ?? body.error ?? `Import fehlgeschlagen (HTTP ${res.status})`);
}
return body as { imported: number };
}
function PluginCard({ plugin }: { plugin: PluginInfo }) {
const queryClient = useQueryClient();
const [message, setMessage] = useState<string | null>(null);
const importMutation = useMutation({
mutationFn: () => importFromPlugin(plugin.name),
onSuccess: (result) => {
setMessage(`${result.imported} Gerät(e) importiert.`);
queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["logs"] });
},
onError: (err: Error) => setMessage(err.message),
});
return (
<div className="rounded-2xl border border-black/10 p-5 dark:border-white/10">
<div className="flex items-start justify-between gap-3">
<div>
<AdminPageHeader title="Plugins" />
<div className="rounded-2xl border border-dashed border-black/15 p-8 text-center dark:border-white/15">
<p className="text-black/60 dark:text-white/60">
Das Plugin-System (Scanner registrieren, Geräte importieren, Menüs erweitern,
Icons bereitstellen) ist noch nicht gebaut.
</p>
<p className="mt-2 text-sm text-black/40 dark:text-white/40">
Geplant als eigener Commit siehe <code className="rounded bg-black/5 px-1 dark:bg-white/10">docs/ROADMAP.md</code>.
</p>
<h2 className="font-medium text-black dark:text-white">
{plugin.name} <span className="text-black/40 dark:text-white/40">v{plugin.version}</span>
</h2>
{plugin.description ? (
<p className="mt-1 text-sm text-black/50 dark:text-white/50">{plugin.description}</p>
) : null}
</div>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{plugin.capabilities.length === 0 ? (
<span className="text-xs text-black/30 dark:text-white/30">Keine Capabilities</span>
) : (
plugin.capabilities.map((cap) => (
<span
key={cap}
className="rounded-full bg-black/5 px-2.5 py-1 text-xs text-black/60 dark:bg-white/10 dark:text-white/60"
>
{CAPABILITY_LABELS[cap] ?? cap}
</span>
))
)}
</div>
{plugin.capabilities.includes("device-import") ? (
<div className="mt-4">
<Button size="sm" variant="primary" onClick={() => importMutation.mutate()} disabled={importMutation.isPending}>
{importMutation.isPending ? "Importiere …" : "Jetzt importieren"}
</Button>
{message ? (
<p className="mt-2 text-xs text-black/50 dark:text-white/50">{message}</p>
) : null}
</div>
) : null}
</div>
);
}
export function PluginsPage() {
const { data: plugins, isLoading, isError } = usePlugins();
return (
<div>
<AdminPageHeader
title="Plugins"
description="Plugins liegen als Ordner unter apps/backend/plugins/ und werden beim Start des Backends geladen kein Rebuild nötig, nur ein Container-Neustart."
/>
{isLoading ? (
<p className="text-sm text-black/40 dark:text-white/40">Lade Plugins </p>
) : isError ? (
<p className="text-sm text-red-500">Plugins konnten nicht geladen werden.</p>
) : plugins && plugins.length > 0 ? (
<div className="grid gap-4 sm:grid-cols-2">
{plugins.map((plugin) => (
<PluginCard key={plugin.name} plugin={plugin} />
))}
</div>
) : (
<div className="rounded-2xl border border-dashed border-black/15 p-8 text-center dark:border-white/15">
<p className="text-black/60 dark:text-white/60">Keine Plugins geladen.</p>
<p className="mt-2 text-sm text-black/40 dark:text-white/40">
Siehe <code className="rounded bg-black/5 px-1 dark:bg-white/10">apps/backend/plugins/README.md</code>{" "}
für eine Anleitung, wie du ein eigenes Plugin schreibst.
</p>
</div>
)}
</div>
);
}

View File

@@ -15,6 +15,7 @@ services:
- NODE_ENV=production
volumes:
- launchpad-data:/data
- ./apps/backend/plugins:/app/plugins:ro
ports:
- "3001:3001"

View File

@@ -92,7 +92,38 @@ Grundgerüst aus Commit 1.
> Docker-Build-Simulation durchlaufen. Die tatsächliche "Zum Homescreen hinzufügen"-
> Installation konnte mangels Browser in dieser Umgebung nicht getestet werden.
## Commit 8 — Plugin-System
## Commit 8 — Plugin-System (erledigt)
- Plugin-Schnittstelle zum Registrieren von Scannern, Import von Geräten,
Erweitern von Menüs, Bereitstellen von Icons
- Plugins liegen als Ordner unter `apps/backend/plugins/*`, jeweils mit einer
`index.js` (reines ESM, kein Build-Schritt). Werden beim Backend-Start
geladen (`apps/backend/src/plugins/loader.ts`) und per Docker-Volume
eingebunden neue Plugins brauchen nur einen Container-Neustart, kein
Image-Rebuild
- Plugin-Vertrag (`apps/backend/src/plugins/types.ts`): `setup(ctx)` zum
Registrieren zusätzlicher Softwareerkennung (inkl. Icon), `importDevices()`
für eigene Geräte-Importquellen
- Zwei funktionierende Beispiel-Plugins: `example-signatures` (Homebridge,
Uptime Kuma) und `static-import` (Geräte aus lokaler `devices.json`)
- `GET /api/plugins`, `POST /api/plugins/:name/import` Import läuft wie
Scans ausschließlich manuell per Knopfdruck
- Frontend-Plugins-Seite zeigt echte geladene Plugins mit Capabilities und
Import-Button (ersetzt den ehrlichen Platzhalter aus Commit 6)
- Von neuen Plugins erkannte Software liefert ein Icon, das wie `category`
seit Commit 5 nur beim erstmaligen Anlegen eines Dienstes als Startwert
übernommen wird, nie nachträglich überschrieben
> Verifiziert: beide Plugins laden nachweislich beim Start; ein echter Scan
> gegen einen Test-Server mit "Homebridge" im Response-Body wurde über die
> Plugin-Signatur erkannt, inkl. korrekt übernommenem Icon; Geräte-Import per
> Plugin getestet (`source: "plugin"`); Fehlerfälle (unbekanntes Plugin → 404,
> Plugin ohne Import-Fähigkeit → 400) geprüft; Docker-Build-Simulation
> bestanden, dabei auch verifiziert, dass ein fehlendes Plugin-Verzeichnis
> nicht zum Absturz führt, sondern nur zu einer leeren Liste.
> **Bewusst nicht umgesetzt:** eigene Admin-Routen/Menüpunkte pro Plugin
> (alle Plugins erscheinen gesammelt auf einer Seite) und Sandboxing
> (Plugin-Code läuft mit vollem Zugriff im Backend-Prozess nur Plugins aus
> vertrauenswürdiger Quelle einbinden, siehe `apps/backend/plugins/README.md`).
Damit ist die komplette in der ursprünglichen Projektübergabe beschriebene
Funktionalität umgesetzt.

View File

@@ -5,6 +5,8 @@
* als auch vom Frontend (apps/frontend) verwendet werden.
*/
import { deviceSourceValues } from "./schemas.js";
export * from "./schemas.js";
@@ -20,7 +22,7 @@ export interface Device {
lastScan: string | null; // ISO-8601 Zeitstempel
}
export type DeviceSource = "fritzbox" | "dns" | "http" | "https" | "portscan" | "manual";
export type DeviceSource = (typeof deviceSourceValues)[number];
export interface Service {
id: string;
@@ -61,6 +63,14 @@ export interface ScanLogEntry {
createdAt: string;
}
export interface PluginInfo {
name: string;
version: string;
description?: string;
/** z. B. "scanner" (registriert Softwareerkennung), "device-import" */
capabilities: string[];
}
/**
* Ranking-Stufen für die Suche, gemäß Spezifikation:
* 1. Displayname beginnt mit Suchtext

View File

@@ -7,6 +7,7 @@ export const deviceSourceValues = [
"https",
"portscan",
"manual",
"plugin",
] as const;
export const DeviceSourceSchema = z.enum(deviceSourceValues);