Skill: network-device-mapper

Application-level patterns for the network-device-mapper project.

7 stars

Best use case

Skill: network-device-mapper is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Application-level patterns for the network-device-mapper project.

Teams using Skill: network-device-mapper should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/network-device-mapper/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/hardware-iot/network-device-mapper/skills/network-device-mapper/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/network-device-mapper/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How Skill: network-device-mapper Compares

Feature / AgentSkill: network-device-mapperStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Application-level patterns for the network-device-mapper project.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Skill: network-device-mapper

Application-level patterns for the network-device-mapper project.

## State management with Zustand

```ts
// packages/client/src/store/useNetmapStore.ts
import { create } from 'zustand';

interface Device {
  id: number;
  ip: string;
  mac: string;
  hostname: string | null;
  vendor: string | null;
  label: string | null;
  grp: string | null;
  is_gateway: 0 | 1;
  first_seen: number;
  last_seen: number;
  online: boolean;
}

interface ScanStatus {
  running: boolean;
  progress: number;      // 0-100
  found: number;
  checking: string | null;
  last_scan_id: number | null;
  last_scan_at: number | null;
}

interface NetmapStore {
  devices: Device[];
  scan: ScanStatus;
  selectedMac: string | null;

  setDevices: (devices: Device[]) => void;
  setScan: (scan: Partial<ScanStatus>) => void;
  selectDevice: (mac: string | null) => void;
  upsertDevice: (device: Device) => void;
}

export const useNetmapStore = create<NetmapStore>((set) => ({
  devices: [],
  scan: { running: false, progress: 0, found: 0, checking: null, last_scan_id: null, last_scan_at: null },
  selectedMac: null,

  setDevices: (devices) => set({ devices }),
  setScan: (scan) => set((s) => ({ scan: { ...s.scan, ...scan } })),
  selectDevice: (mac) => set({ selectedMac: mac }),
  upsertDevice: (device) =>
    set((s) => {
      const idx = s.devices.findIndex((d) => d.mac === device.mac);
      if (idx === -1) return { devices: [...s.devices, device] };
      const next = [...s.devices];
      next[idx] = device;
      return { devices: next };
    }),
}));
```

## Polling scan status

Poll GET /api/scan/status every 2 seconds while a scan is running; fall back to 10 seconds when idle.

```ts
// packages/client/src/hooks/useScanPoller.ts
import { useEffect, useRef } from 'react';
import { useNetmapStore } from '../store/useNetmapStore';

export function useScanPoller() {
  const { scan, setScan, setDevices } = useNetmapStore();
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    let active = true;

    async function poll() {
      try {
        const res = await fetch('/api/scan/status');
        const data = await res.json();
        if (!active) return;
        setScan(data);
        if (!data.running) {
          // Refresh device list when scan completes
          const devRes = await fetch('/api/devices');
          const devData = await devRes.json();
          if (active) setDevices(devData);
        }
      } catch {
        // ignore transient errors
      }
      if (active) {
        const interval = scan.running ? 2000 : 10_000;
        timerRef.current = setTimeout(poll, interval);
      }
    }

    poll();
    return () => {
      active = false;
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, [scan.running]);
}
```

## D3 force layout - positions only, React renders

D3 mutates node objects in place. Keep the simulation in a ref and copy positions to React state each tick.

```ts
// packages/client/src/hooks/useForceLayout.ts
import { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';

interface NodeDatum extends d3.SimulationNodeDatum {
  id: string;   // MAC address
  is_gateway: boolean;
}

interface EdgeDatum {
  source: string;
  target: string;
}

interface LayoutNode extends NodeDatum {
  x: number;
  y: number;
}

export function useForceLayout(
  nodes: NodeDatum[],
  edges: EdgeDatum[],
  width: number,
  height: number
) {
  const simRef = useRef<d3.Simulation<NodeDatum, EdgeDatum> | null>(null);
  const [positions, setPositions] = useState<Map<string, { x: number; y: number }>>(new Map());

  useEffect(() => {
    if (simRef.current) simRef.current.stop();

    const sim = d3
      .forceSimulation(nodes)
      .force('link', d3.forceLink<NodeDatum, EdgeDatum>(edges).id((d) => d.id).distance(120))
      .force('charge', d3.forceManyBody().strength(-300))
      .force('center', d3.forceCenter(width / 2, height / 2))
      .force('collision', d3.forceCollide(30));

    // Fix gateway at center
    nodes.forEach((n) => {
      if (n.is_gateway) { n.fx = width / 2; n.fy = height / 2; }
    });

    sim.on('tick', () => {
      const map = new Map<string, { x: number; y: number }>();
      nodes.forEach((n) => { map.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 }); });
      setPositions(new Map(map));  // copy to trigger React render
    });

    simRef.current = sim;
    return () => { sim.stop(); };
  }, [nodes.length, edges.length, width, height]);

  return positions;
}
```

## NetworkGraph SVG component

React renders the SVG entirely from the positions map. D3 never touches the DOM.

```tsx
// packages/client/src/components/NetworkGraph.tsx
import { useForceLayout } from '../hooks/useForceLayout';
import { useNetmapStore } from '../store/useNetmapStore';

export function NetworkGraph({ width, height }: { width: number; height: number }) {
  const { devices, selectedMac, selectDevice } = useNetmapStore();

  const nodes = devices.map((d) => ({ id: d.mac, is_gateway: d.is_gateway === 1 }));
  const gatewayMac = devices.find((d) => d.is_gateway === 1)?.mac;
  const edges = devices
    .filter((d) => d.is_gateway !== 1 && gatewayMac)
    .map((d) => ({ source: gatewayMac!, target: d.mac }));

  const positions = useForceLayout(nodes, edges, width, height);

  function nodeColor(d: typeof devices[0]) {
    if (d.is_gateway === 1) return '#d97706';
    if (d.mac === selectedMac) return '#2563eb';
    if (d.online) return '#16a34a';
    return '#a8a29e';
  }

  return (
    <svg width={width} height={height}>
      {edges.map((e) => {
        const s = positions.get(e.source);
        const t = positions.get(e.target);
        if (!s || !t) return null;
        return (
          <line
            key={`${e.source}-${e.target}`}
            x1={s.x} y1={s.y}
            x2={t.x} y2={t.y}
            stroke="rgba(217,119,6,0.25)"
            strokeWidth={1.5}
          />
        );
      })}
      {devices.map((d) => {
        const pos = positions.get(d.mac);
        if (!pos) return null;
        const label = d.label ?? d.hostname ?? d.ip.split('.').at(-1) ?? '?';
        return (
          <g key={d.mac} onClick={() => selectDevice(d.mac)} style={{ cursor: 'pointer' }}>
            <circle cx={pos.x} cy={pos.y} r={d.is_gateway === 1 ? 26 : 18} fill={nodeColor(d)} opacity={d.online ? 0.9 : 0.5} />
            <text x={pos.x} y={pos.y + 4} textAnchor="middle" fill="#fff" fontFamily="monospace" fontSize={9}>{label.slice(0, 5)}</text>
            <text x={pos.x} y={pos.y + 22} textAnchor="middle" fill="#a8a29e" fontFamily="monospace" fontSize={8}>.{d.ip.split('.').at(-1)}</text>
          </g>
        );
      })}
    </svg>
  );
}
```

## Starting a scan via POST

```ts
async function startScan() {
  const res = await fetch('/api/scan/start', { method: 'POST' });
  if (!res.ok) throw new Error('Scan failed to start');
  // useScanPoller will pick up the running state automatically
}
```

## Device label update

```ts
async function saveLabel(mac: string, label: string, grp: string) {
  await fetch(`/api/devices/${encodeURIComponent(mac)}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ label, grp }),
  });
}
```

## PATCH route on server

```ts
// packages/server/src/routes/devices.ts
router.patch('/:mac', (req, res) => {
  const { mac } = req.params;
  const { label, grp } = req.body as { label?: string; grp?: string };

  const stmt = db.prepare(`
    UPDATE devices SET label = COALESCE(@label, label), grp = COALESCE(@grp, grp)
    WHERE mac = @mac
  `);
  const result = stmt.run({ mac, label: label ?? null, grp: grp ?? null });

  if (result.changes === 0) return res.status(404).json({ error: 'Device not found' });
  const updated = db.prepare('SELECT * FROM devices WHERE mac = ?').get(mac);
  res.json(updated);
});
```

## New device detection

After each scan completes, compare new device MACs against previously known MACs. Emit `new_devices` SSE event or flag in GET /api/scan/status.

```ts
// packages/server/src/scanner.ts (after scan loop)
const newMacs = discoveredMacs.filter((mac) => !knownMacSet.has(mac));
if (newMacs.length > 0) {
  // Store flag so the next GET /api/scan/status response includes new_devices array
  lastScanNewDevices = newMacs;
}
```

## SQL: grp is the column name

`group` is a reserved word in SQLite. Use `grp` throughout:

```sql
SELECT id, ip, mac, hostname, vendor, label, grp, is_gateway, first_seen, last_seen
FROM devices
WHERE grp = @grp
ORDER BY ip
```

## OUI lookup helper

```ts
// packages/server/src/oui.ts
import fs from 'fs';
import path from 'path';

// Map from "AA:BB:CC" -> "Vendor Name"
let ouiMap: Map<string, string> | null = null;

export function lookupVendor(mac: string): string | null {
  if (!ouiMap) {
    ouiMap = new Map();
    const raw = fs.readFileSync(path.join(__dirname, '../data/oui.txt'), 'utf8');
    for (const line of raw.split('\n')) {
      const m = line.match(/^([0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2})\s+\(hex\)\s+(.+)$/);
      if (m) ouiMap.set(m[1].replace(/-/g, ':'), m[2].trim());
    }
  }
  const prefix = mac.toUpperCase().slice(0, 8);
  return ouiMap.get(prefix) ?? null;
}
```

## Vite proxy config

```ts
// packages/client/vite.config.ts
export default {
  server: {
    proxy: {
      '/api': 'http://localhost:3000',
    },
  },
};
```

Related Skills

Skill: Network scanning with Node.js net.Socket

7
from heldernoid/agentic-build-templates

## When to use

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".

poll-builder

7
from heldernoid/agentic-build-templates

Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.

Skill: personal-finance

7
from heldernoid/agentic-build-templates

## Overview

Skill: csv-import

7
from heldernoid/agentic-build-templates

## Overview

Skill: Syntax Highlighting

7
from heldernoid/agentic-build-templates

## Purpose