This document provides essential information for AI coding agents working in the Alfred Mission Control codebase.
# Development server (with network access)
npm run dev
# Backend/API tests
npm run test:backend
# Full Vitest suite
npm run test:run
# E2E suite (requires app on 127.0.0.1:3000 and E2E_ADMIN_PASSWORD)
E2E_ADMIN_PASSWORD=your_password npm run test:e2e
# Production build
npm run build
# Production server
npm start
# Lint check (runs ESLint)
npm run lint
# Lint specific files
npm run lint -- src/lib/pricing.ts
# Type check (via tsc)
npx tsc --noEmitNote: This project ships with a Vitest suite plus Playwright E2E coverage. Keep the app running on
127.0.0.1:3000for E2E, passE2E_ADMIN_PASSWORD, and treat Chromium console errors as regressions.
npm run test:backendexecutes the whole backend suite wired inpackage.json; to run a single file, usenpx vitest run <path>instead.- Do not mock
@/lib/auth-helpersglobally invitest.setup.ts; keep auth mocks local to the test file so401/unauthorized assertions still exercise the intended code path. - SQLite-backed test modules (
activities-db,kanban-db,pipeline-db) should detect tests withprocess.env.VITEST === "true"in addition toNODE_ENV === "test", and use in-memory databases during Vitest runs. src/lib/activity-logger.tsis a legacy compatibility wrapper overactivities-db; avoid collapsing it into a raw re-export without checking historical consumers and tests.
Alfred Mission Control is a real-time dashboard for OpenClaw AI agent instances. It reads directly from the OpenClaw installation (config, sessions, memory, logs) without requiring a separate database.
Tech Stack:
- Framework: Next.js 16 (App Router)
- UI: React 19 + Tailwind CSS v4
- 3D Graphics: React Three Fiber + Drei + Rapier
- Charts: Recharts
- Icons: Lucide React
- Database: SQLite (better-sqlite3) for usage tracking
- Runtime: Node.js 24.x (keep build/runtime aligned because
better-sqlite3is native)
IMPORTANT: This project has a dedicated UI style guide skill. Load it when working on UI components:
skill: alfred-mission-control-style
| Category | Canonical Pattern | Component |
|---|---|---|
| Confirmations | <ConfirmDialog /> |
src/components/ConfirmDialog.tsx |
| Toasts | useToast() hook |
src/components/Toast.tsx |
| Loading | <Loader2 className="animate-spin" /> |
lucide-react |
- NEVER use
window.confirm()orwindow.alert()— useConfirmDialogoruseToast() - Use CSS variables (
var(--card),var(--text-primary), etc.) — never hardcoded colors - Tailwind for layout, inline styles for CSS vars — don't duplicate
- All text must be internationalized via
useI18n() - No
neutral-*classes in dashboard — use CSS variables
src/
├── app/
│ ├── (dashboard)/ # Protected dashboard pages
│ │ └── settings/ # Settings page with System, Config, Pricing tabs
│ ├── api/ # API routes (all require auth except /api/auth/*, /api/health)
│ │ ├── pricing/ # Model pricing configuration (GET/PUT/DELETE)
│ │ ├── costs/ # Usage costs and budget management
│ │ └── ... # Other API endpoints
│ ├── login/ # Login page
│ └── office/ # 3D office (unprotected)
├── components/
│ ├── Alfred Mission Control/ # OS-style UI shell (topbar, dock, status bar)
│ ├── Office3D/ # React Three Fiber 3D components
│ ├── charts/ # Recharts wrappers
│ ├── ConfirmDialog.tsx# Reusable confirmation dialog
│ ├── Toast.tsx # Toast notification system with useToast()
│ ├── ConfigEditor.tsx # Editor for openclaw.json configuration
│ ├── PricingEditor.tsx# Editor for model pricing overrides
│ └── *.tsx # Other feature components
├── config/
│ └── branding.ts # Branding constants (reads from env vars)
├── hooks/ # Custom React hooks
├── i18n/ # Internationalization (en, es)
├── lib/
│ ├── pricing.ts # Model pricing calculation and configuration
│ ├── usage-collector.ts # Usage data collection
│ └── ... # Other utilities
└── middleware.ts # Auth guard for all routes
data/ # JSON data files (gitignored)
├── model-pricing.json # User-configured model price overrides
├── budget-settings.json # Monthly budget configuration
└── usage-tracking.db # SQLite database for usage history
scripts/ # Setup and data collection scripts
public/models/ # GLB avatar models
This project supports English (en) and Spanish (es) via a custom i18n system. ALL user-visible text MUST be internationalized.
- Provider:
I18nProviderwraps the dashboard layout - Hook:
useI18n()provides translation functions - Messages: JSON files in
src/i18n/messages/(en.json,es.json) - Detection: Auto-detects locale from localStorage, cookie, or browser settings
import { useI18n } from "@/i18n/provider";
export function MyComponent() {
const { t, locale, setLocale, formatNumber, formatDateTime } = useI18n();
return (
<div>
{/* Simple translation */}
<h1>{t("dashboard.title")}</h1>
{/* With interpolation */}
<p>{t("common.showing", { count: 5, total: 100 })}</p>
{/* Number formatting */}
<span>{formatNumber(1234567)}</span>
{/* Date/time formatting */}
<time>{formatDateTime(new Date())}</time>
</div>
);
}-
Add the key to BOTH
en.jsonandes.json:// en.json "myFeature": { "title": "My Feature", "description": "This is {name}'s feature" } // es.json "myFeature": { "title": "Mi Feature", "description": "Este es el feature de {name}" }
-
Use it in code:
t("myFeature.title")ort("myFeature.description", { name: "John" })
- NEVER hardcode user-visible strings in components
- ALWAYS add translations to both
en.jsonandes.json - Use dot notation for nested keys:
"section.subkey" - Use interpolation for dynamic values:
{count},{name}, etc. - API error messages should also be internationalized when shown to users
Order imports in this sequence, separated by blank lines:
- Node.js built-ins (
fs,path,crypto, etc.) - External packages (
next,react,lucide-react, etc.) - Internal aliases (
@/components/...,@/lib/...)
// Node.js built-ins
import fs from 'fs';
import path from 'path';
import { randomUUID } from 'crypto';
// External packages
import { NextResponse } from "next/server";
import { useEffect, useState } from "react";
import { Activity, CheckCircle } from "lucide-react";
// Internal imports with @ alias
import { StatsCard } from "@/components/StatsCard";
import { BRANDING } from "@/config/branding";
import { logActivity } from "@/lib/activity-logger";- Use double quotes for all strings
- Use template literals for interpolation
const configPath = openclawDir + "/openclaw.json";
const message = `Unknown model: ${modelId}`;- Strict mode is enabled — all code must type-check
- Define interfaces for all object shapes, especially props and API responses
- Export interfaces when they're reused across files
- Use
constassertions for constant objects
// Interface for component props
interface StatsCardProps {
title: string;
value: string | number;
icon: ReactNode;
iconColor?: string;
}
// Exported type for reuse
export type ActivityStatus = 'success' | 'error' | 'pending';
// Const assertion for config
export const BRANDING = {
agentName: "...",
agentEmoji: "...",
} as const;- Add
"use client";directive at the very top of client components - Use named exports for components
- Destructure props in the function signature
"use client";
import { ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
onClick?: () => void;
variant?: "primary" | "secondary";
}
export function Button({ children, onClick, variant = "primary" }: ButtonProps) {
return (
<button onClick={onClick} className={...}>
{children}
</button>
);
}- Export
export const dynamic = "force-dynamic";for dynamic routes - Always wrap route handlers in try/catch
- Return consistent JSON error responses
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET() {
try {
const data = await fetchData();
return NextResponse.json({ data });
} catch (error) {
console.error("Error description:", error);
return NextResponse.json(
{ error: "Human-readable error message" },
{ status: 500 }
);
}
}- Use Tailwind CSS classes for most styling
- Use CSS custom properties (defined in
globals.css) for theme colors - Use inline
styleprop for dynamic values (colors from data, etc.)
// Tailwind for layout/spacing
<div className="rounded-xl p-4 md:p-6 grid grid-cols-2 gap-3">
// CSS custom properties for theme
<div style={{ backgroundColor: 'var(--card)', border: '1px solid var(--border)' }}>
// Inline styles for dynamic values
<div style={{ border: `2px solid ${agent.color}` }}>Available CSS variables:
--card,--card-elevated,--border--text-primary,--text-secondary,--text-muted--accent,--success,--error,--info--font-heading,--font-body
- Use try/catch in async operations
- Log errors with
console.error()and a descriptive message - Return user-friendly error messages (not raw error objects)
- Handle missing files gracefully (return empty arrays, default values)
try {
const data = fs.readFileSync(DATA_PATH, 'utf-8');
return JSON.parse(data);
} catch {
// File doesn't exist or is invalid — return safe default
return [];
}| Element | Convention | Example |
|---|---|---|
| Components | PascalCase | StatsCard, ActivityFeed |
| Functions | camelCase | logActivity, calculateCost |
| Constants | SCREAMING_SNAKE_CASE | OPENCLAW_DIR, MODEL_PRICING |
| Interfaces | PascalCase, descriptive | ActivityMetadata, AgentConfig |
| Type aliases | PascalCase | ActivityType, ActivityStatus |
| Files | PascalCase for components | StatsCard.tsx, pricing.ts |
- Use JSDoc for exported functions with parameters/return values
- Use inline comments for non-obvious logic
- Keep comments concise and explain "why", not "what"
/**
* Calculate cost for a given model and token usage
* @param modelId - Model identifier or alias
* @param inputTokens - Number of input tokens
* @param outputTokens - Number of output tokens
* @returns Cost in USD
*/
export function calculateCost(modelId: string, inputTokens: number, outputTokens: number): number {
// ...
}All personal/instance data goes in .env.local (gitignored). Never hardcode:
ADMIN_PASSWORD— Dashboard login passwordAUTH_SECRET— Cookie signing secretOPENCLAW_DIR— Path to OpenClaw installation (default:/root/.openclaw)NEXT_PUBLIC_*— Branding variables (name, emoji, etc.)
Read environment variables via process.env.VAR_NAME or in src/config/branding.ts.
- All routes require authentication via
src/middleware.ts - Only
/login,/api/auth/*, and/api/healthare public - Never commit
.env.localor files indata/ - Terminal API uses a strict command allowlist
- Create
src/app/api/<route>/route.ts - Export
GET,POST, etc. functions - Add
export const dynamic = "force-dynamic"; - All endpoints are protected by middleware automatically
- Create
src/app/(dashboard)/<page>/page.tsx - Use
"use client";if client-side state/effects are needed - Follow the existing page patterns for layout consistency
- Create in
src/components/(or appropriate subdirectory) - Export as named export
- Define props interface above the component
- Import via
@/components/ComponentName
- Add model entry to
MODEL_PRICINGarray insrc/lib/pricing.ts - Include:
id,name,alias(optional),inputPricePerMillion,outputPricePerMillion,contextWindow - Add cache pricing fields if model supports caching:
cacheReadPricePerMillion,cacheWritePricePerMillion - The model will automatically appear in Settings > Pricing tab
Alfred Mission Control uses a contextual help system that explains what each section does and how to use it. All pages include tooltips with explanations accessible via this system.
| Component | Purpose |
|---|---|
HelpTooltip |
Reusable tooltip component with title + description |
PageHeader |
Page header with optional help badge |
| Component | Example |
|---|---|
HelpTooltip |
` |
PageHeader |
` |
-
Add the help key in BOTH
en.jsonandes.json:// en.json "help": { "dashboard": { "title": "Dashboard", "description": "Overview of agent activity, quick stats, and recent events. Your command center for monitoring OpenClaw." }, "agents": { "title": "Agents", "description": "View and manage your AI agents. See hierarchy, communication patterns, and spawn new subagents." }, "office": { "title": "3D Office", "description": "Interactive 3D visualization of your agent workspace. A fun way to see your agents in action." }, "memory": { "title": "Memory Browser", "description": "Explore and edit agent memory files. See what your agents remember across sessions." }, "files": { "title": "File Browser", "description": "Browse agent workspaces and files. Navigate the code your agents are working on." }, "analytics": { "title": "Analytics & Costs", "description": "Track token usage, costs by model, and budget. Keep your AI spending under control." }, "workflows": { "title": "Workflow Designer", "description": "Design visual workflows for multi-agent automation. Chain agents together." }, "terminal": { "title": "Terminal", "description": "Browser-based terminal for quick commands. Read-only for safety (ls, cat, git status, etc.)" }, "system": { "title": "System Monitor", "description": "Real-time server monitoring. CPU, RAM, disk, network, and service status." }, "settings": { "title": "Settings", "description": "Configure OpenClaw, edit model pricing, and manage system settings." } }
-
Use it in code:
t("myFeature.title")ort("myFeature.description", { name: "John" })
- NEVER hardcode user-visible strings in components
- ALWAYS add translations to both
en.jsonandes.json - Use dot notation for nested keys:
"section.subkey" - Use interpolation for dynamic values:
{count},{name}, etc. - API error messages should also be internationalized when shown to users
Alfred Mission Control supports runtime configuration of model prices via the Settings UI.
- Default prices are defined in
src/lib/pricing.tsasMODEL_PRICINGconstant - User overrides are stored in
data/model-pricing.json(gitignored) - Merged pricing combines defaults with overrides at runtime (override wins by
id) - Cost calculations use merged pricing, not hardcoded values
| File | Purpose |
|---|---|
src/lib/pricing.ts |
Default prices, merge logic, cost calculation |
src/app/api/pricing/route.ts |
CRUD API for price overrides |
src/components/PricingEditor.tsx |
Settings UI for editing prices |
data/model-pricing.json |
Runtime price overrides (auto-created) |
// Get user-configured overrides (from JSON file)
getPricingOverrides(): PricingOverride[]
// Get merged pricing (defaults + overrides)
getMergedPricing(): ModelPricingEntry[]
// Calculate cost using merged pricing
calculateCost(modelId, inputTokens, outputTokens, cacheRead?, cacheWrite?): number// PUT /api/pricing
{
"overrides": [
{
"id": "anthropic/claude-opus-4-6",
"inputPricePerMillion": 12.00,
"outputPricePerMillion": 60.00
}
]
}Alfred Mission Control supports heartbeat-driven task polling for OpenClaw agents. This enables agents to autonomously check for new or updated tasks on the Kanban board—similar to how Vikunja or other task queue systems work.
Agents in OpenClaw are typically "set and forget"—they're spawned with tasks but have no built-in mechanism to periodically check for new work. When a task is assigned or updated, the agent doesn't know unless manually triggered or respawned.
Agents configure a heartbeat in openclaw.json that specifies how often they should poll for work. When the heartbeat fires, the agent:
- Calls
GET /api/heartbeat/tasks?agentName=<agent-id> - Receives tasks assigned to it with status
in_progress - Claims unclaimed tasks and processes them
- Updates task status as work progresses
Add to openclaw.json:
{
"agents": {
"defaults": {
"id": "developer",
"name": "Developer Agent",
"heartbeat": {
"every": "15m"
}
}
}
}Supported intervals:
1m— every minute (aggressive, for testing)5m— every 5 minutes15m— every 15 minutes30m— every 30 minutes1h— every hour
Requires agent authentication headers:
X-Agent-IdX-Agent-Key
Returns tasks assigned to the agent with status in_progress:
{
"agentName": "developer",
"count": 2,
"tasks": [
{
"id": "task-123",
"title": "Fix login bug",
"description": "Users can't login with SSO",
"priority": "high",
"status": "in_progress",
"isExecutable": true,
"blockedReason": null,
"claimedBy": "developer",
"claimedAt": "2026-03-07T08:15:00Z"
}
]
}Returns all scheduled tasks (cron + heartbeat):
[
{
"id": "heartbeat",
"name": "Heartbeat",
"type": "heartbeat",
"agentId": "main",
"schedule": "*/15 * * * *",
"scheduleDisplay": "Every 15 min",
"enabled": true,
"description": "Periodic agent self-check"
}
]1. Timer fires (every N minutes)
↓
2. Agent calls GET /api/heartbeat/tasks?agentName=me
(with X-Agent-Id + X-Agent-Key headers)
↓
3. API returns assigned tasks (status=in_progress)
↓
4. For each task:
- Check if already claimed by me
- If not claimed: claim it (PATCH /api/kanban/tasks)
- If already claimed: process it
↓
5. Update task status as work progresses
- PATCH /api/kanban/tasks/{id} { status: "done" }
Every morning at 8:00 AM, the Project Manager agent reviews the Kanban board and assigns three new tasks to the Developer agent. The Developer agent has a heartbeat configured to run every 15 minutes. When it fires, it polls for tasks, finds the new work waiting, and starts processing it—without being respawned.
This enables autonomous agent behavior: agents work like employees checking a task board, picking up new work when it appears.
| File | Purpose |
|---|---|
src/lib/openclaw-agents.ts |
Reads heartbeat config from openclaw.json |
src/lib/kanban-db.ts |
Kanban task storage and queries |
src/app/api/heartbeat/tasks/route.ts |
Returns assigned in-progress tasks |
src/app/api/tasks/route.ts |
Unified cron + heartbeat view |
- Choose appropriate interval: 15-30 minutes is good for most agents
- Claim tasks promptly: Agents should claim tasks quickly to avoid conflicts
- Update status regularly: Mark tasks as done, error, or in-progress as work progresses
- Handle dependencies: Use the dependency resolver to avoid working on blocked tasks
Run these commands to ensure code quality:
npm run lint
npx tsc --noEmit
npm run buildFix all errors before pushing. Warnings are acceptable but should be reviewed.