Browse Source

MEMORY: document dashboard, endpoints, auth flow, evolution, known issues

Gogs 2 months ago
parent
commit
d536349786
1 changed files with 115 additions and 94 deletions
  1. 115 94
      MEMORY.md

+ 115 - 94
MEMORY.md

@@ -1,23 +1,29 @@
 # 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}/*` |
-| 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 |
 | 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)
 ```
@@ -25,30 +31,62 @@ client2server/
 ├── ARCHITECTURE.md      # Mermaid diagrams, full spec
 ├── README.md            # User-facing docs
 ├── 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 | 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_down` | /sys/class/net/* | device |
 | `wan_dhcp_new` | ubus | new_ip |
@@ -57,45 +95,39 @@ client2server/
 | `wifi_disconnected` | hostapd hotplug | mac, interface |
 
 ## 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
 | 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 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
 ```bash
 # Full stack
 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 . && \
-  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/files/etc/init.d/client2server root@router:/etc/init.d/
 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/
 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"
-
-# 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
-   - 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
-- ⚠️ 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
 Luis Rosales — MIT License 2026