cflx 0.6.11

Conflux – a spec-driven parallel coding orchestrator that runs AI agents on git worktrees
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Activity, CheckCircle2, Clock3, RefreshCw, XCircle } from 'lucide-react';

import { fetchStatsOverview } from '../api/restClient';
import { ChangeEventSummary, ProjectStats, StatsOverview } from '../api/types';

function formatDurationMs(durationMs: number | null | undefined): string {
  if (durationMs == null || Number.isNaN(durationMs)) {
    return '-';
  }
  if (durationMs < 1000) {
    return `${Math.round(durationMs)}ms`;
  }
  const seconds = durationMs / 1000;
  if (seconds < 60) {
    return `${seconds.toFixed(1)}s`;
  }
  const minutes = Math.floor(seconds / 60);
  const remainSeconds = Math.round(seconds % 60);
  return `${minutes}m ${remainSeconds}s`;
}

function formatTimestamp(value: string): string {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) {
    return value;
  }
  return date.toLocaleString();
}

function formatPercent(rate: number): string {
  const normalized = Number.isFinite(rate) ? rate : 0;
  const base = normalized > 1 ? normalized : normalized * 100;
  return `${Math.max(0, Math.min(100, base)).toFixed(1)}%`;
}

function getProjectLabel(project: Pick<ProjectStats, 'project_name' | 'project_id'>): string {
  return project.project_name || project.project_id;
}

function getEventProjectLabel(event: Pick<ChangeEventSummary, 'project_name' | 'project_id'>): string {
  return event.project_name || event.project_id;
}

export function OverviewDashboard() {
  const [overview, setOverview] = useState<StatsOverview | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const loadOverview = useCallback(async () => {
    setIsLoading(true);
    setError(null);

    try {
      const data = await fetchStatsOverview();
      setOverview(data);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      console.error('[OverviewDashboard] failed to fetch stats overview', { message, err });
      setError(message);
      setOverview(null);
    } finally {
      setIsLoading(false);
    }
  }, []);

  useEffect(() => {
    void loadOverview();
  }, [loadOverview]);

  const summaryCards = useMemo(() => {
    const summary = overview?.summary;
    return [
      {
        label: 'Success',
        value: summary?.success_count ?? 0,
        icon: CheckCircle2,
        textClass: 'text-[#22c55e]',
        bgClass: 'bg-[#052e16]/40',
      },
      {
        label: 'Failure',
        value: summary?.failure_count ?? 0,
        icon: XCircle,
        textClass: 'text-[#ef4444]',
        bgClass: 'bg-[#450a0a]/40',
      },
      {
        label: 'In Progress',
        value: summary?.in_progress_count ?? 0,
        icon: Activity,
        textClass: 'text-[#f59e0b]',
        bgClass: 'bg-[#451a03]/40',
      },
      {
        label: 'Avg Duration',
        value: formatDurationMs(summary?.average_duration_ms),
        icon: Clock3,
        textClass: 'text-[#a1a1aa]',
        bgClass: 'bg-[#18181b]',
      },
    ];
  }, [overview]);

  return (
    <div className="flex h-full flex-col overflow-hidden bg-[#09090b] p-4 md:p-6">
      <div className="mb-4 flex items-center justify-between gap-3">
        <div>
          <h2 className="text-lg font-semibold text-[#fafafa]">Orchestration Overview</h2>
          <p className="text-xs text-[#71717a]">Global stats across all projects</p>
        </div>
        <button
          onClick={() => void loadOverview()}
          disabled={isLoading}
          className="inline-flex items-center gap-1.5 rounded-md border border-[#27272a] bg-[#111113] px-3 py-1.5 text-xs text-[#d4d4d8] transition-colors hover:bg-[#18181b] disabled:cursor-not-allowed disabled:opacity-50"
        >
          <RefreshCw className={`size-3.5 ${isLoading ? 'animate-spin' : ''}`} />
          Refresh
        </button>
      </div>

      {error && (
        <div className="mb-4 rounded-md border border-[#7f1d1d] bg-[#450a0a]/30 px-3 py-2 text-xs text-[#fca5a5]">
          Failed to load overview: {error}
        </div>
      )}

      <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
        {summaryCards.map((card) => {
          const Icon = card.icon;
          return (
            <div
              key={card.label}
              className="rounded-lg border border-[#27272a] bg-[#111113] p-3"
            >
              <div className="mb-2 flex items-center justify-between">
                <span className="text-xs text-[#71717a]">{card.label}</span>
                <span className={`rounded p-1 ${card.bgClass}`}>
                  <Icon className={`size-3.5 ${card.textClass}`} />
                </span>
              </div>
              <p className="text-lg font-semibold text-[#fafafa]">{card.value}</p>
            </div>
          );
        })}
      </div>

      <div className="mt-4 grid min-h-0 flex-1 grid-cols-1 gap-4 xl:grid-cols-2">
        <section className="flex min-h-0 flex-col rounded-lg border border-[#27272a] bg-[#111113]">
          <header className="border-b border-[#27272a] px-3 py-2">
            <h3 className="text-sm font-medium text-[#fafafa]">Recent Activity</h3>
          </header>
          <div className="min-h-0 flex-1 overflow-y-auto">
            {!overview || !overview.recent_events || overview.recent_events.length === 0 ? (
              <div className="flex h-full items-center justify-center p-4 text-sm text-[#52525b]">
                {isLoading ? 'Loading events...' : 'No recent events'}
              </div>
            ) : (
              <ul className="divide-y divide-[#27272a]">
                {overview.recent_events.map((event, idx) => (
                  <li key={`${event.change_id}-${event.timestamp}-${idx}`} className="px-3 py-2.5">
                    <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
                      <span className="font-medium text-[#e4e4e7]">{getEventProjectLabel(event)}</span>
                      <span className="text-[#52525b]">/</span>
                      <span className="text-[#a1a1aa]">{event.change_id}</span>
                      <span className="rounded bg-[#18181b] px-1.5 py-0.5 text-[#c4b5fd]">
                        {event.operation}
                      </span>
                      <span
                        className={`rounded px-1.5 py-0.5 ${
                          event.result === 'success'
                            ? 'bg-[#052e16]/40 text-[#4ade80]'
                            : event.result === 'failure'
                              ? 'bg-[#450a0a]/40 text-[#f87171]'
                              : 'bg-[#18181b] text-[#a1a1aa]'
                        }`}
                      >
                        {event.result}
                      </span>
                    </div>
                    <p className="mt-1 text-[11px] text-[#71717a]">{formatTimestamp(event.timestamp)}</p>
                  </li>
                ))}
              </ul>
            )}
          </div>
        </section>

        <section className="flex min-h-0 flex-col rounded-lg border border-[#27272a] bg-[#111113]">
          <header className="border-b border-[#27272a] px-3 py-2">
            <h3 className="text-sm font-medium text-[#fafafa]">Project Stats</h3>
          </header>
          <div className="min-h-0 flex-1 overflow-y-auto p-3">
            {!overview || !overview.project_stats || overview.project_stats.length === 0 ? (
              <div className="flex h-full items-center justify-center text-sm text-[#52525b]">
                {isLoading ? 'Loading project stats...' : 'No project stats'}
              </div>
            ) : (
              <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                {overview.project_stats.map((stats) => (
                  <article
                    key={stats.project_id}
                    className="rounded-md border border-[#27272a] bg-[#09090b] p-3"
                  >
                    <h4 className="truncate text-sm font-medium text-[#e4e4e7]">
                      {getProjectLabel(stats)}
                    </h4>
                    <dl className="mt-2 space-y-1.5 text-xs">
                      <div className="flex items-center justify-between gap-2">
                        <dt className="text-[#71717a]">Apply Success</dt>
                        <dd className="font-medium text-[#22c55e]">{formatPercent(stats.apply_success_rate)}</dd>
                      </div>
                      <div className="flex items-center justify-between gap-2">
                        <dt className="text-[#71717a]">Avg Duration</dt>
                        <dd className="text-[#d4d4d8]">{formatDurationMs(stats.average_duration_ms)}</dd>
                      </div>
                      <div className="flex items-center justify-between gap-2">
                        <dt className="text-[#71717a]">Success / Failure / In Progress</dt>
                        <dd className="text-[#d4d4d8]">
                          {stats.success_count} / {stats.failure_count} / {stats.in_progress_count}
                        </dd>
                      </div>
                    </dl>
                  </article>
                ))}
              </div>
            )}
          </div>
        </section>
      </div>
    </div>
  );
}