diff --git a/src/lib/components/DeviceCard.svelte b/src/lib/components/DeviceCard.svelte
index 24a376a..cfe6a13 100644
--- a/src/lib/components/DeviceCard.svelte
+++ b/src/lib/components/DeviceCard.svelte
@@ -13,6 +13,7 @@
import AmpelBadge from './AmpelBadge.svelte';
import type { Device, Measurement } from '$lib/types';
import type { Tool } from '$lib/tools/types';
+ import { teileNachGeraetetyp } from '$lib/tools/geraeteaktionen';
let {
device,
@@ -30,6 +31,9 @@
onrename?: () => void;
} = $props();
+ // Werkzeuge nach Gerätetyp sortieren (passend vorn, Rest nachrangig)
+ const aufgeteilt = $derived(teileNachGeraetetyp(tools, device.deviceType));
+
// Index = MeasureStatus (0 ok, 1 warn, 2 fail, 3 nicht messbar)
const ampel = ['ampel-ok', 'ampel-warn', 'ampel-fail', 'ampel-unmess'];
const ampelDot = ['bg-emerald-500', 'bg-amber-400', 'bg-red-500', 'bg-zinc-500'];
@@ -132,8 +136,14 @@
{/each}
{#if tools.length && onrun}
+
- {#each tools as tool (tool.id)}
+ {#each aufgeteilt.passend as tool (tool.id)}
{/each}
+ {#each aufgeteilt.nachrangig as tool (tool.id)}
+
+ {/each}
{/if}
diff --git a/src/lib/tools/geraeteaktionen.ts b/src/lib/tools/geraeteaktionen.ts
new file mode 100644
index 0000000..69d5c01
--- /dev/null
+++ b/src/lib/tools/geraeteaktionen.ts
@@ -0,0 +1,86 @@
+/**
+ * Welche Geräte-Werkzeuge passen zu welchem Gerätetyp — bewusst OHNE
+ * Laufzeit-Import, damit `tools/pruefe-geraeteaktionen.mjs` die Regeln
+ * nachrechnen kann.
+ *
+ * Anlass: „SNMP-Switch" stand an jeder IP-Kamera und erzeugte dort zuverlässig
+ * eine Messung „nicht messbar" im Kundenprotokoll — eine Zeile, die niemandem
+ * hilft und den Abnahmebeleg verwässert.
+ *
+ * WICHTIG — warum hier nichts verschwindet: `Device.deviceType` ist freier Text
+ * aus zwei unabhängigen Quellen (Kotlin `guessDeviceType`, TS `typeFromMdns`)
+ * und rät anhand von MAC-Hersteller, mDNS-Diensten und offenen Ports. Ein
+ * managed Switch, der als „Linux-Gerät" durchgeht, verlöre bei hartem Filtern
+ * ausgerechnet SNMP — das wäre schlimmer als die überflüssige Zeile, die wir
+ * loswerden wollen. Unpassende Werkzeuge werden deshalb **nachrangig**
+ * einsortiert (die Karte zeigt sie kleiner und blasser), nicht entfernt.
+ * Unbekannter oder leerer Typ ⇒ alles gleichrangig, nie weniger.
+ */
+
+/** Werkzeuge, die nur bei bestimmten Gerätetypen vorn stehen sollen */
+const NUR_BEI: Record = {
+ /*
+ * SNMP beantwortet praktisch nur verwaltbare Netz- und Bürotechnik. Die Liste
+ * ist absichtlich großzügig: lieber ein Werkzeug zu viel vorn als eines, das
+ * der Techniker beim Kunden suchen muss. Drucker gehören dazu — sie sind in
+ * der Praxis die häufigste SNMP-Quelle überhaupt (Tonerstand, Zählerstand).
+ */
+ snmp: [
+ 'switch',
+ 'router',
+ 'gateway',
+ 'nas',
+ 'drucker',
+ 'printer',
+ 'server',
+ 'usv',
+ 'accesspoint',
+ 'access point',
+ 'firewall',
+ ],
+};
+
+/** Gerätetyp auf einen Vergleichswert bringen (Kleinschreibung, ohne Beiwerk) */
+function normiere(typ: string | undefined | null): string {
+ return (typ ?? '').toLowerCase().trim();
+}
+
+/**
+ * Passt ein Werkzeug zum Gerätetyp?
+ *
+ * @param toolId ID aus der Werkzeug-Registry
+ * @param deviceType freier Text aus `Device.deviceType` (darf leer sein)
+ * @returns true = gleichrangig anzeigen, false = nachrangig
+ */
+export function passtZuGeraet(toolId: string, deviceType: string | undefined | null): boolean {
+ const nurBei = NUR_BEI[toolId];
+ if (!nurBei) {
+ return true; // keine Einschränkung hinterlegt
+ }
+ const typ = normiere(deviceType);
+ if (typ === '') {
+ return true; // unbekannt ⇒ nie weniger zeigen
+ }
+ // Teilstring-Vergleich: die Quellen liefern u.a. „SNMP-Switch", „IP-Kamera",
+ // „Windows-PC" — ein Gleichheitsvergleich ginge an der Wirklichkeit vorbei.
+ return nurBei.some((k) => typ.includes(k));
+}
+
+/**
+ * Werkzeugliste in „passend" und „nachrangig" teilen, Reihenfolge bleibt sonst
+ * unverändert.
+ *
+ * @param tools Werkzeuge mit `scope: 'device'`
+ * @param deviceType freier Text aus `Device.deviceType`
+ */
+export function teileNachGeraetetyp(
+ tools: T[],
+ deviceType: string | undefined | null,
+): { passend: T[]; nachrangig: T[] } {
+ const passend: T[] = [];
+ const nachrangig: T[] = [];
+ for (const t of tools) {
+ (passtZuGeraet(t.id, deviceType) ? passend : nachrangig).push(t);
+ }
+ return { passend, nachrangig };
+}
diff --git a/tools/pruefe-geraeteaktionen.mjs b/tools/pruefe-geraeteaktionen.mjs
new file mode 100644
index 0000000..2463355
--- /dev/null
+++ b/tools/pruefe-geraeteaktionen.mjs
@@ -0,0 +1,46 @@
+/**
+ * Prueft die Zuordnung Geraetetyp -> Werkzeuge.
+ *
+ * Anlass: "SNMP-Switch" stand an jeder IP-Kamera und erzeugte dort eine
+ * Messung "nicht messbar" im Kundenprotokoll. Wichtiger als das Wegsortieren
+ * ist aber, dass NICHTS verschwindet: deviceType ist geraten.
+ *
+ * Start: node tools/pruefe-geraeteaktionen.mjs
+ */
+const { passtZuGeraet, teileNachGeraetetyp } =
+ await import('../src/lib/tools/geraeteaktionen.ts');
+
+const WERKZEUGE = [{ id: 'ping' }, { id: 'portscan' }, { id: 'snmp' }];
+
+const faelle = [
+ { typ: 'Kamera', snmpVorn: false, name: 'IP-Kamera -> SNMP nachrangig' },
+ { typ: 'IP-Kamera', snmpVorn: false, name: 'Schreibweise mit Praefix' },
+ { typ: 'Switch', snmpVorn: true, name: 'Switch -> SNMP vorn' },
+ { typ: 'SNMP-Switch', snmpVorn: true, name: 'Teilstring trifft' },
+ { typ: 'Drucker', snmpVorn: true, name: 'Drucker koennen SNMP (Toner/Zaehler)' },
+ { typ: 'NAS', snmpVorn: true, name: 'NAS -> SNMP vorn' },
+ { typ: 'Router', snmpVorn: true, name: 'Router -> SNMP vorn' },
+ { typ: 'Windows-PC', snmpVorn: false, name: 'PC -> SNMP nachrangig' },
+ { typ: 'Wallbox', snmpVorn: false, name: 'Wallbox spricht Modbus, nicht SNMP' },
+ { typ: 'IoT/SPS', snmpVorn: false, name: 'SPS spricht Modbus, nicht SNMP' },
+ { typ: '', snmpVorn: true, name: 'LEER -> alles gleichrangig (nie weniger)' },
+ { typ: undefined, snmpVorn: true, name: 'UNBEKANNT -> alles gleichrangig' },
+ { typ: 'Linux-Gerät', snmpVorn: false, name: 'Linux-Geraet -> nachrangig, aber erreichbar' },
+];
+
+let fehler = 0;
+for (const f of faelle) {
+ const { passend, nachrangig } = teileNachGeraetetyp(WERKZEUGE, f.typ);
+ const snmpVorn = passend.some((t) => t.id === 'snmp');
+ // Zwei harte Zusicherungen: die erwartete Einsortierung UND dass kein
+ // Werkzeug verschwindet.
+ const vollzaehlig = passend.length + nachrangig.length === WERKZEUGE.length;
+ const pingVorn = passend.some((t) => t.id === 'ping');
+ const portVorn = passend.some((t) => t.id === 'portscan');
+ const ok = snmpVorn === f.snmpVorn && vollzaehlig && pingVorn && portVorn;
+ if (!ok) fehler++;
+ console.log(`${ok ? 'OK ' : 'FEHL'} ${f.name}`);
+ console.log(` typ="${f.typ}" vorn=[${passend.map((t) => t.id)}] nachrangig=[${nachrangig.map((t) => t.id)}]`);
+}
+console.log(fehler === 0 ? 'ALLE FAELLE KORREKT' : `${fehler} FEHLER`);
+process.exit(fehler === 0 ? 0 : 1);