Mission Control Builder
Build a personal dashboard for OpenClaw with task management, memory browser, calendar, team tracking, and GitHub trends. Use when the user wants to create a...
Description
name: mission-control description: Build a personal dashboard for OpenClaw with task management, memory browser, calendar, team tracking, and GitHub trends. Use when the user wants to create a web-based mission control dashboard for their OpenClaw instance, track tasks, view memories, manage calendar events, or build a custom dashboard interface.
Mission Control Skill
Build your own personal dashboard for OpenClaw - a central command center for tasks, memories, calendar, and team management.
What You'll Build
A Next.js web dashboard that connects to your OpenClaw instance and provides:
- Task Board - Kanban-style task management
- Memory Browser - Search and view your OpenClaw memories
- Calendar View - See scheduled events and cron jobs
- Team Status - Track who's working on what
- GitHub Trends - Discover trending repositories
Architecture Overview
┌─────────────────────────────────────┐
│ Next.js Frontend │
│ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ │ Task │ │ Memory │ │Calendar│ │
│ │ Board │ │ List │ │ View │ │
│ └────┬────┘ └────┬────┘ └───┬────┘ │
│ └───────────┴──────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ API Routes │ │
│ └──────┬──────┘ │
└──────────────┼───────────────────────┘
│
┌───────┴───────┐
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ Local JSON │ │ OpenClaw │
│ Files │ │ Memory │
└─────────────┘ └─────────────┘
Prerequisites
- Node.js 18+
- OpenClaw installed and running
- Basic knowledge of React/Next.js
Step-by-Step Guide
Step 1: Initialize Project
npx create-next-app@latest mission-control --typescript --tailwind
Step 2: Install Dependencies
cd mission-control
npm install lucide-react
Step 3: Create Data Layer
Create src/lib/data.ts for file-based storage:
import fs from "fs/promises";
import path from "path";
const DATA_DIR = path.join(process.cwd(), "src", "data");
// Types
export interface Task {
id: string;
title: string;
description: string;
status: "todo" | "in-progress" | "done";
assignee: string;
createdAt: string;
updatedAt: string;
}
// Initialize data files
async function initDataFile(filename: string, defaultData: unknown) {
const filepath = path.join(DATA_DIR, filename);
try {
await fs.access(filepath);
} catch {
await fs.mkdir(DATA_DIR, { recursive: true });
await fs.writeFile(filepath, JSON.stringify(defaultData, null, 2));
}
return filepath;
}
// Tasks
export async function getTasks(): Promise<Task[]> {
const filepath = await initDataFile("tasks.json", []);
const data = await fs.readFile(filepath, "utf-8");
return JSON.parse(data);
}
export async function addTask(task: Omit<Task, "id" | "createdAt" | "updatedAt">): Promise<Task> {
const tasks = await getTasks();
const newTask: Task = {
...task,
id: Date.now().toString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
tasks.push(newTask);
await fs.writeFile(
path.join(DATA_DIR, "tasks.json"),
JSON.stringify(tasks, null, 2)
);
return newTask;
}
export async function updateTask(id: string, updates: Partial<Task>): Promise<Task | null> {
const tasks = await getTasks();
const index = tasks.findIndex((t) => t.id === id);
if (index === -1) return null;
tasks[index] = {
...tasks[index],
...updates,
updatedAt: new Date().toISOString()
};
await fs.writeFile(
path.join(DATA_DIR, "tasks.json"),
JSON.stringify(tasks, null, 2)
);
return tasks[index];
}
Step 4: Create API Routes
Create src/app/api/tasks/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { getTasks, addTask, updateTask } from "@/lib/data";
export async function GET() {
const tasks = await getTasks();
return NextResponse.json(tasks);
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const task = await addTask(body);
return NextResponse.json(task, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: "Failed to create task" },
{ status: 500 }
);
}
}
export async function PUT(request: NextRequest) {
try {
const { id, ...updates } = await request.json();
const task = await updateTask(id, updates);
if (!task) {
return NextResponse.json({ error: "Task not found" }, { status: 404 });
}
return NextResponse.json(task);
} catch (error) {
return NextResponse.json(
{ error: "Failed to update task" },
{ status: 500 }
);
}
}
Step 5: Create Components
Navigation Component (src/components/Navigation.tsx):
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { LayoutDashboard, ClipboardList, Menu, X } from "lucide-react";
const navItems = [
{ href: "/", label: "Dashboard", icon: LayoutDashboard },
{ href: "/tasks", label: "Tasks", icon: ClipboardList },
];
export function Navigation() {
const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<>
{/* Desktop Sidebar */}
<aside className="hidden lg:flex w-64 bg-gray-800 border-r border-gray-700 flex-col h-screen">
<div className="p-6 border-b border-gray-700">
<h1 className="text-xl font-bold text-blue-400">Mission Control</h1>
</div>
<nav className="flex-1 p-4">
<ul className="space-y-2">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
return (
<li key={item.href}>
<Link
href={item.href}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
isActive
? "bg-blue-600 text-white"
: "text-gray-300 hover:bg-gray-700"
}`}
>
<Icon size={20} />
<span>{item.label}</span>
</Link>
</li>
);
})}
</ul>
</nav>
</aside>
{/* Mobile Header */}
<div className="lg:hidden fixed top-0 left-0 right-0 z-40 bg-gray-800 border-b border-gray-700">
<div className="flex items-center justify-between px-4 py-3">
<h1 className="text-lg font-bold text-blue-400">Mission Control</h1>
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="p-2 rounded-lg bg-gray-700"
>
{mobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
</div>
{/* Mobile Drawer */}
{mobileMenuOpen && (
<aside className="lg:hidden fixed top-[60px] left-0 bottom-0 w-64 bg-gray-800 z-40">
{/* Same nav as desktop */}
</aside>
)}
</>
);
}
Step 6: Create Task Board
"use client";
import { useState, useEffect } from "react";
interface Task {
id: string;
title: string;
description: string;
status: "todo" | "in-progress" | "done";
assignee: string;
}
export function TaskBoard() {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchTasks();
}, []);
const fetchTasks = async () => {
try {
const response = await fetch("/api/tasks");
const data = await response.json();
setTasks(data);
} finally {
setLoading(false);
}
};
const handleUpdateStatus = async (taskId: string, newStatus: Task["status"]) => {
await fetch("/api/tasks", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: taskId, status: newStatus }),
});
fetchTasks();
};
const tasksByStatus = {
todo: tasks.filter((t) => t.status === "todo"),
"in-progress": tasks.filter((t) => t.status === "in-progress"),
done: tasks.filter((t) => t.status === "done"),
};
if (loading) return <div>Loading...</div>;
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{Object.entries(tasksByStatus).map(([status, statusTasks]) => (
<div key={status} className="bg-gray-800/50 rounded-xl p-4">
<h3 className="font-semibold text-gray-300 mb-4 capitalize">{status}</h3>
<div className="space-y-3">
{statusTasks.map((task) => (
<div key={task.id} className="bg-gray-800 p-4 rounded-lg">
<h4 className="font-medium">{task.title}</h4>
<select
value={task.status}
onChange={(e) => handleUpdateStatus(task.id, e.target.value as Task["status"])}
className="mt-2 text-sm bg-gray-700 border border-gray-600 rounded px-2 py-1"
>
<option value="todo">To Do</option>
<option value="in-progress">In Progress</option>
<option value="done">Done</option>
</select>
</div>
))}
</div>
</div>
))}
</div>
);
}
Step 7: OpenClaw Memory Sync
Create src/app/api/sync/route.ts:
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
export async function GET() {
try {
// Read OpenClaw memory files
const memoryDir = path.join(process.env.HOME || "", "clawd", "memory");
const files = await fs.readdir(memoryDir);
const memories = [];
for (const file of files.filter(f => f.endsWith('.md'))) {
const content = await fs.readFile(path.join(memoryDir, file), 'utf-8');
memories.push({
id: file,
title: file.replace('.md', ''),
content: content.slice(0, 500) + '...',
createdAt: new Date().toISOString(),
});
}
return NextResponse.json(memories);
} catch (error) {
return NextResponse.json([]);
}
}
Step 8: Create Layout
import type { Metadata } from "next";
import "./globals.css";
import { Navigation } from "@/components/Navigation";
export const metadata: Metadata = {
title: "Mission Control",
description: "Personal dashboard for OpenClaw",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="bg-gray-900 text-white">
<div className="flex flex-col lg:flex-row min-h-screen">
<Navigation />
<main className="flex-1 overflow-auto pt-[60px] lg:pt-0 p-6">
{children}
</main>
</div>
</body>
</html>
);
}
Step 9: Start Script
Create start.sh:
#!/bin/bash
cd "$(dirname "$0")"
npm run dev
Make it executable:
chmod +x start.sh
Step 10: Run
./start.sh
Open http://localhost:3000
Extending Mission Control
Add GitHub Trends
// src/app/api/github-trends/route.ts
export async function GET() {
const response = await fetch(
"https://api.github.com/search/repositories?q=stars:>1000&sort=stars&per_page=10"
);
const data = await response.json();
return NextResponse.json(data.items);
}
Add Calendar Events
Store events in src/data/calendar.json and create similar API routes.
Add Team Members
Create src/data/team.json with member info and current tasks.
Mobile Responsiveness Tips
- Use Tailwind breakpoints:
lg:for desktop, default for mobile - Touch targets: Minimum 40px for buttons
- Horizontal scroll: For Kanban board on mobile
- Drawer navigation: Slide-in menu for mobile
Security Considerations
- Store personal data in
src/data/(gitignored) - Keep template data in the skill
- No authentication included - add your own if needed
- Run locally or behind a VPN
Troubleshooting
Port already in use?
PORT=3001 ./start.sh
Data not saving?
Ensure src/data/ directory exists and is writable.
OpenClaw sync not working? Check that OpenClaw memory path is correct in your environment.
Resources
- Next.js docs: https://nextjs.org/docs
- Tailwind CSS: https://tailwindcss.com
- Lucide icons: https://lucide.dev
License
MIT - Built for the OpenClaw community 🦞
Reviews (0)
No reviews yet. Be the first to review!
Comments (0)
No comments yet. Be the first to share your thoughts!