Skip to content

robotapp — Next.js 14 Dashboard

UI tier Next.js 14 TypeScript strict Tailwind 3 Cloudflare Pages

The browser-side of the stack. A single-page app that turns a running robot_agent into an interactive control surface — every endpoint typed, every WebSocket framed, every panel collapsible.

Stack

LayerChoiceWhy
FrameworkNext.js 14.2 (App Router, static export)Static export → Cloudflare Pages; no server runtime to manage
LanguageTypeScript 5 (strict: true)API contract is fully typed in lib/types.ts
StylingTailwindCSS 3 + PostCSSUtility-first; ~0 custom CSS bytes shipped
StateLocal useState / useRef + localStorageNo client-state lib needed for this app’s depth
StreamingNative WebSocket + zlib-deflate decodeDepth maps decoded in the browser
Deploywrangler 3.78 → Cloudflare PagesOne make deploy, custom domain pinned

Components

FileLOCResponsibility
CameraFeed.tsx615Multi-camera tabs, WebSocket frame decode, depth colormap, pixel-hover mm readout, rectangle annotation overlay
DevicePanel.tsx1070ROS scan, register pub/sub/service/action/WebRTC/TCP/LLM clients, encode/decode template editor
SkillPanel.tsx422Skill CRUD, hot-reload, per-skill JSON config editor with live diff
AgentPanel.tsx131Structured / unstructured prompt input, language selector (EN/KO/VI), Ctrl+Enter dispatch
PlanPanel.tsx157Live task-plan timeline with step status, expandable JSON results, inline log images
ButtonPanel.tsx268Server-persisted quick-action buttons with drag-reorder + bulk import
EnvPanel.tsx143Live ENV / HOME_LOC editor backed by /skill-configs

The depth decode pipeline

The trickiest piece of the frontend. ROS publishes 16-bit depth maps which the browser must reconstruct from a compressed binary frame:

// pseudocode of CameraFeed.tsx depth path
ws.onmessage = async (e) => {
const { mode, w, h, raw } = JSON.parse(e.data);
if (mode === 'raw') {
const u16 = new Uint16Array(await pako.inflate(decodeBase64(raw)).buffer);
const px = u16Length === w * h ? u16 : reshape(u16, w, h);
// hover: mm = px[y * w + x]
const rgba = applyJetColormap(px, dmin, dmax); // 4-channel ImageData
ctx.putImageData(new ImageData(rgba, w, h), 0, 0);
} else {
const img = new Image();
img.onload = () => ctx.drawImage(img, 0, 0);
img.src = `data:image/jpeg;base64,${raw}`; // pre-colorized JPEG
}
};

The same pipeline handles ROS sensor_msgs/CompressedImage (with the 12-byte compressedDepth header that the backend strips for us) and Image (16uc1, 32fc1).

API client

lib/api.ts exposes a typed function per endpoint. A few highlights:

  • Multi-robot registry in localStorage with active-robot toggle; every call routes to the selected base URL.
  • Legacy single-URL migration runs on app boot.
  • WebSocket helpers wrap /ws/agent and /ws/camera/{id} with typed event callbacks (AgentEvent, CameraFrame).

Routes

PathWhat it does
/app/page.tsxSingle-page dashboard (3-column responsive layout)
/app/layout.tsxRoot html/body + metadata

The whole app fits in one page because every panel is independently collapsible and the camera + plan panels are the main visual real estate.

Deploy

Terminal window
make install
make run-frontend # next dev on :3007
export CLOUDFLARE_API_TOKEN=<token> # needs "Cloudflare Pages: Edit"
make deploy # next build → out/ → wrangler pages deploy
make deploy-status # custom domain + SSL status

Static export config in next.config.js: output: 'export', trailingSlash: true, images.unoptimized: true. Pinned in Makefile: account ID, project name robotapp, custom domain robot.aistations.org.