Tools
Tools: I Let My AI Agent Build an Entire Smart Home Dashboard — Here's What Happened
2026-02-22
0 views
admin
The Premise ## What We Built: Mandakini Palace ## The Build Process: Agent-Driven Development ## Phase 1: The HTML Era (Day 1) ## Phase 2: The React Modernization (Day 2) ## Phase 3: The Cutover Saga (Day 2, Evening) ## Technical Decisions Worth Noting ## Architecture ## Server-Side API Caching ## Async Everything ## Performance Stack ## What Surprised Me ## 1. The Agent Has Taste ## 2. It Documents Like a Senior Engineer ## 3. It Learns From Mistakes ## 4. It Knows When to Shell Out ## What Didn't Work ## The Meta-Lesson ## Try It Yourself What happens when you give an AI coding agent access to your Raspberry Pi, a bunch of smart home APIs, and say "build me a dashboard"? I'm an engineer who runs OpenClaw — an open-source AI agent framework — as my personal assistant. It lives on a Raspberry Pi at home, connected to WhatsApp, with access to my filesystem, shell, and a growing collection of integrations. One weekend, I decided to push it: could it build and ship a full-stack web app, end to end, with minimal hand-holding? The answer was yes. And the journey was fascinating. "Mandakini Palace" (named after a river in Indian mythology — we like Sanskrit names around here) is a self-hosted smart home dashboard running on a Raspberry Pi. It's a React + TypeScript SPA backed by a Node.js API server, served behind Nginx with TLS. The home dashboard — each card is a self-contained app with a Sanskrit name It controls and monitors: Each app has a Sanskrit name (because why not): Prakasha Nivasa for lights, Netra Darpana for cameras, Saffron Vahana for the car, and so on. The agent started simple. For each integration, it: This was surprisingly effective for a v1. Each page was self-contained, worked on mobile, and talked to real hardware. The agent built the lights controller, camera viewer, family map (with Leaflet.js), calendar viewer, and car control panel — all in a single session. Key moment: The car control panel needed a PIN modal for security-sensitive commands. The agent's first attempt used inline onclick handlers that had scope issues. It debugged this by switching to programmatic event listeners — a classic web dev lesson, discovered and fixed autonomously. The standalone HTML pages worked, but they were getting unwieldy. Each page had its own copy of navigation links, styles were inconsistent, and adding features meant touching N files. The agent proposed a modernization: The entire migration — all 10 screens — was done in a single session. The agent: This is where it got real. The cutover had four sequential bugs: Blank page — Vite was built with base: '/new/' (the staging path) instead of '/'. Assets referenced /new/assets/... which didn't exist at the root. Nav links redirecting to /new — React Router routes were still prefixed with /new/. Nav links hardcoded per page — Some pages had inline nav arrays instead of importing from a central registry. Straggler pages — Two pages still had inline navLinks={[...]} in JSX that the initial fix script missed. Each bug was caught, diagnosed, and fixed by the agent within minutes. The key lesson it documented: "Cutover needs THREE things updated together: (1) Vite base, (2) React Router paths, (3) server serving logic." This is exactly the kind of lesson a human developer learns once and remembers. The agent wrote it down in its memory files so it would know for next time. Full stack: browser → Nginx → Node.js + React SPA → scripts → hardware The agent implemented an in-memory cache (Map) with per-endpoint TTLs: Mutation endpoints (POST) automatically invalidate their related cache entries. Simple, effective, no Redis needed. The agent initially used execSync for Python script calls. When slow APIs (Life360, Porsche — 3-8 second responses) started blocking the Node.js event loop and causing unrelated page loads to stall for 4-10 seconds, it diagnosed the issue and converted every integration call to async exec/execFile. This is a real production debugging skill. The agent identified the root cause (event loop starvation), understood why it affected unrelated requests, and applied the systematic fix. All of this was proposed and implemented by the agent, not requested. It established design principles unprompted: These aren't just rules — they reflect aesthetic judgment about information density and mobile UX. After the build, it had created: architecture docs, API contracts, a parity checklist, performance notes, troubleshooting guides, and deployment runbooks. Not because I asked, but because it knew future sessions of itself would need them (the agent wakes fresh each session — its files are its memory). The cutover bugs were fixed, but more importantly, they were documented as lessons. The agent maintains a lessons file that it reviews at the start of each session. It's building institutional knowledge across sessions, entirely self-directed. For integrations, the agent chose to wrap existing Python scripts and CLI tools rather than rewrite everything in Node. Life360 has a Python library? Use it. Netatmo has a Python auth flow? Wrap it. This pragmatism — using the right tool for each job — is something junior developers often struggle with. The most interesting thing isn't that an AI built a web app. It's how it built it: This felt less like "AI-generated code" and more like pair programming with a capable junior who works really fast and never gets tired. OpenClaw is open source. It runs on a Raspberry Pi, connects to WhatsApp/Telegram/Discord, and has access to shell, filesystem, browser, and more. The agent framework handles memory, skills, sub-agents, and cron jobs. The Palace dashboard isn't open-sourced (it's too specific to my setup), but the pattern is reproducible: give an agent access to your APIs, point it at your hardware, and let it build. Just maybe double-check the Vite base path before cutover. 😄 Built on a Raspberry Pi in two sessions. Zero lines of code written by a human. Every bug fixed by the agent that caused it. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
┌─────────────────────────────────────────────┐
│ 🏛️ MANDAKINI PALACE │
│ Mukhya Mantapa │
├──────────────────────┬──────────────────────┤
│ 💡 Prakasha Nivasa │ 🚗 Saffron Vahana │
│ 3 lights on │ 🔋 85% · 289km · 🔒 │
├──────────────────────┼──────────────────────┤
│ 📹 Netra Darpana │ 🔊 Shravana Darpana │
│ 3 cams · 2 events │ 4 speakers found │
├──────────────────────┼──────────────────────┤
│ 👨👩👧 Kutumba Darpana │ 📅 Panchanga Darpana│
│ ● All members home │ 3 events today │
├──────────────────────┼──────────────────────┤
│ ⚡ Gati Darpana │ 🖥️ Pi-Yantra │
│ ↓245 ↑48 Mbps │ CPU 12% · 48°C │
└──────────────────────┴──────────────────────┘ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
┌─────────────────────────────────────────────┐
│ 🏛️ MANDAKINI PALACE │
│ Mukhya Mantapa │
├──────────────────────┬──────────────────────┤
│ 💡 Prakasha Nivasa │ 🚗 Saffron Vahana │
│ 3 lights on │ 🔋 85% · 289km · 🔒 │
├──────────────────────┼──────────────────────┤
│ 📹 Netra Darpana │ 🔊 Shravana Darpana │
│ 3 cams · 2 events │ 4 speakers found │
├──────────────────────┼──────────────────────┤
│ 👨👩👧 Kutumba Darpana │ 📅 Panchanga Darpana│
│ ● All members home │ 3 events today │
├──────────────────────┼──────────────────────┤
│ ⚡ Gati Darpana │ 🖥️ Pi-Yantra │
│ ↓245 ↑48 Mbps │ CPU 12% · 48°C │
└──────────────────────┴──────────────────────┘ CODE_BLOCK:
┌─────────────────────────────────────────────┐
│ 🏛️ MANDAKINI PALACE │
│ Mukhya Mantapa │
├──────────────────────┬──────────────────────┤
│ 💡 Prakasha Nivasa │ 🚗 Saffron Vahana │
│ 3 lights on │ 🔋 85% · 289km · 🔒 │
├──────────────────────┼──────────────────────┤
│ 📹 Netra Darpana │ 🔊 Shravana Darpana │
│ 3 cams · 2 events │ 4 speakers found │
├──────────────────────┼──────────────────────┤
│ 👨👩👧 Kutumba Darpana │ 📅 Panchanga Darpana│
│ ● All members home │ 3 events today │
├──────────────────────┼──────────────────────┤
│ ⚡ Gati Darpana │ 🖥️ Pi-Yantra │
│ ↓245 ↑48 Mbps │ CPU 12% · 48°C │
└──────────────────────┴──────────────────────┘ CODE_BLOCK:
DAY 1 AM ─── 🏗️ Phase 1: HTML Foundation ─────────────────── │ 6 pages · vanilla JS · Leaflet.js · mobile-ready │ DAY 2 AM ─── ⚛️ Phase 2: React Migration ─────────────────── │ React+Vite+TS · 10 screens · shared components │ DAY 2 PM ─── 🧪 Phase 3: Testing & QA ────────────────────── │ 16 contract · 25 E2E · 11 smoke checks │ DAY 2 EVE ── 🚀 Phase 4: Cutover ─────────────────────────── 4 bugs found & fixed in ~15 min · 3 lessons logged ┌────────────┬────────────┬────────────┬────────────┐ │ 2 sessions │ 10 screens │ 0 human LOC│ 52 tests │ └────────────┴────────────┴────────────┴────────────┘ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
DAY 1 AM ─── 🏗️ Phase 1: HTML Foundation ─────────────────── │ 6 pages · vanilla JS · Leaflet.js · mobile-ready │ DAY 2 AM ─── ⚛️ Phase 2: React Migration ─────────────────── │ React+Vite+TS · 10 screens · shared components │ DAY 2 PM ─── 🧪 Phase 3: Testing & QA ────────────────────── │ 16 contract · 25 E2E · 11 smoke checks │ DAY 2 EVE ── 🚀 Phase 4: Cutover ─────────────────────────── 4 bugs found & fixed in ~15 min · 3 lessons logged ┌────────────┬────────────┬────────────┬────────────┐ │ 2 sessions │ 10 screens │ 0 human LOC│ 52 tests │ └────────────┴────────────┴────────────┴────────────┘ CODE_BLOCK:
DAY 1 AM ─── 🏗️ Phase 1: HTML Foundation ─────────────────── │ 6 pages · vanilla JS · Leaflet.js · mobile-ready │ DAY 2 AM ─── ⚛️ Phase 2: React Migration ─────────────────── │ React+Vite+TS · 10 screens · shared components │ DAY 2 PM ─── 🧪 Phase 3: Testing & QA ────────────────────── │ 16 contract · 25 E2E · 11 smoke checks │ DAY 2 EVE ── 🚀 Phase 4: Cutover ─────────────────────────── 4 bugs found & fixed in ~15 min · 3 lessons logged ┌────────────┬────────────┬────────────┬────────────┐ │ 2 sessions │ 10 screens │ 0 human LOC│ 52 tests │ └────────────┴────────────┴────────────┴────────────┘ CODE_BLOCK:
┌─────────────────┐ │ 📱 Browser │ │ (React SPA) │ └────────┬────────┘ │ HTTPS ┌────────▼────────┐ │ 🔒 Nginx │ │ TLS + Gzip │ └────────┬────────┘ │ ┌──────────────▼──────────────┐ │ 🟢 Node.js server │ │ ┌────────┐ ┌───────────┐ │ │ │React │ │ API routes│ │ │ │SPA │ │ + cache │ │ │ │(dist/) │ │ (Map) │ │ │ └────────┘ └─────┬─────┘ │ └────────────────────┼────────┘ │ async exec ┌──────────────▼──────────────┐ │ Integration Scripts │ │ 🐍 Python 🔧 Shell 📡 HTTP │ └──────────────┬──────────────┘ │ ┌──────┬───────┬───────┬───▼───┬────────┬────────┐ │💡 │🔊 │📹 │🚗 │📍 │📅 │ │Z-Wave│Cast │Cameras│Car API│Life360 │CalDAV │ └──────┴───────┴───────┴───────┴────────┴────────┘ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
┌─────────────────┐ │ 📱 Browser │ │ (React SPA) │ └────────┬────────┘ │ HTTPS ┌────────▼────────┐ │ 🔒 Nginx │ │ TLS + Gzip │ └────────┬────────┘ │ ┌──────────────▼──────────────┐ │ 🟢 Node.js server │ │ ┌────────┐ ┌───────────┐ │ │ │React │ │ API routes│ │ │ │SPA │ │ + cache │ │ │ │(dist/) │ │ (Map) │ │ │ └────────┘ └─────┬─────┘ │ └────────────────────┼────────┘ │ async exec ┌──────────────▼──────────────┐ │ Integration Scripts │ │ 🐍 Python 🔧 Shell 📡 HTTP │ └──────────────┬──────────────┘ │ ┌──────┬───────┬───────┬───▼───┬────────┬────────┐ │💡 │🔊 │📹 │🚗 │📍 │📅 │ │Z-Wave│Cast │Cameras│Car API│Life360 │CalDAV │ └──────┴───────┴───────┴───────┴────────┴────────┘ CODE_BLOCK:
┌─────────────────┐ │ 📱 Browser │ │ (React SPA) │ └────────┬────────┘ │ HTTPS ┌────────▼────────┐ │ 🔒 Nginx │ │ TLS + Gzip │ └────────┬────────┘ │ ┌──────────────▼──────────────┐ │ 🟢 Node.js server │ │ ┌────────┐ ┌───────────┐ │ │ │React │ │ API routes│ │ │ │SPA │ │ + cache │ │ │ │(dist/) │ │ (Map) │ │ │ └────────┘ └─────┬─────┘ │ └────────────────────┼────────┘ │ async exec ┌──────────────▼──────────────┐ │ Integration Scripts │ │ 🐍 Python 🔧 Shell 📡 HTTP │ └──────────────┬──────────────┘ │ ┌──────┬───────┬───────┬───▼───┬────────┬────────┐ │💡 │🔊 │📹 │🚗 │📍 │📅 │ │Z-Wave│Cast │Cameras│Car API│Life360 │CalDAV │ └──────┴───────┴───────┴───────┴────────┴────────┘ CODE_BLOCK:
📱 Request → 🗄️ Cache Check → 🐍 Script (async) → ☁️ API → ✅ Response ┌─────────────────────────┐ ┌─────────────────────────┐ │ ✅ CACHE HIT │ │ 🔄 CACHE MISS │ │ → Entry exists + fresh │ │ → No entry / expired │ │ → Return cached (<1ms) │ │ → Spawn async script │ │ → No script spawned │ │ → Store in Map + TTL │ └─────────────────────────┘ └─────────────────────────┘ ┌─────────────────────────┐ ┌─────────────────────────┐ │ 💥 MUTATION INVALIDATE │ │ 🔀 WHY ASYNC MATTERS │ │ → POST /api/lights/tog │ │ Before: execSync │ │ → Execute on hardware │ │ → Slow API blocks ALL │ │ → Delete cache entry │ │ After: async exec │ │ → Next GET = fresh │ │ → Only that req waits │ └─────────────────────────┘ └─────────────────────────┘ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
📱 Request → 🗄️ Cache Check → 🐍 Script (async) → ☁️ API → ✅ Response ┌─────────────────────────┐ ┌─────────────────────────┐ │ ✅ CACHE HIT │ │ 🔄 CACHE MISS │ │ → Entry exists + fresh │ │ → No entry / expired │ │ → Return cached (<1ms) │ │ → Spawn async script │ │ → No script spawned │ │ → Store in Map + TTL │ └─────────────────────────┘ └─────────────────────────┘ ┌─────────────────────────┐ ┌─────────────────────────┐ │ 💥 MUTATION INVALIDATE │ │ 🔀 WHY ASYNC MATTERS │ │ → POST /api/lights/tog │ │ Before: execSync │ │ → Execute on hardware │ │ → Slow API blocks ALL │ │ → Delete cache entry │ │ After: async exec │ │ → Next GET = fresh │ │ → Only that req waits │ └─────────────────────────┘ └─────────────────────────┘ CODE_BLOCK:
📱 Request → 🗄️ Cache Check → 🐍 Script (async) → ☁️ API → ✅ Response ┌─────────────────────────┐ ┌─────────────────────────┐ │ ✅ CACHE HIT │ │ 🔄 CACHE MISS │ │ → Entry exists + fresh │ │ → No entry / expired │ │ → Return cached (<1ms) │ │ → Spawn async script │ │ → No script spawned │ │ → Store in Map + TTL │ └─────────────────────────┘ └─────────────────────────┘ ┌─────────────────────────┐ ┌─────────────────────────┐ │ 💥 MUTATION INVALIDATE │ │ 🔀 WHY ASYNC MATTERS │ │ → POST /api/lights/tog │ │ Before: execSync │ │ → Execute on hardware │ │ → Slow API blocks ALL │ │ → Delete cache entry │ │ After: async exec │ │ → Next GET = fresh │ │ → Only that req waits │ └─────────────────────────┘ └─────────────────────────┘ - 🔌 Smart lights — Vera Z-Wave hub (toggle, dim, scenes per room)
- 🔊 Google Home speakers — Cast audio, TTS announcements, volume control
- 🚗 Electric car — Status, preconditioning, flash/honk, trip analytics (via manufacturer API)
- 📹 Security cameras — Live snapshots, motion events with filtering
- 👨👩👧 Family locations — Real-time map with Life360 integration
- 📅 Calendar — iCloud CalDAV 7-day view
- ⚡ Internet speed — On-demand speedtest
- 🖥️ Pi health — CPU, memory, temperature, disk, uptime - Created a standalone HTML page with inline CSS and vanilla JS
- Added the corresponding API routes in server.js
- Updated Nginx routing
- Added it to the home launcher - React + Vite + TypeScript foundation
- Shared component library (nav bar, status cards, loading states)
- Route-level code splitting with React.lazy()
- Typed API client modules with consistent error handling
- Vendor chunk splitting (React, Leaflet, Marked as separate bundles) - Wrote an architectural spec and parity checklist
- Built the React foundation and shared primitives
- Migrated each screen, preserving all functionality
- Added API contract tests (16 tests) and E2E tests (25 tests)
- Created a pre-deploy smoke test script
- Executed the cutover - Blank page — Vite was built with base: '/new/' (the staging path) instead of '/'. Assets referenced /new/assets/... which didn't exist at the root.
- Nav links redirecting to /new — React Router routes were still prefixed with /new/.
- Nav links hardcoded per page — Some pages had inline nav arrays instead of importing from a central registry.
- Straggler pages — Two pages still had inline navLinks={[...]} in JSX that the initial fix script missed. - Nginx gzip for all text MIME types
- Immutable cache headers for Vite-fingerprinted assets (1 year)
- Code splitting — users only download code for the page they visit
- Vendor chunks — React/Leaflet/Marked cached separately from app code - "Icons speak, skip redundant labels" — 🔋 85% not Battery: 85 percent
- "CSS Grid handles responsive, not JavaScript conditional rendering"
- "No horizontal scroll — grid layout preferred over flex scroll on mobile"
- "Compact cards — padding 15px/12px, gap 12px" - Life360 API instability — Third-party API, regularly breaks with 403s when they rotate client tokens. The agent can update the user-agent string as a fix, but it's a cat-and-mouse game.
- Netatmo live streaming — Their REST API only provides event-based snapshots, not live video. The agent discovered this limitation and adapted rather than fighting it.
- The monolithic server.js — The agent flagged this as tech debt in its own architecture doc. It knows it's not ideal but made a pragmatic choice for a home project. - Incrementally — HTML pages first, React later. Ship something that works, then improve.
- With real debugging — Not just generating code, but diagnosing production issues (event loop starvation, scope bugs, API failures).
- With self-awareness — Documenting its own limitations, flagging tech debt, writing lessons for its future self.
- With taste — Making design decisions that prioritize usability over feature completeness.
how-totutorialguidedev.toaimlservershellcronroutingrouterswitchnginxnodepython