← back to Homesonspec

apps/admin/src/app/engine/EngineLive.tsx

80 lines

"use client";

import { useEffect, useRef, useState } from "react";

type Resp = { ts: string; ok: boolean; error?: string; lines: string[] };

function lineClass(l: string): string {
  if (/SWEEP/i.test(l)) return "text-cyan-300 font-semibold";
  if (/"errors":\s*\[\s*\]/.test(l)) return "text-neutral-600"; // empty errors array = NO errors, don't flag red
  if (/Prisma|throw|ECONN|blocked|disallow|\b403\b|\b429\b|"errors":\s*\[[^\]]|\bError\b/.test(l)) return "text-red-400";
  if (/"published":\s*(?!0\b)\d+/.test(l)) return "text-emerald-300";
  if (/fanning out|enrich|sleeping/i.test(l)) return "text-amber-300";
  if (/^\s*(drh|plt|len|dw|kb|tri|tol)\b/i.test(l.replace(/^[0-9T:\-]+:?\s*/, ""))) return "text-neutral-300";
  return "text-neutral-400";
}

export default function EngineLive() {
  const [r, setR] = useState<Resp | null>(null);
  const [err, setErr] = useState(false);
  const [live, setLive] = useState(true);
  const boxRef = useRef<HTMLDivElement>(null);
  const stick = useRef(true);

  useEffect(() => {
    let alive = true;
    const load = async () => {
      try {
        const res = await fetch("/api/engine-log", { cache: "no-store" });
        const j: Resp = await res.json();
        if (!alive) return;
        setR(j); setErr(!j.ok);
      } catch { if (alive) setErr(true); }
    };
    load();
    const id = setInterval(() => live && load(), 2500);
    return () => { alive = false; clearInterval(id); };
  }, [live]);

  // auto-scroll to bottom unless the user scrolled up
  useEffect(() => {
    const el = boxRef.current;
    if (el && stick.current) el.scrollTop = el.scrollHeight;
  }, [r]);

  const onScroll = () => {
    const el = boxRef.current;
    if (el) stick.current = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
  };

  const lines = r?.lines ?? [];
  const newest = lines[lines.length - 1] ?? "";

  return (
    <div className="mt-4">
      <div className="mb-2 flex items-center gap-2 text-sm">
        <span className={`inline-block h-2.5 w-2.5 rounded-full ${err ? "bg-red-500" : "bg-emerald-500 animate-pulse"}`} />
        <span className={err ? "text-red-700" : "text-emerald-700"}>{err ? "engine log unavailable" : "engine live"}</span>
        <span className="text-neutral-400">· {lines.length} lines · updated {r ? new Date(r.ts).toLocaleTimeString() : "…"}</span>
        <button
          onClick={() => setLive((v) => !v)}
          className={`ml-auto rounded-md border px-2 py-0.5 text-xs ${live ? "border-emerald-300 text-emerald-700" : "border-neutral-300 text-neutral-500"}`}
        >
          {live ? "⏸ pause" : "▶ resume"}
        </button>
      </div>
      <div
        ref={boxRef}
        onScroll={onScroll}
        className="h-[560px] overflow-auto rounded-xl border border-neutral-800 bg-neutral-950 p-3 font-mono text-xs leading-relaxed shadow-inner"
      >
        {err && <div className="text-red-400">{r?.error ?? "could not read the engine log"}</div>}
        {lines.map((l, i) => (
          <div key={i} className={`whitespace-pre-wrap break-all ${lineClass(l)}`}>{l}</div>
        ))}
      </div>
      <p className="mt-2 text-xs text-neutral-400 truncate">newest: <span className="font-mono">{newest}</span></p>
    </div>
  );
}