diff --git a/dashboard/src/dashboard/BotCard.tsx b/dashboard/src/dashboard/BotCard.tsx index fc458df..93a624c 100644 --- a/dashboard/src/dashboard/BotCard.tsx +++ b/dashboard/src/dashboard/BotCard.tsx @@ -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 { @@ -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', diff --git a/dashboard/src/dashboard/DashboardTab.tsx b/dashboard/src/dashboard/DashboardTab.tsx index 25a94d5..43cbad1 100644 --- a/dashboard/src/dashboard/DashboardTab.tsx +++ b/dashboard/src/dashboard/DashboardTab.tsx @@ -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 { @@ -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, diff --git a/dashboard/src/types.ts b/dashboard/src/types.ts index d0dd984..4deea7f 100644 --- a/dashboard/src/types.ts +++ b/dashboard/src/types.ts @@ -1,5 +1,7 @@ export type BotStatus = 'created' | 'starting' | 'running' | 'stopped' | 'error'; +export type HealthStatus = 'none' | 'starting' | 'healthy' | 'unhealthy'; + export interface ContainerStatus { id: string; state: string; @@ -7,6 +9,7 @@ export interface ContainerStatus { exitCode: number; startedAt: string; finishedAt: string; + health: HealthStatus; } export interface Bot { diff --git a/dashboard/src/utils/bot-status.ts b/dashboard/src/utils/bot-status.ts new file mode 100644 index 0000000..ad99173 --- /dev/null +++ b/dashboard/src/utils/bot-status.ts @@ -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; +} diff --git a/src/services/DockerService.ts b/src/services/DockerService.ts index 193eef8..9711140 100644 --- a/src/services/DockerService.ts +++ b/src/services/DockerService.ts @@ -55,6 +55,13 @@ export class DockerService { [LABEL_BOT_ID]: botId, [LABEL_BOT_HOSTNAME]: hostname }, + Healthcheck: { + Test: ['CMD', 'curl', '-sf', `http://localhost:${config.port}/`], + Interval: 2_000_000_000, // 2s in nanoseconds + Timeout: 3_000_000_000, // 3s in nanoseconds + Retries: 30, + StartPeriod: 5_000_000_000, // 5s in nanoseconds + }, HostConfig: { Binds: [ `${config.hostSecretsPath}:/run/secrets:ro`, @@ -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 }; diff --git a/src/types/container.ts b/src/types/container.ts index bff5ad4..3c44c3c 100644 --- a/src/types/container.ts +++ b/src/types/container.ts @@ -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; @@ -22,6 +25,7 @@ export interface ContainerStatus { exitCode: number; startedAt: string; finishedAt: string; + health: HealthStatus; } /** Container info from Docker list (human-readable) */