No Description

jarvis aea496d0c4 docs: render existing Mermaid blocks as SVG + PNG 1 month ago
dashboard 5b57be3a90 Dashboard: full Apple-style React SPA + fix publish blocking 2 months ago
docs aea496d0c4 docs: render existing Mermaid blocks as SVG + PNG 1 month ago
package ff7bf22d7a Lua: fix line 1 syntax error that broke all of 5.1 compatibility 2 months ago
server 5b57be3a90 Dashboard: full Apple-style React SPA + fix publish blocking 2 months ago
.gitignore 5b57be3a90 Dashboard: full Apple-style React SPA + fix publish blocking 2 months ago
ARCHITECTURE.md 2af96b034a Buffering: unify hotplug + Lua through shared on-disk buffer 2 months ago
Caddyfile 5b57be3a90 Dashboard: full Apple-style React SPA + fix publish blocking 2 months ago
MEMORY.md 87f476dcb1 docs(MEMORY): document Lua 5.1 compatibility rule for all OpenWrt code 2 months ago
README.md 2af96b034a Buffering: unify hotplug + Lua through shared on-disk buffer 2 months ago
client2server.dlog aa0a605f7c Adopt workspace .dlog deployment protocol 1 month ago
docker-compose.yml 5b57be3a90 Dashboard: full Apple-style React SPA + fix publish blocking 2 months ago

README.md

client2server

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

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)

# 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)

# 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

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)

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

# 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

# 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

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

Monitoring

# 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