summaryrefslogblamecommitdiff
path: root/frontend/src/components/DaemonDetail.tsx
blob: 2de0997f18ba57be18ae04af3950aaa0e4a3df13 (plain) (tree)




































































































































































































































                                                                                 
import React, { useEffect, useState } from 'react'
import { useParams, Link } from 'react-router-dom'

interface Daemon {
  id: string
  ownerId: string
  connectionId: string
  hostname: string | null
  machineId: string | null
  maxConcurrentTasks: number
  currentTaskCount: number
  status: string
  lastHeartbeatAt: string
  connectedAt: string
  disconnectedAt: string | null
}

interface DaemonDirectory {
  path: string
  label: string
  directoryType: string
  hostname: string | null
  exists?: boolean
}

type Tab = 'overview' | 'directories'

function statusIndicator(status: string): { color: string; label: string } {
  switch (status.toLowerCase()) {
    case 'connected':
      return { color: 'green', label: 'Connected' }
    case 'disconnected':
      return { color: 'red', label: 'Disconnected' }
    case 'unhealthy':
      return { color: 'yellow', label: 'Unhealthy' }
    default:
      return { color: 'gray', label: status }
  }
}

export function DaemonDetail() {
  const { id } = useParams<{ id: string }>()
  const [daemon, setDaemon] = useState<Daemon | null>(null)
  const [directories, setDirectories] = useState<DaemonDirectory[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [activeTab, setActiveTab] = useState<Tab>('overview')

  useEffect(() => {
    async function fetchDaemon() {
      if (!id) return

      try {
        setLoading(true)
        const [daemonRes, dirRes] = await Promise.all([
          fetch(`/api/v1/mesh/daemons/${id}`),
          fetch('/api/v1/mesh/daemons/directories'),
        ])

        if (!daemonRes.ok) {
          throw new Error(`Failed to fetch daemon: ${daemonRes.statusText}`)
        }

        const daemonData = await daemonRes.json()
        setDaemon(daemonData)

        if (dirRes.ok) {
          const dirData = await dirRes.json()
          setDirectories(dirData.directories || [])
        }
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error')
      } finally {
        setLoading(false)
      }
    }

    fetchDaemon()

    const interval = setInterval(async () => {
      if (!id) return
      try {
        const response = await fetch(`/api/v1/mesh/daemons/${id}`)
        if (response.ok) {
          const daemonData = await response.json()
          setDaemon(daemonData)
        }
      } catch {
        // Silently ignore refresh errors
      }
    }, 10000)

    return () => clearInterval(interval)
  }, [id])

  if (loading) {
    return (
      <div className="daemon-detail-container">
        <div className="loading">Loading daemon...</div>
      </div>
    )
  }

  if (error) {
    return (
      <div className="daemon-detail-container">
        <div className="error">Error: {error}</div>
        <Link to="/daemons" className="back-link">
          Back to Daemons
        </Link>
      </div>
    )
  }

  if (!daemon) {
    return (
      <div className="daemon-detail-container">
        <div className="not-found">Daemon not found</div>
        <Link to="/daemons" className="back-link">
          Back to Daemons
        </Link>
      </div>
    )
  }

  const status = statusIndicator(daemon.status)
  const filteredDirectories = directories.filter(
    (dir) => dir.hostname === daemon.hostname
  )

  return (
    <div className="daemon-detail-container">
      <div className="daemon-detail-header">
        <Link to="/daemons" className="back-link">
          Back to Daemons
        </Link>
        <h1 className="daemon-title">{daemon.hostname || 'Unknown Host'}</h1>
        <div className="daemon-meta">
          <span>
            <span
              className="status-dot"
              style={{ backgroundColor: status.color }}
            />
            {status.label}
          </span>
          <span>
            Tasks: {daemon.currentTaskCount} / {daemon.maxConcurrentTasks}
          </span>
        </div>
      </div>

      <div className="daemon-tabs">
        <button
          className={`tab-button ${activeTab === 'overview' ? 'active' : ''}`}
          onClick={() => setActiveTab('overview')}
        >
          Overview
        </button>
        <button
          className={`tab-button ${activeTab === 'directories' ? 'active' : ''}`}
          onClick={() => setActiveTab('directories')}
        >
          Directories ({filteredDirectories.length})
        </button>
      </div>

      <div className="daemon-tab-content">
        {activeTab === 'overview' && (
          <div className="tab-panel">
            <h2>Daemon Overview</h2>
            <dl className="overview-list">
              <dt>ID</dt>
              <dd>{daemon.id}</dd>
              <dt>Hostname</dt>
              <dd>{daemon.hostname || 'N/A'}</dd>
              <dt>Machine ID</dt>
              <dd>{daemon.machineId || 'N/A'}</dd>
              <dt>Status</dt>
              <dd>
                <span
                  className="status-dot"
                  style={{ backgroundColor: status.color }}
                />
                {status.label}
              </dd>
              <dt>Connection ID</dt>
              <dd>{daemon.connectionId}</dd>
              <dt>Max Concurrent Tasks</dt>
              <dd>{daemon.maxConcurrentTasks}</dd>
              <dt>Current Task Count</dt>
              <dd>{daemon.currentTaskCount}</dd>
              <dt>Connected At</dt>
              <dd>{new Date(daemon.connectedAt).toLocaleString()}</dd>
              <dt>Last Heartbeat At</dt>
              <dd>{new Date(daemon.lastHeartbeatAt).toLocaleString()}</dd>
              <dt>Disconnected At</dt>
              <dd>
                {daemon.disconnectedAt
                  ? new Date(daemon.disconnectedAt).toLocaleString()
                  : 'N/A'}
              </dd>
            </dl>
          </div>
        )}

        {activeTab === 'directories' && (
          <div className="tab-panel">
            <h2>Directories</h2>
            {filteredDirectories.length === 0 ? (
              <p>No directories found for this daemon</p>
            ) : (
              <ul className="directory-list">
                {filteredDirectories.map((dir, index) => (
                  <li key={`${dir.path}-${index}`} className="directory-item">
                    <h3>{dir.label}</h3>
                    <div className="daemon-meta">
                      <span>Path: {dir.path}</span>
                      <span>Type: {dir.directoryType}</span>
                    </div>
                  </li>
                ))}
              </ul>
            )}
          </div>
        )}
      </div>
    </div>
  )
}