Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 2 additions & 18 deletions dashboard/src/dashboard/BotCard.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState } from 'react';
import type { Bot, BotStatus } from '../types';
import type { Bot } from '../types';
import { StatusLight } from '../ui/StatusLight';
import { Button } from '../ui/Button';
import { Badge } from '../ui/Badge';
import { Panel } from '../ui/Panel';
import { TokenDisplay } from '../ui/TokenDisplay';
import { BotLink } from '../ui/BotLink';
import { getEffectiveStatus } from '../utils/bot-status';
import './BotCard.css';

interface BotCardProps {
Expand All @@ -16,23 +17,6 @@ interface BotCardProps {
loading: boolean;
}

function getEffectiveStatus(bot: Bot): BotStatus {
const containerState = bot.container_status?.state;
if (containerState === 'running') {
// Check if recently started (within 8 seconds)
const startedAt = bot.container_status?.startedAt;
if (startedAt) {
const elapsed = Date.now() - new Date(startedAt).getTime();
if (elapsed < 8000) return 'starting';
}
return 'running';
}
if (containerState === 'exited' || containerState === 'dead') {
return bot.container_status?.exitCode === 0 ? 'stopped' : 'error';
}
return bot.status;
}

function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString(undefined, {
month: 'short',
Expand Down
18 changes: 1 addition & 17 deletions dashboard/src/dashboard/DashboardTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo } from 'react';
import type { Bot, BotStatus } from '../types';
import { StatusSection } from './StatusSection';
import { EmptyState } from './EmptyState';
import { getEffectiveStatus } from '../utils/bot-status';
import './DashboardTab.css';

interface DashboardTabProps {
Expand All @@ -15,23 +16,6 @@ interface DashboardTabProps {
error: string;
}

function getEffectiveStatus(bot: Bot): BotStatus {
const containerState = bot.container_status?.state;
if (containerState === 'running') {
// Check if recently started (within 8 seconds)
const startedAt = bot.container_status?.startedAt;
if (startedAt) {
const elapsed = Date.now() - new Date(startedAt).getTime();
if (elapsed < 8000) return 'starting';
}
return 'running';
}
if (containerState === 'exited' || containerState === 'dead') {
return bot.container_status?.exitCode === 0 ? 'stopped' : 'error';
}
return bot.status;
}

export function DashboardTab({
bots,
onStart,
Expand Down
3 changes: 3 additions & 0 deletions dashboard/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
export type BotStatus = 'created' | 'starting' | 'running' | 'stopped' | 'error';

export type HealthStatus = 'none' | 'starting' | 'healthy' | 'unhealthy';

export interface ContainerStatus {
id: string;
state: string;
running: boolean;
exitCode: number;
startedAt: string;
finishedAt: string;
health: HealthStatus;
}

export interface Bot {
Expand Down
34 changes: 34 additions & 0 deletions dashboard/src/utils/bot-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Bot, BotStatus } from '../types';

/**
* Derives the effective bot status from database status and live container state.
*
* Uses Docker health check status to determine if a running container is ready:
* - health='starting' → bot is initializing
* - health='healthy' → bot is ready
* - health='unhealthy' → bot has failed health checks
* - health='none' → legacy container without health check (treat as running)
*/
export function getEffectiveStatus(bot: Bot): BotStatus {
const containerState = bot.container_status?.state;

if (containerState === 'running') {
// Use Docker health check status to determine if bot is ready
const health = bot.container_status?.health;
if (health === 'starting') {
return 'starting';
}
if (health === 'unhealthy') {
return 'error';
}
// 'healthy' or 'none' (no healthcheck configured) = running
return 'running';
}

if (containerState === 'exited' || containerState === 'dead') {
return bot.container_status?.exitCode === 0 ? 'stopped' : 'error';
}

// Fallback to database status
return bot.status;
}
Comment on lines +12 to +34
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This utility function lacks test coverage. The codebase includes test files (e.g., dashboard/src/api.test.ts), indicating that utility functions should be tested. Consider adding tests for getEffectiveStatus to cover different health status combinations and edge cases, such as when health is 'starting', 'healthy', 'unhealthy', 'none', when container_status is null or undefined, and various container states.

Copilot uses AI. Check for mistakes.
17 changes: 16 additions & 1 deletion src/services/DockerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ export class DockerService {
[LABEL_BOT_ID]: botId,
[LABEL_BOT_HOSTNAME]: hostname
},
Healthcheck: {
Test: ['CMD', 'curl', '-sf', `http://localhost:${config.port}/`],
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The health check assumes the OpenClaw gateway responds to HTTP requests at the root path. Consider verifying that OpenClaw exposes a health endpoint or root endpoint that can be used for health checks. If OpenClaw doesn't respond to root path requests, the health check will fail even for healthy containers. You may need to use a specific health endpoint path (e.g., /health) or use a different health check method like checking if the port is listening using curl -f http://localhost:${config.port} without expecting specific content, or consider using CMD-SHELL with a simpler command like nc -z localhost ${config.port}.

Suggested change
Test: ['CMD', 'curl', '-sf', `http://localhost:${config.port}/`],
Test: ['CMD-SHELL', `nc -z localhost ${config.port}`],

Copilot uses AI. Check for mistakes.
Interval: 2_000_000_000, // 2s in nanoseconds
Timeout: 3_000_000_000, // 3s in nanoseconds
Retries: 30,
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With 30 retries at 2-second intervals, the container will be marked as unhealthy after 60 seconds of consecutive failures. This is a very high retry count. For context, the botmaker and keyring-proxy containers in docker-compose.yml use only 3 retries. Consider whether 30 retries is appropriate for bot containers, or if this should be reduced to a more typical value like 3-5 retries to detect issues faster.

Suggested change
Retries: 30,
Retries: 5,

Copilot uses AI. Check for mistakes.
StartPeriod: 5_000_000_000, // 5s in nanoseconds
},
HostConfig: {
Binds: [
`${config.hostSecretsPath}:/run/secrets:ro`,
Expand Down Expand Up @@ -182,13 +189,21 @@ export class DockerService {
const container = this.docker.getContainer(containerName);
const info = await container.inspect();

// Extract health status from Docker's Health field
const healthState = info.State.Health?.Status;
let health: ContainerStatus['health'] = 'none';
if (healthState === 'starting') health = 'starting';
else if (healthState === 'healthy') health = 'healthy';
else if (healthState === 'unhealthy') health = 'unhealthy';

return {
id: info.Id,
state: info.State.Status as ContainerStatus['state'],
running: info.State.Running,
exitCode: info.State.ExitCode,
startedAt: info.State.StartedAt,
finishedAt: info.State.FinishedAt
finishedAt: info.State.FinishedAt,
health
};
} catch (err) {
const dockerErr = err as { statusCode?: number };
Expand Down
4 changes: 4 additions & 0 deletions src/types/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export type ContainerState =
| 'removing'
| 'dead';

/** Docker health check status */
export type HealthStatus = 'none' | 'starting' | 'healthy' | 'unhealthy';

/** Container status from Docker inspect */
export interface ContainerStatus {
id: string;
Expand All @@ -22,6 +25,7 @@ export interface ContainerStatus {
exitCode: number;
startedAt: string;
finishedAt: string;
health: HealthStatus;
}

/** Container info from Docker list (human-readable) */
Expand Down
Loading