# client2server

> **Bi-directional event forwarder + Apple-style dashboard for OpenWrt router fleets**

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

OpenWrt event forwarder with a real-time web dashboard, command queue, and persistent history.
Routers stream DHCP, WiFi, and WAN events over WebSocket; the Go server
fans them out to Redpanda and persists them to SQLite; a React/TypeScript
SPA gives you live visibility and command control from any browser.

## Overview

```
┌────────────────────────────────────────────────────────────────┐
│                    client2server Architecture                   │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│   OpenWrt Routers                                               │
│       │  WebSocket :3843  +  Hotplug (shared buffer)           │
│       ▼                                                          │
│   ┌──────────────┐                                              │
│   │ Caddy LB     │  ────────────────┐                          │
│   │  :80/443 UI  │  :3843 API/WS   │                          │
│   └──────┬───────┘  └──────┬────────┘                          │
│          │                  │                                    │
│          ▼                  ▼                                    │
│   ┌─────────────┐    ┌─────────────┐    ┌─────────────┐      │
│   │  Dashboard  │    │  server1   │    │  server2    │      │
│   │  (React)    │    │  (Go)      │    │  (Go)       │      │
│   │  /login     │    │  SQLite    │    │  SQLite     │      │
│   │  /routers   │    │  JWT       │    │  JWT        │      │
│   │  /events    │    │  SSE       │    │  SSE        │      │
│   │  /commands  │    │  WebSocket │    │  WebSocket  │      │
│   │  /alerts    │    └──────┬──────┘    └──────┬──────┘      │
│   └─────────────┘            │                  │              │
│                              └────────┬─────────┘              │
│                                       │                          │
│                                       ▼                          │
│                              ┌─────────────────┐               │
│                              │   Redpanda     │  Kafka protocol │
│                              │   :9092        │  Persisted     │
│                              └─────────────────┘               │
│                                                                 │
└────────────────────────────────────────────────────────────────┘
```

## Features

### Server (Go)
- ✅ **WebSocket** connection with auto-reconnect from routers
- ✅ **HTTP POST** fallback for hotplug scripts
- ✅ **Redpanda** (Kafka) event fan-out for downstream consumers
- ✅ **SQLite** persistence (events, commands, alerts, users)
- ✅ **JWT auth** with role-based access (system_admin / project_admin / user)
- ✅ **Server-Sent Events** for real-time dashboard feed
- ✅ **Time-series metrics** (1-min buckets, 24h retention)
- ✅ **Alerts** for offline routers (auto-clear on reconnect)
- ✅ **Command queue** for offline routers (auto-flush on reconnect)
- ✅ **Idempotency** (5-min TTL on command IDs)
- ✅ **Load balanced** behind Caddy (2× Go servers, health checks)

### Router client (Lua)
- ✅ **Hotplug-driven** WiFi connect/disconnect + DHCP lease events (instant, no polling)
- ✅ **luv-based** async state-diff for WAN link, DHCP renew, IP changes (≤1s latency)
- ✅ **UCI-configurable** server URL, token, check intervals
- ✅ **Unified event buffer** (`/var/run/client2server/buffer`) — shared by hotplug + Lua, survives internet outages
- ✅ **Wake-file signaling** — hotplug events nudge the agent for ~1s latency instead of waiting for the 30s flush cycle
- ✅ **Bidirectional** command execution (uci_set, shell, reboot, wifi_restart, status)

### Dashboard (React)
- ✅ **Apple-style** dark theme (frosted glass surfaces, gradient hero, tabular stat numbers)
- ✅ **Live event feed** via Server-Sent Events
- ✅ **Real-time charts** of events and commands over time
- ✅ **Router list** with online/offline status and queued command count
- ✅ **Command console** with target router picker and one-click reboot/wifi_restart/status
- ✅ **Alerts inbox** with one-click acknowledgement
- ✅ **Event history** with router-id and event-type filters

## Quick Start

### Option 1: Docker Compose (Recommended)

```bash
# Clone and run
git clone https://git3.techno-world.net/lrosales/client2server.git
cd client2server

# Set secrets (use any 32+ byte strings)
cat > .env <<EOF
TOKEN=replace-me
JWT_SECRET=$(openssl rand -hex 32)
EOF

# Start all services
docker-compose up -d

# Open the dashboard
open http://localhost/
# Login: admin / admin  (CHANGE IN PRODUCTION)

# Check status
docker-compose ps
```

### Option 2: Manual (Development)

```bash
# 1. Go server (with Redpanda running locally)
cd server
go build -o server .
REDPANDA_BROKERS=localhost:9092 \
  TOKEN=replace-me \
  JWT_SECRET=$(openssl rand -hex 32) \
  PORT=3843 \
  ./server

# 2. Dashboard (Vite dev server)
cd dashboard
npm install
VITE_API_TARGET=http://localhost:3843 npm run dev
# → http://localhost:5173

# 3. On OpenWrt router (copy all files)
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/
scp package/hotplug/_lib.sh root@router:/usr/share/client2server/hotplug-lib.sh
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 /usr/share/client2server/hotplug-lib.sh"
ssh root@router "/etc/init.d/client2server enable && /etc/init.d/client2server start"
```

## Ports

| Service | Port | Protocol |
|----------|------|----------|
| Dashboard (web UI) | 80 / 443 | HTTP(S) |
| WebSocket + HTTP API | 3843 | WS + HTTP (unified) |
| Redpanda Kafka | 9092 | Kafka (Redpanda) |
| Dashboard dev (Vite) | 5173 | HTTP (local dev only) |

## Configuration

### Environment Variables

```bash
TOKEN=replace-me                    # Legacy router/hotplug shared token
JWT_SECRET=replace-me-32+bytes       # JWT signing key for dashboard auth (32+ random bytes)
REDPANDA_BROKERS=redpanda:9092        # Comma-separated Redpanda addresses
PORT=3843                            # Server listen port
DB_PATH=/var/lib/c2s/client2server.db # SQLite path
```

### UCI Config (on Router)

```bash
# /etc/config/client2server
config client2server 'general'
    option enabled '1'

config server
    option url 'wss://your-server.com:3843'
    option token 'replace-me'

config wan
    option interface 'wan'
    option device 'eth0'
```

## HTTP API

All endpoints on `:3843`. The dashboard uses JWT (from `/api/auth/login`); routers use the legacy shared `TOKEN` directly.

| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET  | `/health` | none | Liveness + router stats |
| POST | `/api/auth/login` | none | `{username,password}` → JWT |
| GET  | `/api/auth/me` | JWT | Current user info |
| POST | `/api/events` | token or JWT | Ingest event from router/hotplug |
| GET  | `/api/events/list` | JWT | Historical events (filterable) |
| GET  | `/api/events/stream` | token or JWT | Server-Sent Events live feed |
| GET  | `/api/routers` | none | Known routers |
| POST | `/api/command` | token or JWT | Send command, awaits result |
| GET  | `/api/commands` | JWT | Command history |
| GET  | `/api/metrics?since=1h` | JWT | Time-series metrics |
| GET  | `/api/alerts?unack=1` | JWT | Alerts list |
| POST | `/api/alerts/{id}/ack` | JWT | Acknowledge alert |
| GET  | `/ws` | token | WebSocket from router |

## Commands

Send from server to router via WebSocket or HTTP API:

```bash
# Reboot
curl -X POST http://localhost:3843/api/command \
  -H "Authorization: Bearer <your-token-here>" \
  -d '{"router_id":"router1","command":"reboot"}'

# UCI set
curl -X POST http://localhost:3843/api/command \
  -H "Authorization: Bearer <your-token-here>" \
  -d '{"router_id":"router1","command":"uci_set","args":{"config":"network","section":"lan","option":"ipaddr","value":"192.168.1.1"}}'

# Shell
curl -X POST http://localhost:3843/api/command \
  -H "Authorization: Bearer <your-token-here>" \
  -d '{"router_id":"router1","command":"shell","args":{"command":"uptime"}}'
```

The token can be either the legacy `TOKEN` shared secret (used by routers and
hotplug scripts) or a JWT obtained from `POST /api/auth/login` (used by the
dashboard). The router doesn't need a JWT — it sends the shared `TOKEN` directly.

### Available Commands

| Command | Description | Arguments | Dangerous |
|---------|-------------|-----------|-----------|
| `reboot` | Reboot router | — | ⚠️ |
| `wifi_restart` | Restart WiFi | — | |
| `status` | Get router status | — | |
| `shell` | Run shell command | `command` | ⚠️ |
| `uci_set` | Set UCI config value | `config,section,option,value` | ⚠️ |

## Events

Router sends these events to server (via the shared on-disk buffer):

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

## Offline Behavior

Both event paths (hotplug scripts + Lua agent) share a single on-disk
buffer at `/var/run/client2server/buffer` (NDJSON, max 1000 events).
When the internet is down:

1. Hotplug scripts append events to the buffer + touch a wake file.
2. The Lua agent polls the wake file on every loop iteration (~1s).
3. The Lua agent tries to flush on reconnect; on failure, events stay
   in the buffer and reconnect logic kicks in (30s→5min backoff,
   10s when buffer >80% full).
4. When the buffer overflows, the **oldest** events are dropped (FIFO).

The buffer survives agent restarts and internet outages; it does NOT
survive router reboots (it's in tmpfs), which is acceptable since
network events from a rebooting router are stale anyway.

## Project Structure

```
client2server/
├── ARCHITECTURE.md         # Architecture docs
├── README.md               # This file
├── MEMORY.md               # Project memory (AI/agent context)
├── Caddyfile               # Caddy load balancer
├── docker-compose.yml      # Full stack
├── package/                # OpenWrt IPK build
│   ├── Makefile
│   ├── src/
│   │   └── client2server-unified.lua  # Router script (canonical)
│   ├── files/
│   │   ├── etc/init.d/client2server
│   │   └── etc/config/client2server
│   └── hotplug/
│       ├── 01-wifi         # /etc/hotplug.d/wireless/ - instant WiFi events
│       ├── 02-dhcp         # /etc/hotplug.d/dhcp/     - instant DHCP events
│       └── _lib.sh         # /usr/share/client2server/hotplug-lib.sh (shared buffer/spool)
├── 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
│   └── Dockerfile
└── dashboard/              # React SPA (Apple-style)
    ├── src/
    │   ├── App.tsx                 # Routes + auth gate
    │   ├── styles.css              # Apple design tokens
    │   ├── lib/{api,types,sse}.ts
    │   ├── components/{Shell,ui}.tsx
    │   └── pages/{Login,Overview,Routers,Events,Commands,Alerts}.tsx
    ├── screenshots/                # Visual reference (6 retina PNGs)
    ├── Dockerfile                  # Node builder + Caddy runtime
    └── Caddyfile.production        # /api + /ws proxy, SPA fallback
```

## Building IPK

```bash
# With OpenWrt SDK
./scripts/feeds update -a
./scripts/feeds install client2server-unified
make package/client2server-unified/compile
make package/client2server-unified/ipk

# Output
# bin/packages/*/client2server-unified_*.ipk
```

## Building Dashboard

```bash
cd dashboard
npm install
npm run build       # → dist/ (static files)
# Or development server:
VITE_API_TARGET=http://localhost:3843 npm run dev
```

## Monitoring

```bash
# Server logs
docker-compose logs -f server1

# Router logs (on OpenWrt)
logread -f -e client2server

# Redpanda
docker-compose logs -f redpanda

# API
curl http://localhost:3843/api/routers
curl http://localhost:3843/api/events
```

## Default Credentials

The Go server creates a default admin user on first run:

- **Username:** `admin`
- **Password:** `admin`

**Change this immediately in production.** Connect to the SQLite database
and replace the password hash, or extend the `/api/auth` endpoints to
expose a user-management UI.

## Security

- **JWT auth** for dashboard access (HS256, 24h tokens)
- **Legacy shared token** for router/hotplug access (simpler on minimal OpenWrt)
- **Use TLS** (`wss://`, `https://`) in production
- **Firewalls**: Only allow port 3843 from router network
- **Redpanda**: Enable auth for production
- **JWT_SECRET**: Must be 32+ random bytes in production
- **Bind Caddy to a reverse proxy** (Cloudflare, nginx) for HTTPS termination

## License

MIT - Luis Rosales 2026
