Skill: network-device-mapper
Application-level patterns for the network-device-mapper project.
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
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/network-device-mapper/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How Skill: network-device-mapper Compares
| Feature / Agent | Skill: network-device-mapper | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/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
## When to use
Skill: Uptime Monitoring
## Overview
Skill: Status Page
## Overview
Skill: unit-conversion
## Overview
Skill: recipe-scaler
## Overview
reading-list
Operate the reading-list API to save, manage, tag, search, and export articles.
email-digest
Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.
websocket-realtime
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
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
## Overview
Skill: csv-import
## Overview
Skill: Syntax Highlighting
## Purpose