MEMORY.md 8.8 KB

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

# 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 <jwt>
  5. SSE uses ?token=<jwt> query param (EventSource doesn't support headers)
  6. Routers continue to use legacy shared TOKEN (not JWT)

Default Credentials

  • Username: admin
  • Password: adminCHANGE 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