Geraete-Aktionen nach Geraetetyp sortieren statt sie zu verstecken

"SNMP-Switch" stand an jeder IP-Kamera und erzeugte dort zuverlaessig eine
Messung "nicht messbar" im Kundenprotokoll - eine Zeile, die niemandem hilft
und den Abnahmebeleg verwaessert.

Bewusst KEIN hartes Filtern: Device.deviceType ist freier Text aus zwei
unabhaengigen Quellen (Kotlin guessDeviceType, TS typeFromMdns) und wird aus
MAC-Hersteller, mDNS-Diensten und offenen Ports geraten. Ein managed Switch,
der als "Linux-Geraet" durchgeht, verloere beim Filtern ausgerechnet SNMP -
schlimmer als die ueberfluessige Zeile, die wir loswerden wollen. Unpassende
Werkzeuge stehen deshalb nachrangig (kleiner, blasser, mit Titel-Hinweis)
hinter den passenden, statt zu verschwinden. Leerer oder unbekannter Typ:
alles gleichrangig, nie weniger.

Die SNMP-Liste ist absichtlich grosszuegig (Switch, Router, Gateway, NAS,
Drucker, Server, USV, Accesspoint, Firewall) - Drucker sind in der Praxis die
haeufigste SNMP-Quelle ueberhaupt.

tools/pruefe-geraeteaktionen.mjs rechnet 13 Faelle nach und prueft dabei zwei
Zusicherungen: die erwartete Einsortierung UND dass kein Werkzeug verschwindet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-08-16 23:14:06 +02:00
parent c954afc889
commit 7d79358c02
3 changed files with 152 additions and 1 deletions

View file

@ -13,6 +13,7 @@
import AmpelBadge from './AmpelBadge.svelte'; import AmpelBadge from './AmpelBadge.svelte';
import type { Device, Measurement } from '$lib/types'; import type { Device, Measurement } from '$lib/types';
import type { Tool } from '$lib/tools/types'; import type { Tool } from '$lib/tools/types';
import { teileNachGeraetetyp } from '$lib/tools/geraeteaktionen';
let { let {
device, device,
@ -30,6 +31,9 @@
onrename?: () => void; onrename?: () => void;
} = $props(); } = $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) // Index = MeasureStatus (0 ok, 1 warn, 2 fail, 3 nicht messbar)
const ampel = ['ampel-ok', 'ampel-warn', 'ampel-fail', 'ampel-unmess']; const ampel = ['ampel-ok', 'ampel-warn', 'ampel-fail', 'ampel-unmess'];
const ampelDot = ['bg-emerald-500', 'bg-amber-400', 'bg-red-500', 'bg-zinc-500']; const ampelDot = ['bg-emerald-500', 'bg-amber-400', 'bg-red-500', 'bg-zinc-500'];
@ -132,8 +136,14 @@
{/each} {/each}
{#if tools.length && onrun} {#if tools.length && onrun}
<!--
Nach Gerätetyp getrennt: passende Werkzeuge vorn, unpassende blasser
dahinter. Bewusst NICHT ausgeblendet — `deviceType` ist geraten (MAC-
Hersteller, mDNS, offene Ports), und ein als „Linux-Gerät" erkannter
Switch verlöre sonst ausgerechnet SNMP. Siehe tools/geraeteaktionen.ts.
-->
<div class="mt-2 flex flex-wrap gap-1.5"> <div class="mt-2 flex flex-wrap gap-1.5">
{#each tools as tool (tool.id)} {#each aufgeteilt.passend as tool (tool.id)}
<button <button
class="rounded bg-zinc-800 px-2 py-1 text-xs text-sky-300 active:bg-zinc-700" class="rounded bg-zinc-800 px-2 py-1 text-xs text-sky-300 active:bg-zinc-700"
onclick={() => onrun?.(tool)} onclick={() => onrun?.(tool)}
@ -141,6 +151,15 @@
{tool.name} {tool.name}
</button> </button>
{/each} {/each}
{#each aufgeteilt.nachrangig as tool (tool.id)}
<button
class="rounded bg-zinc-900 px-2 py-1 text-xs text-zinc-500 active:bg-zinc-700"
title="Passt selten zu diesem Gerätetyp — trotzdem ausführbar"
onclick={() => onrun?.(tool)}
>
{tool.name}
</button>
{/each}
</div> </div>
{/if} {/if}
</div> </div>

View file

@ -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<string, string[]> = {
/*
* 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<T extends { id: string }>(
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 };
}

View file

@ -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);