# client2server — Project Memory > Bi-directional event forwarder: OpenWrt routers → Go server → Redpanda (Kafka) → LuIS backend. > + Apple-style React dashboard for monitoring and control. ## Purpose - Routers push events (DHCP leases, WiFi connects/disconnects, WAN state) to a central server. - Two delivery paths: WebSocket long-lived connection for command roundtrips, and direct HTTP POST for hotplug-triggered events. - Server fans out via Redpanda topics; LuIS backend consumes. - Server can also push commands back to routers (uci_set, shell, reboot, wifi_restart, status). - Dashboard (new in 2.1) provides Apple-style web UI for monitoring and control. ## Stack | 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) | ## Repo Layout (current) ``` 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 ``` ## Endpoints (Go server, port 3843) | 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 Types (router → server) | 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 | ## Commands (server → router) | Command | Args | Notes | |---------|------|-------| | `reboot` | — | dangerous | | `wifi_restart` | — | | | `status` | — | | | `shell` | `command` | dangerous | | `uci_set` | `config,section,option,value` | dangerous | ## Ports | 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 | ## Quick Run ```bash # 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" ``` ## Auth Flow 1. User visits dashboard → redirected to /login 2. POSTs username/password to /api/auth/login → gets JWT 3. JWT stored in localStorage as `c2s_token` 4. Every API call attaches `Authorization: Bearer ` 5. SSE uses `?token=` query param (EventSource doesn't support headers) 6. Routers continue to use legacy shared TOKEN (not JWT) ## Default Credentials - Username: `admin` - Password: `admin` — **CHANGE IN PRODUCTION** - Role: `system_admin` (can do everything) To add users: connect to SQLite, INSERT into `users` table with scrypt hash from `HashPassword()`. ## Architecture: Hybrid Event Delivery (router side) The router has two parallel event paths: 1. **Hotplug path (instant)** — kernel fires, shell runs, curl POSTs to `/api/events` 2. **Lua state-diff path (≤1s)** — luv async loop polls state, sends diffs Both paths POST to `/api/events`; server saves to SQLite, publishes to Redpanda, broadcasts via SSE. ## Evolution (commit history) - `6d6780c` — Removed legacy event-forwarder + unused Lua clients, fixed event names, wired UCI to hotplug - `9f2269a` — Migrated Go server to coder/websocket + franz-go (the legacy deps didn't exist/weren't archived), unified port 3843 - `6af966d` — Added SQLite, JWT auth, SSE live feed, metrics, alerts, Redpanda consumer - `5b57be3` — Built the Apple-style React dashboard SPA + fixed publish() to be fire-and-forget (was blocking HTTP for 3s when Redpanda down) ## Known Issues / TODO - ⚠️ Default admin/admin must be changed in production - ⚠️ JWT_SECRET needs to be set in env (32+ bytes) — server falls back to insecure dev key - ⚠️ `/api/routers` has no auth (intentional for monitoring but flag for production) - ⚠️ Caddyfile email is commented out — needed if `auto_https on` - ⚠️ UCI default `wss://your-server.com/ws` is a placeholder - ⚠️ `unified.lua` is 759 lines — could be split into modules (DHCP/WiFi/WAN/WS/CMD) - ⚠️ No automated tests for the Go server (smoke tests done manually, all endpoints work) - ⚠️ Redpanda topic creation is best-effort; production should manage topics explicitly - ⚠️ Dashboard bundle is 742KB (219KB gzip) — could be code-split with manualChunks - ⚠️ `franz-go` retries silently in background; failed publishes only log, no metric for publish failures - ⚠️ Alerts only fire from offline watcher (30s tick); no flap detection, no recovery notifications beyond clearing on reconnect ## Author Luis Rosales — MIT License 2026