A self-hosted network monitoring service for your home server. Continuously scans the local network for connected devices, tracks metadata and connection history, and sends configurable notifications when devices connect or disconnect.
- Automatic network scanning — discovers devices via
arp-scanor/proc/net/arpon a configurable interval - FRITZ!Box integration — queries your AVM FRITZ!Box router via the TR-064 API for richer metadata: online/offline status, interface type (WiFi vs Ethernet), and signal strength
- Device metadata — tracks IP address, hostname, MAC address, manufacturer (from IEEE OUI database), inferred device type, and connection history
- Event log — records every device discovered, connected, disconnected, or IP/hostname change
- Notification rules — create configurable rules that fire on events, with support for per-device filters, tag filters, and cooldowns
- Notification channels — Telegram bot, SMTP email, or any HTTP webhook (ntfy.sh, Home Assistant, etc.)
- Web dashboard — live device table with search, filter, and per-device detail pages
- Docker deployment — single
docker compose upwith persistent storage
The main device table with live online/offline status, statistics, activity chart, and search/filter controls.
Per-device metadata, 24 h presence timeline, and full event history.
Rule management — configure triggers, channels (Telegram, email, webhook), tag/MAC filters, and cooldowns.
┌─────────────────────────────────────────────────┐
│ SvelteKit Server │
│ │
│ hooks.server.ts ──► Scanner loop (setInterval) │
│ │ │ │
│ │ ┌────────┴────────┐ │
│ │ │ │ │
│ │ arp-scan FritzBox │
│ │ proc-arp TR-064 API │
│ │ │ │ │
│ │ └────────┬────────┘ │
│ │ merge by MAC │
│ │ │ │
│ ▼ ▼ │
│ KV Storage (Deno KV) ◄── Reconcile state │
│ │ │ │
│ │ emit DeviceEvents │
│ │ │ │
│ │ Notification Dispatcher │
│ │ Telegram / Email / Webhook │
│ │ │
│ REST API ◄────────────────────────────── │
│ /api/* │
│ │ │
│ Svelte UI (dashboard, device detail, rules) │
└─────────────────────────────────────────────────┘
Tech stack:
| Concern | Choice |
|---|---|
| Runtime | Deno 2.x |
| Frontend | SvelteKit 2 + Svelte 5 |
| Storage | unstorage + Deno KV driver (by default) |
| Build output | @sveltejs/adapter-node (run with Deno's Node.js compat) |
| Containerisation | Docker + Docker Compose |
Source layout:
src/
├── hooks.server.ts # Server init — starts the scanner background loop
├── lib/
│ ├── types.ts # Shared TypeScript interfaces
│ └── server/
│ ├── config.ts # Config loading (YAML file + env var overrides)
│ ├── logger.ts # Structured logger (pretty / JSON)
│ ├── storage/ # Deno KV CRUD — devices, events, rules, meta
│ ├── oui/ # IEEE OUI manufacturer lookup + device type heuristics
│ ├── scanner/ # arp-scan, proc-arp, FritzBox TR-064 adapters + poll loop
│ └── notifications/ # Telegram, email, webhook adapters + rule evaluator
└── routes/
├── +page.svelte # Dashboard
├── devices/[mac]/ # Device detail page
├── rules/ # Notification rules management
└── api/ # REST API endpoints
mkdir -p config data
cp config.example.yaml config/config.yamlAt minimum set your network interface:
scanner:
interface: eth0 # replace with your LAN interface (ip link to find it)Enable FRITZ!Box integration if you have one:
fritzbox:
enabled: true
host: fritz.box # or 192.168.178.1
password: "" # set via FRITZBOX_PASSWORD env var insteadcp .env.example .env
# Edit .env — add TELEGRAM_BOT_TOKEN, FRITZBOX_PASSWORD, etc.docker compose up -dThe dashboard is available at http://your-server:3000.
Note: The service uses
network_mode: hostin Docker Compose. This is required for ARP scanning to work — it gives the container direct access to the host's network interfaces.
All settings can be provided via config/config.yaml or environment variables. Env vars take precedence and are useful for secrets.
scanner:
interval_seconds: 60 # scan frequency
tool: auto # arp-scan | proc-arp | auto
interface: eth0 # network interface for arp-scan
offline_threshold_seconds: 180 # seconds without a response before marking offline
fritzbox:
enabled: false
host: fritz.box
port: 49000 # 49000 (HTTP) or 49443 (HTTPS)
username: ""
password: "" # prefer FRITZBOX_PASSWORD env var
server:
port: 3000
notifications:
telegram:
enabled: false
bot_token: "" # prefer TELEGRAM_BOT_TOKEN env var
chat_id: "" # prefer TELEGRAM_CHAT_ID env var
email:
enabled: false
smtp_host: ""
smtp_port: 587
smtp_user: ""
smtp_pass: "" # prefer SMTP_PASS env var
from: "scanner@example.com"
to: ["you@example.com"]
webhook:
enabled: false
url: "" # prefer WEBHOOK_URL env var
method: POST
headers: {} # e.g. { Authorization: "Bearer xxx" }
oui:
db_path: /data/oui.txt # IEEE OUI database (downloaded at Docker build time)
logging:
level: info # debug | info | warn | error
format: pretty # pretty | json| Variable | Description |
|---|---|
CONFIG_PATH |
Path to config.yaml (default: /config/config.yaml) |
KV_PATH |
Path to Deno KV database file (default: ./data/scanner.kv) |
FRITZBOX_PASSWORD |
FRITZ!Box password |
FRITZBOX_USERNAME |
FRITZ!Box username (often not needed) |
FRITZBOX_ENABLED |
Set to true to enable (overrides config) |
TELEGRAM_BOT_TOKEN |
Telegram bot token from @BotFather |
TELEGRAM_CHAT_ID |
Telegram chat or channel ID |
SMTP_HOST / SMTP_PORT |
SMTP server settings |
SMTP_USER / SMTP_PASS |
SMTP credentials |
WEBHOOK_URL |
Webhook target URL |
PORT |
HTTP port (default: 3000) |
LOG_LEVEL |
Log level override |
Rules are created from the web dashboard at /rules or via the API.
Each rule defines:
- Trigger — which event to react to
- Channel — where to send the notification (Telegram, email, or webhook)
- Filters (optional) — match only a specific MAC address or device tag
- Cooldown — minimum seconds between repeat notifications for the same rule
Available triggers:
| Trigger | Description |
|---|---|
any_new_device |
A MAC address is seen for the first time |
any_device_online |
Any device transitions from offline → online |
any_device_offline |
Any device transitions from online → offline |
specific_device_online |
A particular device (by MAC or tag) comes online |
specific_device_offline |
A particular device goes offline |
ip_changed |
A device's IP address changed |
{
"name": "New device alert",
"trigger": "any_new_device",
"channel": "telegram",
"cooldown_seconds": 0
}{
"name": "NAS offline",
"trigger": "specific_device_offline",
"filter_mac": "AA:BB:CC:DD:EE:FF",
"channel": "telegram",
"cooldown_seconds": 300
}All endpoints return { data: ... } JSON. Errors return { message: "..." }.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/devices |
List all devices. Query: ?online=true, ?tag=iot |
PATCH |
/api/devices/:mac |
Update custom_name, tags, or device_type |
GET |
/api/events |
Recent events. Query: ?limit=50, ?type=device_offline |
GET |
/api/rules |
List notification rules |
POST |
/api/rules |
Create a rule |
PATCH |
/api/rules/:id |
Update a rule (e.g. toggle enabled) |
DELETE |
/api/rules/:id |
Delete a rule |
POST |
/api/scan |
Trigger an immediate scan |
GET |
/api/scan |
Scanner status, last scan time, device counts |
The integration uses the TR-064 protocol (SOAP over HTTP) to query the router's host table.
- In your FRITZ!Box web interface go to Home Network → Network → Network Settings
- Enable "Allow access for applications" and "Transmit status information over UPnP"
- Set
fritzbox.enabled: trueand provide your FRITZ!Box password
When enabled, the FRITZ!Box becomes the authoritative source for online/offline status (it knows all DHCP-assigned devices). ARP scanning still runs alongside to catch any devices not using DHCP.
Requirements: Deno 2.x, arp-scan installed
# Install dependencies
deno install
# Start dev server (hot reload)
deno task dev
# Build for production
deno task build
# Run production build
deno run --allow-all --unstable-kv build/index.jsThe
--unstable-kvflag is required for Deno KV storage.
Set OUI_DB_PATH to a local copy of the IEEE OUI database for manufacturer lookup, or skip it (the service will warn but continue).
Storage is configured in storage.config.ts via unstorage. Default backend is Deno KV, stored at ./data/lanlord.kv (or remotely via KV_PATH). You can swap drivers (filesystem, memory, redis, …) in that file.
Key schema:
devices:{MAC} Device record
device_events:{MAC}:{timestamp} Per-device event log
events:{timestamp}:{MAC} Global event log (sorted by time)
rules:{id} Notification rule
meta:last_scan Last scan metadata
meta:scanner_status "idle" | "running"
MIT


