Context

I run an OpenClaw instance on a headless Mac mini: a Codex primary agent, an Ollama fallback and a WhatsApp channel. With no screen and no public exposure, I needed a single place to check service health and talk to the agent.

clawdeck fills that gap with a web dashboard reachable only over Tailscale. Two phases are in place: a health panel (phase 1) and a chat (phase 2).

Technical approach

The backend is written in TypeScript with Bun and Hono; it also serves the built front end (React, Vite, Tailwind, dark theme). Statuses stream in real time over SSE on /api/status, while chat runs through a relayed WebSocket on /api/chat/ws. SQLite (bun:sqlite) persists only the ping history.

The health panel shows the status of the OpenClaw gateway, Ollama and its fallback model, pings to Cloudflare and the network gateway, and a 24h/7d latency graph. The chat relays markdown, visible tool calls and streaming to the front end. On the security side: bind on 127.0.0.1 or a Tailscale IP, never 0.0.0.0; auth via AUTH_TOKEN and GATEWAY_AUTH_TOKEN; the gateway connection is authenticated by device identity (Ed25519).

What I took away

I sharpened how I pick real-time transports: SSE for one-way statuses, WebSocket for a bidirectional relayed chat. Keeping persistence minimal (pings only) simplified storage without losing what mattered.

The project also pushed me to think about the security posture of a self-hosted service: a network surface limited to Tailscale, no public bind, and authentication on both the dashboard access and the link to the gateway.