# 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 <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: `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
