|
@@ -1,23 +1,29 @@
|
|
|
# client2server — Project Memory
|
|
# client2server — Project Memory
|
|
|
|
|
|
|
|
> Bi-directional event forwarder: OpenWrt routers → Go server → Redpanda (Kafka) → LuIS backend.
|
|
> Bi-directional event forwarder: OpenWrt routers → Go server → Redpanda (Kafka) → LuIS backend.
|
|
|
|
|
+> + Apple-style React dashboard for monitoring and control.
|
|
|
|
|
|
|
|
## Purpose
|
|
## Purpose
|
|
|
- Routers push events (DHCP leases, WiFi connects/disconnects, WAN state) to a central server.
|
|
- 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.
|
|
- 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 fans out via Redpanda topics; LuIS backend consumes.
|
|
|
- Server can also push commands back to routers (uci_set, shell, reboot, wifi_restart, status).
|
|
- 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
|
|
## Stack
|
|
|
| Layer | Tech |
|
|
| Layer | Tech |
|
|
|
|-------|------|
|
|
|-------|------|
|
|
|
| Router client | Lua (OpenWrt), `client2server-unified.lua` |
|
|
| Router client | Lua (OpenWrt), `client2server-unified.lua` |
|
|
|
| Hotplug paths | Shell + curl, `/etc/hotplug.d/{wireless,dhcp}/*` |
|
|
| Hotplug paths | Shell + curl, `/etc/hotplug.d/{wireless,dhcp}/*` |
|
|
|
-| Transport | WebSocket + HTTP (unified port) via Caddy LB |
|
|
|
|
|
-| WebSocket lib | `github.com/coder/websocket` v1.8.13 (fork of nhooyr.io) |
|
|
|
|
|
|
|
+| Server runtime | Go 1.22 |
|
|
|
|
|
+| WebSocket lib | `github.com/coder/websocket` v1.8.13 |
|
|
|
| Event bus | Redpanda (Kafka-compatible) :9092 |
|
|
| Event bus | Redpanda (Kafka-compatible) :9092 |
|
|
|
| Kafka client | `github.com/twmb/franz-go` v1.18.0 + `pkg/kadm` v1.14.0 |
|
|
| Kafka client | `github.com/twmb/franz-go` v1.18.0 + `pkg/kadm` v1.14.0 |
|
|
|
-| Server | Go 1.22 |
|
|
|
|
|
|
|
+| 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)
|
|
## Repo Layout (current)
|
|
|
```
|
|
```
|
|
@@ -25,30 +31,62 @@ client2server/
|
|
|
├── ARCHITECTURE.md # Mermaid diagrams, full spec
|
|
├── ARCHITECTURE.md # Mermaid diagrams, full spec
|
|
|
├── README.md # User-facing docs
|
|
├── README.md # User-facing docs
|
|
|
├── MEMORY.md # ← you are here
|
|
├── MEMORY.md # ← you are here
|
|
|
-├── Caddyfile # LB + reverse proxy (single port :3843)
|
|
|
|
|
-├── docker-compose.yml # redpanda + 2× server + caddy
|
|
|
|
|
-├── package/
|
|
|
|
|
-│ ├── Makefile # IPK build (includes hotplug)
|
|
|
|
|
-│ ├── src/
|
|
|
|
|
-│ │ └── client2server-unified.lua # CANONICAL Lua client
|
|
|
|
|
-│ ├── files/
|
|
|
|
|
-│ │ ├── etc/config/client2server # UCI defaults
|
|
|
|
|
-│ │ └── etc/init.d/client2server # Procd init script (exports UCI → env)
|
|
|
|
|
-│ └── hotplug/
|
|
|
|
|
-│ ├── 01-wifi # wireless hotplug → wifi_connected/disconnected
|
|
|
|
|
-│ └── 02-dhcp # dhcp hotplug → dhcp_lease_new/expire
|
|
|
|
|
-└── server/
|
|
|
|
|
- ├── main.go # WS handler + HTTP API + Redpanda producer
|
|
|
|
|
- ├── go.mod
|
|
|
|
|
- ├── go.sum
|
|
|
|
|
- └── Dockerfile # Go 1.22 builder + alpine runtime
|
|
|
|
|
|
|
+├── 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 Types (router → server)
|
|
|
| Event | Source | Payload |
|
|
| Event | Source | Payload |
|
|
|
|-------|--------|---------|
|
|
|-------|--------|---------|
|
|
|
-| `dhcp_lease_new` | dnsmasq (luv timer + hotplug) | mac, ip, hostname |
|
|
|
|
|
-| `dhcp_lease_expire` | dnsmasq (luv timer + hotplug) | mac, old_ip |
|
|
|
|
|
|
|
+| `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_up` | /sys/class/net/* | device |
|
|
|
| `wan_link_down` | /sys/class/net/* | device |
|
|
| `wan_link_down` | /sys/class/net/* | device |
|
|
|
| `wan_dhcp_new` | ubus | new_ip |
|
|
| `wan_dhcp_new` | ubus | new_ip |
|
|
@@ -57,45 +95,39 @@ client2server/
|
|
|
| `wifi_disconnected` | hostapd hotplug | mac, interface |
|
|
| `wifi_disconnected` | hostapd hotplug | mac, interface |
|
|
|
|
|
|
|
|
## Commands (server → router)
|
|
## Commands (server → router)
|
|
|
-| Command | Args |
|
|
|
|
|
-|---------|------|
|
|
|
|
|
-| `uci_set` | config, section, option, value |
|
|
|
|
|
-| `shell` | command |
|
|
|
|
|
-| `reboot` | — |
|
|
|
|
|
-| `wifi_restart` | — |
|
|
|
|
|
-| `status` | — |
|
|
|
|
|
|
|
+| Command | Args | Notes |
|
|
|
|
|
+|---------|------|-------|
|
|
|
|
|
+| `reboot` | — | dangerous |
|
|
|
|
|
+| `wifi_restart` | — | |
|
|
|
|
|
+| `status` | — | |
|
|
|
|
|
+| `shell` | `command` | dangerous |
|
|
|
|
|
+| `uci_set` | `config,section,option,value` | dangerous |
|
|
|
|
|
|
|
|
## Ports
|
|
## Ports
|
|
|
| Service | Port | Notes |
|
|
| Service | Port | Notes |
|
|
|
|---------|------|-------|
|
|
|---------|------|-------|
|
|
|
-| Caddy LB (WS + HTTP) | 3843 | Routers connect here, API served here too |
|
|
|
|
|
-| Go server (×2) | 3843 (internal) | Behind Caddy, not directly exposed |
|
|
|
|
|
-| Redpanda Kafka | 9092 | Internal Docker network |
|
|
|
|
|
|
|
+| 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 |
|
|
| Redpanda REST | 8082 | Schema/management |
|
|
|
-| Redpanda Schema | 8081 | Schema registry |
|
|
|
|
|
-
|
|
|
|
|
-## HTTP API
|
|
|
|
|
-All endpoints on `:3843`, behind Caddy LB.
|
|
|
|
|
-
|
|
|
|
|
-| Method | Path | Auth | Purpose |
|
|
|
|
|
-|--------|------|------|---------|
|
|
|
|
|
-| `GET` | `/health` | none | Liveness + router stats |
|
|
|
|
|
-| `POST` | `/api/events` | Bearer | Event ingestion (hotplug + lua) |
|
|
|
|
|
-| `GET` | `/api/routers` | none (recommended: add Bearer) | List known routers |
|
|
|
|
|
-| `POST` | `/api/command` | Bearer | Send command to router; awaits result |
|
|
|
|
|
-| `GET` | `/ws` | query `?token=` or `Authorization: Bearer` | WebSocket upgrade |
|
|
|
|
|
|
|
+| Dashboard dev (Vite) | 5173 | Local dev only |
|
|
|
|
|
|
|
|
## Quick Run
|
|
## Quick Run
|
|
|
```bash
|
|
```bash
|
|
|
# Full stack
|
|
# Full stack
|
|
|
cd /root/.openclaw/workspace/client2server
|
|
cd /root/.openclaw/workspace/client2server
|
|
|
-TOKEN=*** docker-compose up -d
|
|
|
|
|
|
|
+# 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
|
|
|
|
|
|
|
|
-# Server only (Go 1.22+ required)
|
|
|
|
|
|
|
+# Local dev: server + Vite dashboard
|
|
|
cd server && go build -o server . && \
|
|
cd server && go build -o server . && \
|
|
|
- REDPANDA_BROKERS=localhost:9092 TOKEN=*** PORT=3843 ./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 (manual)
|
|
|
|
|
|
|
+# Install on router
|
|
|
scp package/src/client2server-unified.lua root@router:/usr/sbin/
|
|
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/init.d/client2server root@router:/etc/init.d/
|
|
|
scp package/files/etc/config/client2server root@router:/etc/config/
|
|
scp package/files/etc/config/client2server root@router:/etc/config/
|
|
@@ -103,59 +135,48 @@ scp package/hotplug/01-wifi root@router:/etc/hotplug.d/wireless/
|
|
|
scp package/hotplug/02-dhcp root@router:/etc/hotplug.d/dhcp/
|
|
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 "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"
|
|
ssh root@router "/etc/init.d/client2server enable && /etc/init.d/client2server start"
|
|
|
-
|
|
|
|
|
-# Or build IPK
|
|
|
|
|
-make package/client2server-unified/ipk
|
|
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## Architecture: Hybrid Event Delivery
|
|
|
|
|
-The router has **two parallel event paths** to the server:
|
|
|
|
|
|
|
+## 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)
|
|
|
|
|
|
|
|
-1. **Hotplug path (instant)** — kernel fires, shell runs, curl POSTs
|
|
|
|
|
- - `01-wifi` for WiFi connect/disconnect
|
|
|
|
|
- - `02-dhcp` for DHCP lease add/del
|
|
|
|
|
- - Latency: ~10ms
|
|
|
|
|
|
|
+## 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
|
|
2. **Lua state-diff path (≤1s)** — luv async loop polls state, sends diffs
|
|
|
- - SSID name changes
|
|
|
|
|
- - `wan_link_up/down`
|
|
|
|
|
- - `wan_dhcp_new/changed`
|
|
|
|
|
- - `dhcp_lease_new/expire` (fallback / redundancy with hotplug)
|
|
|
|
|
-
|
|
|
|
|
-Both paths post to the same `POST /api/events` endpoint.
|
|
|
|
|
-
|
|
|
|
|
-## Environment Wiring (init.d → hotplug)
|
|
|
|
|
-- `init.d/client2server` reads UCI on `start()` and writes `/var/run/client2server.env`
|
|
|
|
|
-- Exports `SERVER_URL`, `TOKEN`, `ROUTER_ID` to Lua's environment
|
|
|
|
|
-- Hotplug scripts also `uci get` directly as fallback (in case called outside init.d context)
|
|
|
|
|
-- Env file removed on `stop()`
|
|
|
|
|
-
|
|
|
|
|
-## Server Architecture (Go)
|
|
|
|
|
-- Single `main.go` (~600 lines) — WS handler, HTTP handlers, Redpanda producer
|
|
|
|
|
-- In-memory state: `routers`, `routerQueues` (per-router offline queue), `pendingCmds` (awaiting result), `executedCmds` (idempotency)
|
|
|
|
|
-- Background janitor: cleans `executedCmds` after `idempotencyTTL` (5 min)
|
|
|
|
|
-- **Publish timeout**: 3s per `kcl.ProduceSync()` call — if Redpanda is down, HTTP returns 502 instead of hanging
|
|
|
|
|
-- WebSocket auth: `?token=` query param or `Authorization: Bearer` header
|
|
|
|
|
-- HTTP API auth: `Authorization: Bearer` or raw token
|
|
|
|
|
-- Single port (3843) — mux routes `/ws` to WS handler, `/api/*` to REST, `/health` to liveness
|
|
|
|
|
-
|
|
|
|
|
-## Client Evolution
|
|
|
|
|
-The Lua client went through ~6 architectural rewrites in 2 days (Jun 7 2026) trying to kill polling. Final settled state = hybrid hotplug + luv state-diff.
|
|
|
|
|
-
|
|
|
|
|
-The Go server went through a dependency overhaul on 2026-06-09:
|
|
|
|
|
-- `nhooyr.io/websocket` → `github.com/coder/websocket` (nhooyr archived, coder is the maintained fork; same API)
|
|
|
|
|
-- `github.com/redpanda-data/redpanda-sdk-go` (which never existed as a public module) → `github.com/twmb/franz-go` + `kadm` (real, fast, pure Go Kafka client — Redpanda speaks Kafka wire protocol natively)
|
|
|
|
|
-- Unified WebSocket and HTTP on port 3843 (Caddy handles routing)
|
|
|
|
|
|
|
+
|
|
|
|
|
+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
|
|
## Known Issues / TODO
|
|
|
-- ⚠️ Caddyfile email is commented out — set real value if enabling `auto_https`
|
|
|
|
|
-- ⚠️ UCI default `wss://your-server.com/ws` is a placeholder — must be edited per-deployment
|
|
|
|
|
-- ⚠️ Docker-compose TOKEN is the literal string `***` — override via env or `.env` file
|
|
|
|
|
-- ⚠️ `/api/routers` currently has no auth — should require Bearer (intentional for monitoring, but flag it)
|
|
|
|
|
-- `unified.lua` is 759 lines — worth splitting into modules (DHCP/WiFi/WAN/WS/CMD) but functional as-is
|
|
|
|
|
-- No automated tests for the Go server; manual smoke test confirms endpoints work (build, /health 200, /api/events 401/502, /api/command queues while offline)
|
|
|
|
|
-- Redpanda topic creation is best-effort (relies on `AUTO_CREATE_TOPICS=true` in dev); production should manage topics explicitly
|
|
|
|
|
-- `command_id` idempotency is server-side only — clients should pass `id` in `RouterCommand` for replay safety
|
|
|
|
|
|
|
+- ⚠️ 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
|
|
## Author
|
|
|
Luis Rosales — MIT License 2026
|
|
Luis Rosales — MIT License 2026
|