// screens-home.jsx
//
// Two routes share this file:
//
// • Home - the launcher. Big-button entry into each section of the MAM.
//   Untouched in this rewrite.
//
// • Dashboard - the operations view. Rebuilt as a control-room status
//   board, not a SaaS analytics page. Sections render top-down by
//   operator priority:
//
//     1. ON AIR        - live recorder tiles, full-width
//     2. UP NEXT       - single-row strip of next scheduled recordings
//     3. ATTENTION     - conditional; only when something failed
//     4. WORK + CLUSTER - two-column dense panels
//     5. STATUS BAR    - single mono-text line, bottom
//
//   Anything that would just say "all clear" is hidden, not rendered.

function Home({ navigate }) {
  const [showDownloads, setShowDownloads] = React.useState(false);

  // Pull live counts so the tile subtitles ("34 assets", "0 live", "3 running")
  // reflect what's actually in the DB right now, not a stale boot-time cache.
  const [cards, setCards] = React.useState({});
  React.useEffect(() => {
    let cancelled = false;
    const load = () => {
      window.ZAMPP_API.fetch('/metrics/home?hours=1')
        .then(d => { if (!cancelled) setCards(d?.cards || {}); })
        .catch(() => {});
    };
    load();
    const t = setInterval(load, 30_000);
    return () => { cancelled = true; clearInterval(t); };
  }, []);

  const { ASSETS = [], RECORDERS = [], JOBS = [], NODES = [] } = window.ZAMPP_DATA || {};
  const assetsTotal  = cards.assets?.total      ?? ASSETS.length;
  const liveCount    = cards.recorders?.live    ?? RECORDERS.filter(r => r.status === 'recording').length;
  const totalRecs    = cards.recorders?.total   ?? RECORDERS.length;
  const runningJobs  = cards.jobs?.running      ?? JOBS.filter(j => j.status === 'running' || j.status === 'queued').length;
  const failedJobs   = cards.jobs?.failed_total ?? JOBS.filter(j => j.status === 'failed').length;
  const nodesOnline  = cards.cluster?.online    ?? NODES.filter(n => n.status === 'online' || n.online === true).length;
  const nodesTotal   = cards.cluster?.total     ?? NODES.length;

  const tiles = [
    {
      id: 'library',
      label: 'Library',
      icon: 'library',
      tone: 'accent',
      sub: assetsTotal > 0 ? assetsTotal.toLocaleString() + ' assets' : 'No assets yet',
      desc: 'Browse projects, bins, and assets. Hover-scrub previews.',
    },
    {
      id: 'recorders',
      label: 'Recorders',
      icon: 'record',
      tone: 'live',
      sub: liveCount > 0
        ? liveCount + ' live · ' + totalRecs + ' configured'
        : totalRecs + ' configured',
      desc: 'SDI · SRT · RTMP ingest. Start, stop, schedule.',
    },
    {
      id: 'playout',
      label: 'Playout',
      icon: 'monitor',
      tone: 'live',
      sub: 'Master Control',
      desc: 'Play assets to SDI, NDI, SRT or RTMP via CasparCG.',
    },
    {
      id: '__downloads',
      label: 'Downloads',
      icon: 'download',
      tone: 'purple',
      sub: 'Premiere panel · Dragon-ISO',
      desc: 'Download the Premiere Pro panel and Dragon-ISO NDI tools.',
    },
    {
      id: 'jobs',
      label: 'Jobs',
      icon: 'jobs',
      tone: failedJobs > 0 ? 'warn' : 'success',
      sub: runningJobs > 0
        ? runningJobs + ' running' + (failedJobs > 0 ? ' · ' + failedJobs + ' failed' : '')
        : (failedJobs > 0 ? failedJobs + ' failed' : 'All clear'),
      desc: 'Proxy + thumbnail queue. Retry failed jobs.',
    },
    {
      id: 'settings',
      label: 'Settings',
      icon: 'settings',
      tone: 'neutral',
      sub: 'S3 · Encoder · Growing files',
      desc: 'Storage, proxy encoder, capture SDK, growing-file mode.',
    },
  ];

  const clusterHealthy = !nodesTotal || nodesOnline >= nodesTotal;

  // Activity strip (#153): live recorders + last-24h assets + alerts.
  const liveRecorders = RECORDERS.filter(r => r.status === 'recording').slice(0, 4);
  const recentAssets = (() => {
    const dayAgo = Date.now() - 86400000;
    return ASSETS
      .filter(a => a.created_at && new Date(a.created_at).getTime() > dayAgo)
      .sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
      .slice(0, 6);
  })();
  const failedCount = JOBS.filter(j => j.status === 'failed').length;
  const errCount = RECORDERS.filter(r => r.status === 'error').length;
  const hasActivity = liveRecorders.length || recentAssets.length || failedCount || errCount;

  return (
    <div className="launcher">
      <div className="launcher-inner">
        <div className="launcher-hero">
          <img
            className="launcher-logo"
            src="img/dragon-logo.png"
            alt="Dragonflight"
            draggable="false"
          />
          <h1 className="launcher-wordmark">DRAGONFLIGHT</h1>
          <p className="launcher-tagline">
            Self-hosted broadcast media-asset management
          </p>
        </div>

        <div className="launcher-grid">
          {tiles.map(t => (
            <button
              key={t.id}
              className={'launcher-tile tone-' + t.tone}
              onClick={() => t.id === '__downloads' ? setShowDownloads(true) : navigate(t.id)}
            >
              <span className="launcher-tile-icon">
                <Icon name={t.icon} size={26} />
              </span>
              <span className="launcher-tile-label">{t.label}</span>
              <span className="launcher-tile-sub">{t.sub}</span>
              <span className="launcher-tile-desc">{t.desc}</span>
              <span className="launcher-tile-arrow">
                <Icon name="arrowRight" size={14} />
              </span>
            </button>
          ))}

          <button
            className="launcher-tile tone-ghost launcher-tile-secondary"
            onClick={() => navigate('dashboard')}
          >
            <span className="launcher-tile-icon">
              <Icon name="home" size={22} />
            </span>
            <span className="launcher-tile-label">Dashboard</span>
            <span className="launcher-tile-sub">Operations view</span>
            <span className="launcher-tile-desc">
              Recent activity, job queue, cluster health.
            </span>
            <span className="launcher-tile-arrow">
              <Icon name="arrowRight" size={14} />
            </span>
          </button>
        </div>

        {hasActivity && (
          <div className="launcher-activity">
            {(failedCount > 0 || errCount > 0) && (
              <div className="launcher-activity-strip alert">
                <Icon name="alert" size={14} />
                <span>
                  {errCount > 0 && <strong>{errCount} recorder{errCount === 1 ? '' : 's'} in error.</strong>}
                  {errCount > 0 && failedCount > 0 && ' '}
                  {failedCount > 0 && <strong>{failedCount} failed job{failedCount === 1 ? '' : 's'}.</strong>}
                </span>
                <button className="btn ghost sm" onClick={() => navigate('dashboard')}>Open Dashboard</button>
              </div>
            )}

            {liveRecorders.length > 0 && (
              <div className="launcher-activity-section">
                <div className="launcher-activity-head">
                  <span className="rec-dot" />
                  Recording now
                  <span className="muted">{liveRecorders.length} live</span>
                  <div style={{ flex: 1 }} />
                  <button className="btn ghost sm" onClick={() => navigate('monitors')}>Monitors</button>
                </div>
                <div className="launcher-activity-grid">
                  {liveRecorders.map(r => (
                    <button key={r.id} className="launcher-activity-item" onClick={() => navigate('recorders')}>
                      <span className="badge live">REC</span>
                      <span className="launcher-activity-item-name">{r.name}</span>
                      <span className="launcher-activity-item-meta mono">{r.source_type || 'sdi'}</span>
                    </button>
                  ))}
                </div>
              </div>
            )}

            {recentAssets.length > 0 && (
              <div className="launcher-activity-section">
                <div className="launcher-activity-head">
                  <Icon name="library" size={12} />
                  Last 24 hours
                  <span className="muted">{recentAssets.length} new asset{recentAssets.length === 1 ? '' : 's'}</span>
                  <div style={{ flex: 1 }} />
                  <button className="btn ghost sm" onClick={() => navigate('library')}>Library</button>
                </div>
                <div className="launcher-activity-grid">
                  {recentAssets.map(a => (
                    <button key={a.id} className="launcher-activity-item" onClick={() => navigate('library')}>
                      <Icon name={a.media_type === 'audio' ? 'audio' : 'video'} size={13} />
                      <span className="launcher-activity-item-name">{a.display_name || a.filename || 'untitled'}</span>
                      <span className="launcher-activity-item-meta mono">
                        {(() => {
                          const mins = Math.round((Date.now() - new Date(a.created_at)) / 60000);
                          if (mins < 60) return mins + 'm';
                          const h = Math.round(mins / 60);
                          return h + 'h';
                        })()}
                      </span>
                    </button>
                  ))}
                </div>
              </div>
            )}
          </div>
        )}

        <div className="launcher-status">
          <span className="launcher-status-pip">
            <span
              className="dot"
              style={{ background: clusterHealthy ? 'var(--success)' : 'var(--warning)' }}
            />
            {clusterHealthy ? 'cluster healthy' : 'cluster degraded'}
            <span className="muted">· {nodesOnline}/{nodesTotal || 0} node{nodesTotal === 1 ? '' : 's'}</span>
          </span>
          {liveCount > 0 && (
            <span className="launcher-status-pip live">
              <span className="dot" style={{ background: 'var(--live)' }} />
              {liveCount} recorder{liveCount === 1 ? '' : 's'} live
            </span>
          )}
        </div>
      </div>
      {showDownloads && <DownloadsModal onClose={() => setShowDownloads(false)} />}
    </div>
  );
}

// Combined downloads modal: Premiere Pro panel + Dragon-ISO.
function DownloadsModal({ onClose }) {
  const releases = (window.PREMIERE_RELEASES || []).slice().sort((a, b) => {
    const av = String(a.version || ''), bv = String(b.version || '');
    return bv.localeCompare(av, undefined, { numeric: true });
  });
  const latest = window.PREMIERE_LATEST || releases[0] || null;

  const DRAGON_ISO_RELEASES_URL = 'https://forge.wilddragon.net/WildDragonLLC/dragon-iso/releases';

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 580 }}>
        <div className="modal-head">
          <div>
            <div style={{ fontSize: 15, fontWeight: 600 }}>Downloads</div>
            <div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 2 }}>
              Premiere Pro panel and Dragon-ISO NDI tools.
            </div>
          </div>
          <button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" /></button>
        </div>

        <div className="modal-body">
          {/* ── Premiere panel ── */}
          <div className="downloads-section-head">
            <Icon name="editor" size={13} />
            <span>Premiere Pro panel (UXP)</span>
          </div>
          <div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 10 }}>
            Install the .ccx per workstation via the Adobe UXP Developer Tool, or double-click it with Creative Cloud installed.
          </div>
          {releases.length === 0 && (
            <div style={{ padding: '12px 0', color: 'var(--text-3)', fontSize: 12 }}>
              No releases registered. Upload one from Settings → Capture SDKs.
            </div>
          )}
          {releases.map((rel, i) => (
            <div key={rel.version || i} className="premiere-release">
              <div className="premiere-release-head">
                <span className="premiere-release-version mono">v{rel.version}</span>
                {latest && latest.version === rel.version && (
                  <span className="badge accent">LATEST</span>
                )}
                {rel.released_at && (
                  <span className="premiere-release-date mono">
                    {new Date(rel.released_at).toLocaleDateString()}
                  </span>
                )}
              </div>
              {rel.notes && <div className="premiere-release-notes">{rel.notes}</div>}
              <div className="premiere-release-actions">
                {rel.ccx && (
                  <a href={rel.ccx} download className="btn primary sm">
                    <Icon name="download" size={12} />UXP plugin (.ccx)
                  </a>
                )}
                {rel.installer && (
                  <a href={rel.installer} download className="btn ghost sm">
                    <Icon name="download" size={12} />Windows installer
                  </a>
                )}
              </div>
            </div>
          ))}

          {/* ── Dragon-ISO ── */}
          <div className="downloads-section-head" style={{ marginTop: 20 }}>
            <Icon name="film" size={13} />
            <span>Dragon-ISO</span>
          </div>
          <div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 10 }}>
            NDI ISO recorder for Microsoft Teams. Windows, .NET 8 WPF.
          </div>
          <div className="premiere-release">
            <div className="premiere-release-head">
              <span className="premiere-release-version mono">Releases</span>
            </div>
            <div className="premiere-release-actions">
              <a href={DRAGON_ISO_RELEASES_URL} target="_blank" rel="noopener noreferrer" className="btn primary sm">
                <Icon name="download" size={12} />View releases on Forgejo
              </a>
            </div>
          </div>
        </div>

        <div className="modal-foot">
          <button className="btn ghost" onClick={onClose}>Close</button>
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────
// Dashboard - broadcast-ops control board
// ─────────────────────────────────────────────────────────────────────────

function Dashboard({ navigate }) {
  const { RECORDERS, JOBS, NODES } = window.ZAMPP_DATA;

  // Live state - recompute every second so elapsed timers keep ticking.
  const [tick, setTick] = React.useState(0);
  React.useEffect(() => {
    const i = setInterval(() => setTick(t => t + 1), 1000);
    return () => clearInterval(i);
  }, []);

  // Upcoming schedule for UP NEXT strip.
  const [upcoming, setUpcoming] = React.useState([]);
  React.useEffect(() => {
    let cancelled = false;
    const load = () => {
      window.ZAMPP_API.fetch('/schedules?status=upcoming')
        .then(d => { if (!cancelled) setUpcoming(d?.schedules || []); })
        .catch(() => {});
    };
    load();
    const t = setInterval(load, 30_000);
    return () => { cancelled = true; clearInterval(t); };
  }, []);

  // Refresh jobs frequently - this screen is the failed-job alert surface.
  const [jobs, setJobs] = React.useState(JOBS);
  React.useEffect(() => {
    let cancelled = false;
    const load = () => {
      window.ZAMPP_API.fetch('/jobs')
        .then(raw => {
          if (cancelled || !Array.isArray(raw)) return;
          const statusMap = { waiting: 'queued', active: 'running', completed: 'done', failed: 'failed' };
          const kindMap = { proxy: 'Proxy', thumbnail: 'Thumbnail', conform: 'Conform', transcode: 'Transcode', import: 'YouTube' };
          const norm = raw.map(j => {
            const meta = j.metadata || {};
            return {
              ...j,
              status: statusMap[j.status] || j.status,
              kind: kindMap[j.type] || j.type || 'Job',
              asset: j.asset_name || meta.filename || '·',
              node: meta.node || '·',
              error: j.error || null,
              progress: j.progress || 0,
            };
          });
          window.ZAMPP_DATA.JOBS = norm;
          setJobs(norm);
        })
        .catch(() => {});
    };
    load();
    const t = setInterval(load, 5_000);
    return () => { cancelled = true; clearInterval(t); };
  }, []);

  const liveRecorders   = RECORDERS.filter(r => r.status === 'recording');
  const erroredRecorders = RECORDERS.filter(r => r.status === 'error');
  const runningJobs     = jobs.filter(j => j.status === 'running');
  const queuedJobs      = jobs.filter(j => j.status === 'queued');
  const failedJobs      = jobs.filter(j => j.status === 'failed');
  const doneJobs        = jobs.filter(j => j.status === 'done');
  const offlineNodes    = NODES.filter(n => !(n.status === 'online' || n.online === true));
  const onlineNodes     = NODES.length - offlineNodes.length;

  // Next 3 upcoming, sorted, future only
  const nowMs = Date.now();
  const nextUp = upcoming
    .filter(s => new Date(s.start_at).getTime() > nowMs && s.status === 'pending')
    .slice(0, 3);

  const hasAttention = failedJobs.length > 0 || offlineNodes.length > 0 || erroredRecorders.length > 0;

  return (
    <div className="page dash">
      <div className="page-header">
        <h1>Dashboard</h1>
        <span className="subtitle">Live operations: on-air recorders, jobs, cluster health</span>
        <div className="spacer" />
        {hasAttention && (
          <span className="badge danger" title="Items need attention">
            <Icon name="alert" size={10} />
            {failedJobs.length + offlineNodes.length + erroredRecorders.length} alert{failedJobs.length + offlineNodes.length + erroredRecorders.length === 1 ? '' : 's'}
          </span>
        )}
        <span className="status-pip">
          <span className="dot" style={{ background: offlineNodes.length === 0 ? 'var(--success)' : 'var(--warning)' }} />
          <span>{onlineNodes}/{NODES.length || 0} nodes online</span>
        </span>
      </div>

      {/* ────────── ON AIR ────────── */}
      <section className="dash-section">
        <DashSectionHead
          title="On air"
          accent="live"
          count={liveRecorders.length}
          countLabel={liveRecorders.length === 1 ? 'recorder live' : 'recorders live'}
          onMore={() => navigate('recorders')}
          moreLabel="All recorders"
        />
        {liveRecorders.length === 0 ? (
          <DashInlineEmpty
            icon="record"
            text="No recorders on air."
            cta="Start a recorder"
            onCta={() => navigate('recorders')}
          />
        ) : (
          <div className="dash-onair-grid" data-count={Math.min(liveRecorders.length, 4)}>
            {liveRecorders.slice(0, 4).map(r => (
              <OnAirTile key={r.id} recorder={r} onClick={() => navigate('recorders')} />
            ))}
          </div>
        )}
      </section>

      {/* ────────── UP NEXT ────────── */}
      {nextUp.length > 0 && (
        <section className="dash-section">
          <DashSectionHead
            title="Up next"
            accent="accent"
            count={nextUp.length}
            countLabel="scheduled"
            onMore={() => navigate('schedule')}
            moreLabel="Schedule"
          />
          <div className="dash-next-row">
            {nextUp.map(s => (
              <UpNextCard key={s.id} schedule={s} />
            ))}
          </div>
        </section>
      )}

      {/* ────────── ATTENTION ────────── */}
      {hasAttention && (
        <section className="dash-section dash-attention">
          <DashSectionHead
            title="Needs attention"
            accent="danger"
            count={failedJobs.length + offlineNodes.length + erroredRecorders.length}
            countLabel="items"
          />
          <div className="panel dash-attention-panel">
            {erroredRecorders.map(r => (
              <AttentionRow
                key={'r-' + r.id}
                level="danger"
                icon="record"
                title={r.name}
                detail={'recorder error' + (r.error_message ? ' · ' + r.error_message : '')}
                onClick={() => navigate('recorders')}
              />
            ))}
            {offlineNodes.map(n => (
              <AttentionRow
                key={'n-' + (n.id || n.hostname)}
                level="warning"
                icon="cluster"
                title={n.hostname || n.id}
                detail="node offline · no heartbeat for >2 min"
                onClick={() => navigate('cluster')}
              />
            ))}
            {failedJobs.slice(0, 5).map(j => (
              <AttentionRow
                key={'j-' + j.id}
                level="danger"
                icon="alert"
                title={j.kind + ' failed'}
                detail={(j.asset || '·') + (j.error ? ' · ' + j.error.slice(0, 100) : '')}
                onClick={() => navigate('jobs')}
              />
            ))}
            {failedJobs.length > 5 && (
              <div className="dash-attention-more" onClick={() => navigate('jobs')}>
                +{failedJobs.length - 5} more failed jobs
                <Icon name="arrowRight" size={12} />
              </div>
            )}
          </div>
        </section>
      )}

      {/* ────────── WORK + CLUSTER ────────── */}
      <section className="dash-grid">
        {/* JOB QUEUE */}
        <div className="dash-col">
          <DashSectionHead
            title="Job queue"
            count={runningJobs.length + queuedJobs.length}
            countLabel="active"
            onMore={() => navigate('jobs')}
            moreLabel="All jobs"
          />
          <div className="panel dash-jobs-panel">
            {jobs.length === 0 ? (
              <div className="dash-panel-empty">
                <Icon name="check" size={14} />
                Queue clear · {doneJobs.length} done
              </div>
            ) : (
              <>
                <div className="dash-jobs-head">
                  <span></span>
                  <span>Job</span>
                  <span>Asset</span>
                  <span>Progress</span>
                </div>
                {/* Running jobs first (with bars), then queued, then a few recent done */}
                {[...runningJobs, ...queuedJobs, ...doneJobs.slice(0, Math.max(0, 6 - runningJobs.length - queuedJobs.length))]
                  .slice(0, 8)
                  .map(j => <DashJobRow key={j.id} job={j} />)}
              </>
            )}
          </div>
        </div>

        {/* CLUSTER */}
        <div className="dash-col">
          <DashSectionHead
            title="Cluster"
            count={onlineNodes}
            countLabel={NODES.length ? 'of ' + NODES.length + ' online' : 'no nodes'}
            onMore={() => navigate('cluster')}
            moreLabel="All nodes"
          />
          <div className="panel dash-cluster-panel">
            {NODES.length === 0 ? (
              <div className="dash-panel-empty">
                <Icon name="alert" size={14} />
                No nodes registered
              </div>
            ) : (
              <>
                <div className="dash-cluster-head">
                  <span></span>
                  <span>Host</span>
                  <span>CPU</span>
                  <span>Mem</span>
                </div>
                {NODES.slice(0, 6).map(n => <DashClusterRow key={n.id || n.hostname || n.name} node={n} />)}
              </>
            )}
          </div>
        </div>
      </section>

      {/* ────────── RESOURCES ────────── */}
      <section className="dash-section">
        <DashSectionHead title="Resources" />
        {window.ClusterResources && <window.ClusterResources />}
      </section>

      {/* ────────── RESOURCES ────────── */}
      <section className="dash-section">
        <DashSectionHead title="Resources" />
        {window.ClusterResources && React.createElement(window.ClusterResources)}
      </section>

      {/* ────────── STATUS BAR (bottom) ────────── */}
      <footer className="dash-statusbar">
        <span className="dash-stat-pip" data-tone={liveRecorders.length > 0 ? 'live' : 'idle'}>
          <span className="dash-pip-dot" />
          <span className="dash-pip-num">{liveRecorders.length}</span>
          <span className="dash-pip-label">live</span>
        </span>
        <span className="dash-statusbar-sep">·</span>
        <span className="dash-stat-pip" data-tone={runningJobs.length > 0 ? 'accent' : 'idle'}>
          <span className="dash-pip-num">{runningJobs.length}</span>
          <span className="dash-pip-label">running</span>
        </span>
        <span className="dash-statusbar-sep">·</span>
        <span className="dash-stat-pip">
          <span className="dash-pip-num">{queuedJobs.length}</span>
          <span className="dash-pip-label">queued</span>
        </span>
        <span className="dash-statusbar-sep">·</span>
        <span className="dash-stat-pip" data-tone={failedJobs.length > 0 ? 'warning' : 'idle'}>
          <span className="dash-pip-num">{failedJobs.length}</span>
          <span className="dash-pip-label">failed</span>
        </span>
        <span className="dash-statusbar-spacer" />
        <span className="dash-stat-pip" data-tone={offlineNodes.length === 0 ? 'success' : 'warning'}>
          <span className="dash-pip-num">{onlineNodes}/{NODES.length}</span>
          <span className="dash-pip-label">nodes online</span>
        </span>
        <span className="dash-statusbar-sep">·</span>
        <span className="dash-statusbar-clock">{new Date(nowMs).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' })}</span>
      </footer>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────
// Subcomponents
// ─────────────────────────────────────────────────────────────────────────

function DashSectionHead({ title, accent, count, countLabel, onMore, moreLabel }) {
  return (
    <div className="dash-sectionhead" data-accent={accent || 'neutral'}>
      <span className="dash-sectionhead-title">{title}</span>
      {typeof count === 'number' && (
        <span className="dash-sectionhead-count">
          <span className="dash-sectionhead-num">{count}</span>
          {countLabel && <span className="dash-sectionhead-label">{countLabel}</span>}
        </span>
      )}
      {onMore && (
        <button className="dash-sectionhead-more" onClick={onMore}>
          {moreLabel || 'View all'}
          <Icon name="arrowRight" size={11} />
        </button>
      )}
    </div>
  );
}

function DashInlineEmpty({ icon, text, cta, onCta }) {
  return (
    <div className="dash-inline-empty">
      <Icon name={icon} size={13} />
      <span>{text}</span>
      {cta && onCta && (
        <button className="dash-inline-empty-cta" onClick={onCta}>
          {cta} <Icon name="arrowRight" size={11} />
        </button>
      )}
    </div>
  );
}

function OnAirTile({ recorder, onClick }) {
  return (
    <div className="dash-onair-tile" onClick={onClick}>
      <div className="dash-onair-video">
        {recorder.live_asset_id
          ? <HlsPreview assetId={recorder.live_asset_id} recorderId={recorder.id} />
          : <FauxFrame />}
        <span className="dash-onair-rec-pip">
          <span className="dash-onair-rec-dot" />
          REC
        </span>
        <span className="dash-onair-time">{recorder.elapsed || '00:00:00'}</span>
      </div>
      <div className="dash-onair-meta">
        <div className="dash-onair-name">{recorder.name}</div>
        <div className="dash-onair-sub">
          <span className="dash-onair-source">{recorder.source || '·'}</span>
          {recorder.res && recorder.res !== '·' && (
            <>
              <span className="dash-onair-dot">·</span>
              <span className="dash-onair-res">{recorder.res}</span>
            </>
          )}
          {recorder.codec && recorder.codec !== '·' && (
            <>
              <span className="dash-onair-dot">·</span>
              <span className="dash-onair-codec">{recorder.codec}</span>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

function UpNextCard({ schedule }) {
  const start = new Date(schedule.start_at);
  const end = new Date(schedule.end_at);
  const nowMs = Date.now();
  const inMs = start.getTime() - nowMs;
  const inMin = Math.round(inMs / 60_000);
  const durMin = Math.round((end - start) / 60_000);

  let relative;
  if (inMin < 1) relative = 'starting now';
  else if (inMin < 60) relative = 'in ' + inMin + 'm';
  else if (inMin < 1440) relative = 'in ' + Math.round(inMin / 60) + 'h';
  else relative = 'in ' + Math.round(inMin / 1440) + 'd';

  const imminent = inMin <= 5;

  return (
    <div className={'dash-next-card' + (imminent ? ' imminent' : '')}>
      <div className="dash-next-time">
        <span className="dash-next-clock">
          {start.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}
        </span>
        <span className="dash-next-rel">{relative}</span>
      </div>
      <div className="dash-next-body">
        <div className="dash-next-name">{schedule.name}</div>
        <div className="dash-next-sub">
          <Icon name="record" size={11} />
          <span>{schedule.recorder_name || 'unbound'}</span>
          <span className="dash-onair-dot">·</span>
          <span>{durMin}m</span>
        </div>
      </div>
    </div>
  );
}

function AttentionRow({ level, icon, title, detail, onClick }) {
  return (
    <div className={'dash-attention-row level-' + level} onClick={onClick}>
      <span className="dash-attention-icon"><Icon name={icon} size={13} /></span>
      <span className="dash-attention-title">{title}</span>
      <span className="dash-attention-detail">{detail}</span>
      <Icon name="arrowRight" size={12} className="dash-attention-arrow" />
    </div>
  );
}

function DashJobRow({ job }) {
  const iconMap = { Proxy: 'proxy', Transcode: 'film', Thumbnail: 'image', Conform: 'layers', YouTube: 'download' };
  return (
    <div className="dash-jobs-row" data-status={job.status}>
      <span className="dash-jobs-status">
        <StatusDot status={job.status} />
      </span>
      <span className="dash-jobs-kind">
        <Icon name={iconMap[job.kind] || 'jobs'} size={12} />
        <span>{job.kind}</span>
      </span>
      <span className="dash-jobs-asset">{job.asset}</span>
      <span className="dash-jobs-progress">
        {job.status === 'running' && (
          <>
            <span className="dash-jobs-bar">
              <span className="dash-jobs-bar-fill" style={{ width: Math.round(job.progress || 0) + '%' }} />
            </span>
            <span className="dash-jobs-pct">{Math.round(job.progress || 0)}%</span>
          </>
        )}
        {job.status === 'queued' && <span className="dash-jobs-state">queued</span>}
        {job.status === 'done' && <span className="dash-jobs-state done">done</span>}
        {job.status === 'failed' && <span className="dash-jobs-state failed">failed</span>}
      </span>
    </div>
  );
}

function DashClusterRow({ node }) {
  const nodeId = node.hostname || node.id || node.name || 'node';
  const isOnline = node.status === 'online' || node.online === true;
  const cpuPct = node.cpu_percent ?? node.cpu ?? node.cpu_usage ?? null;
  const memUsed = node.memory_used_gb ?? node.mem ?? (node.mem_used_mb != null ? node.mem_used_mb / 1024 : null);
  const memTotal = node.memory_total_gb ?? node.mem_total_gb ?? null;
  const memPct = memUsed != null && memTotal != null
    ? Math.round((memUsed / memTotal) * 100)
    : (memUsed != null ? Math.min(100, Math.round((memUsed / 32) * 100)) : null);

  return (
    <div className="dash-cluster-row" data-online={isOnline}>
      <span className="dash-cluster-status">
        <StatusDot status={isOnline ? 'online' : 'offline'} />
      </span>
      <span className="dash-cluster-name">
        <span className="dash-cluster-host">{nodeId}</span>
        {node.role && <span className="dash-cluster-role">{node.role}</span>}
      </span>
      <span className="dash-cluster-metric">
        {cpuPct != null ? (
          <>
            <span className="dash-cluster-bar">
              <span
                className="dash-cluster-bar-fill"
                style={{
                  width: Math.round(cpuPct) + '%',
                  background: cpuPct > 85 ? 'var(--warning)' : cpuPct > 60 ? 'var(--accent)' : 'var(--success)',
                }}
              />
            </span>
            <span className="dash-cluster-val">{Math.round(cpuPct)}%</span>
          </>
        ) : <span className="dash-cluster-val muted">·</span>}
      </span>
      <span className="dash-cluster-metric">
        {memPct != null ? (
          <>
            <span className="dash-cluster-bar">
              <span
                className="dash-cluster-bar-fill"
                style={{
                  width: memPct + '%',
                  background: memPct > 85 ? 'var(--warning)' : 'var(--text-2)',
                }}
              />
            </span>
            <span className="dash-cluster-val">{memUsed < 1 ? Math.round(memUsed * 1024) + 'M' : memUsed.toFixed(1) + 'G'}</span>
          </>
        ) : <span className="dash-cluster-val muted">·</span>}
      </span>
    </div>
  );
}

window.Home = Home;
window.Dashboard = Dashboard;
