Bi-directional event forwarder: OpenWrt routers → Go server → Redpanda (Kafka) → LuIS backend.
- Apple-style React dashboard for monitoring and control.
| Layer | Tech |
|---|---|
| Router client | Lua (OpenWrt), client2server-unified.lua |
| Hotplug paths | Shell + curl, /etc/hotplug.d/{wireless,dhcp}/* |
| Server runtime | Go 1.22 |
| WebSocket lib | github.com/coder/websocket v1.8.13 |
| Event bus | Redpanda (Kafka-compatible) :9092 |
| Kafka client | github.com/twmb/franz-go v1.18.0 + pkg/kadm v1.14.0 |
| Storage | SQLite (modernc.org/sqlite, pure Go, no cgo) |
| Auth | JWT (HS256) with scrypt password hashing |
| Live feed | Server-Sent Events (/api/events/stream) |
| Frontend | React 19 + TypeScript + Vite + Tailwind v3 + Framer Motion + Recharts + TanStack Query + Wouter |
| Dashboard hosting | Caddy (serves SPA + proxies /api, /ws) |
client2server/
├── ARCHITECTURE.md # Mermaid diagrams, full spec
├── README.md # User-facing docs
├── MEMORY.md # ← you are here
├── Caddyfile # LB: 3843 → servers, 80/443 → dashboard
├── docker-compose.yml # redpanda + 2× server + dashboard + caddy
├── package/ # OpenWrt IPK build
│ ├── Makefile
│ ├── src/client2server-unified.lua
│ ├── files/etc/{config,init.d}/client2server
│ └── hotplug/{01-wifi,02-dhcp}
├── server/ # Go server
│ ├── main.go # WS handler + HTTP API + ingestEvent
│ ├── auth.go # JWT + scrypt + login + middleware
│ ├── consumer.go # Redpanda → SQLite consumer
│ ├── metrics.go # 1-min buckets, 24h retention
│ ├── sse.go # SSE broadcaster
│ ├── store.go # SQLite (users, events, commands, alerts)
│ ├── go.mod
│ ├── go.sum
│ └── Dockerfile
└── dashboard/ # React SPA
├── src/
│ ├── App.tsx # Routes + auth gate
│ ├── main.tsx
│ ├── styles.css # Apple design tokens
│ ├── lib/{api,types,sse}.ts
│ ├── components/{Shell,ui}.tsx
│ └── pages/{Login,Overview,Routers,Events,Commands,Alerts}.tsx
├── screenshots/ # 6 retina PNGs (visual reference)
├── package.json
├── vite.config.ts # VITE_API_TARGET proxy
├── tailwind.config.js # Apple palette + display sizes
├── Dockerfile # Node builder + Caddy runtime
└── Caddyfile.production # /api + /ws proxy, SPA fallback
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health |
none | Liveness + router stats |
| POST | /api/auth/login |
none | {username,password} → {token,role} |
| GET | /api/auth/me |
JWT | Current user info |
| POST | /api/events |
JWT or legacy token | Ingest event from router/hotplug |
| GET | /api/events/list |
JWT | Historical events (filter by router_id, event_type) |
| GET | /api/events/stream |
JWT or legacy token | Server-Sent Events live feed |
| GET | /api/routers |
none | Known routers (online, last_seen, queued) |
| POST | /api/command |
JWT | Send command, awaits result |
| GET | /api/commands |
JWT | Command history |
| GET | /api/metrics?since=1h |
JWT | Time-series metrics (1-min buckets) |
| GET | /api/alerts?unack=1 |
JWT | Alerts list |
| POST | /api/alerts/{id}/ack |
JWT | Acknowledge alert |
| GET | /ws |
legacy token | WebSocket from router |
| Event | Source | Payload |
|---|---|---|
dhcp_lease_new |
dnsmasq (luv + hotplug) | mac, ip, hostname |
dhcp_lease_expire |
dnsmasq (luv + hotplug) | mac, old_ip |
wan_link_up |
/sys/class/net/* | device |
wan_link_down |
/sys/class/net/* | device |
wan_dhcp_new |
ubus | new_ip |
wan_dhcp_changed |
ubus | old_ip, new_ip |
wifi_connected |
hostapd hotplug | mac, interface |
wifi_disconnected |
hostapd hotplug | mac, interface |
| Command | Args | Notes |
|---|---|---|
reboot |
— | dangerous |
wifi_restart |
— | |
status |
— | |
shell |
command |
dangerous |
uci_set |
config,section,option,value |
dangerous |
| Service | Port | Notes |
|---|---|---|
| Caddy (WS+HTTP API) | 3843 | Routers + API clients |
| Caddy (Dashboard) | 80/443 | Web UI |
| Go server (×2) | 3843 (internal) | Behind Caddy |
| Redpanda Kafka | 9092 | Internal |
| Redpanda REST | 8082 | Schema/management |
| Dashboard dev (Vite) | 5173 | Local dev only |
# Full stack
cd /root/.openclaw/workspace/client2server
# Set in .env or export:
# TOKEN=*** (legacy router/hotplug shared token)
# JWT_SECRET=*** (>= 32 bytes for dashboard)
TOKEN=*** JWT_SECRET=$(openssl rand -hex 32) docker-compose up -d
# Local dev: server + Vite dashboard
cd server && go build -o server . && \
REDPANDA_BROKERS=localhost:9092 TOKEN=*** JWT_SECRET=dev PORT=3843 ./server &
cd ../dashboard && VITE_API_TARGET=http://localhost:3843 npm run dev
# Install on router
scp package/src/client2server-unified.lua root@router:/usr/sbin/
scp package/files/etc/init.d/client2server root@router:/etc/init.d/
scp package/files/etc/config/client2server root@router:/etc/config/
scp package/hotplug/01-wifi root@router:/etc/hotplug.d/wireless/
scp package/hotplug/02-dhcp root@router:/etc/hotplug.d/dhcp/
ssh root@router "chmod +x /usr/sbin/client2server-unified.lua /etc/init.d/client2server /etc/hotplug.d/wireless/01-wifi /etc/hotplug.d/dhcp/02-dhcp"
ssh root@router "/etc/init.d/client2server enable && /etc/init.d/client2server start"
c2s_tokenAuthorization: Bearer <jwt>?token=<jwt> query param (EventSource doesn't support headers)adminadmin — CHANGE IN PRODUCTIONsystem_admin (can do everything)To add users: connect to SQLite, INSERT into users table with scrypt hash from HashPassword().
The router has two parallel event paths:
/api/eventsBoth paths POST to /api/events; server saves to SQLite, publishes to Redpanda, broadcasts via SSE.
6d6780c — Removed legacy event-forwarder + unused Lua clients, fixed event names, wired UCI to hotplug9f2269a — Migrated Go server to coder/websocket + franz-go (the legacy deps didn't exist/weren't archived), unified port 38436af966d — Added SQLite, JWT auth, SSE live feed, metrics, alerts, Redpanda consumer5b57be3 — Built the Apple-style React dashboard SPA + fixed publish() to be fire-and-forget (was blocking HTTP for 3s when Redpanda down)/api/routers has no auth (intentional for monitoring but flag for production)auto_https onwss://your-server.com/ws is a placeholderunified.lua is 759 lines — could be split into modules (DHCP/WiFi/WAN/WS/CMD)franz-go retries silently in background; failed publishes only log, no metric for publish failuresLuis Rosales — MIT License 2026