← back to Omega Watches 2

src/app/(dashboard)/page.tsx

429 lines

"use client";

import { useEffect, useState } from "react";
import { api } from "@/lib/api-client";
import { formatDateTime, formatCurrency } from "@/lib/utils";
import { CollectionPriceChart, MarketActivityChart, TopMoversChart, SourceHealthDonut } from "@/components/charts";

interface HealthData {
  status: string;
  version: string;
  uptime_seconds: number;
  database: { ok: boolean; latency: number; connections: { total: number; idle: number; waiting: number } };
  timestamp: string;
}

function StatCard({ label, value, sub, color = "text-gray-100" }: { label: string; value: string | number; sub?: string; color?: string }) {
  return (
    <div className="card border-navy-700">
      <div className="text-xs uppercase tracking-wider text-gray-500">{label}</div>
      <div className={`mt-1 text-2xl font-bold ${color}`}>{value}</div>
      {sub && <div className="mt-1 text-xs text-gray-500">{sub}</div>}
    </div>
  );
}

export default function SystemHealthPage() {
  const [health, setHealth] = useState<HealthData | null>(null);
  const [sources, setSources] = useState<any[]>([]);
  const [summary, setSummary] = useState<any>(null);
  const [dashboard, setDashboard] = useState<any>(null);
  const [chartData, setChartData] = useState<any>(null);
  const [refreshing, setRefreshing] = useState(false);
  const [error, setError] = useState("");

  useEffect(() => {
    async function load() {
      try {
        const token = localStorage.getItem("omega_token");
        const headers = { Authorization: `Bearer ${token}` };

        const [h, s, refRes, dashRes, chartRes] = await Promise.all([
          api.health(),
          api.getSources(),
          fetch("/api/references", { headers }).then((r) => r.json()),
          fetch("/api/dashboard", { headers }).then((r) => r.json()),
          api.getDashboardChartData().catch(() => null),
        ]);

        setHealth(h);
        setSources(s);
        if (refRes.success) setSummary(refRes.summary);
        if (dashRes.success) setDashboard(dashRes.data);
        if (chartRes) setChartData(chartRes);
      } catch (e: any) {
        setError(e.message);
      }
    }
    load();
    const interval = setInterval(load, 30000);
    return () => clearInterval(interval);
  }, []);

  async function refreshViews() {
    setRefreshing(true);
    try {
      const token = localStorage.getItem("omega_token");
      await fetch("/api/admin/refresh-views", {
        method: "POST",
        headers: { Authorization: `Bearer ${token}` },
      });
    } catch {}
    setRefreshing(false);
  }

  if (error) {
    return <div className="rounded bg-red-900/30 p-4 text-red-300">{error}</div>;
  }

  const uptimeMin = health ? Math.round(health.uptime_seconds / 60) : 0;
  const uptimeStr = uptimeMin >= 60 ? `${Math.floor(uptimeMin / 60)}h ${uptimeMin % 60}m` : `${uptimeMin}m`;
  const activeSources = sources.filter((s: any) => s.is_active).length;
  const totalRuns = sources.reduce((acc: number, s: any) => acc + (parseInt(s.total_runs) || 0), 0);
  const failedRuns = sources.reduce((acc: number, s: any) => acc + (parseInt(s.failed_runs) || 0), 0);
  const totalRecords = sources.reduce((acc: number, s: any) => acc + (parseInt(s.total_records_inserted) || 0), 0);
  const s24 = dashboard?.stats_24h;

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-xl font-bold text-gray-100">Omega Watches 2.0 — Control Plane</h1>
        <div className="flex items-center gap-3">
          <button
            onClick={refreshViews}
            disabled={refreshing}
            className="btn-secondary text-xs disabled:opacity-50"
          >
            {refreshing ? "Refreshing..." : "Refresh Views"}
          </button>
          <span className={`badge ${health?.status === "healthy" ? "badge-success" : "badge-warning"}`}>
            {health?.status || "loading..."}
          </span>
        </div>
      </div>

      {/* Primary KPIs */}
      <div className="grid grid-cols-2 gap-4 md:grid-cols-5">
        <StatCard
          label="Watch References"
          value={summary?.total_references || "—"}
          sub="Across all collections"
          color="text-omega-400"
        />
        <StatCard
          label="Market Events"
          value={summary?.total_events || "—"}
          sub="Auctions, sales, asks"
          color="text-blue-400"
        />
        <StatCard
          label="MSRP Snapshots"
          value={summary?.total_snapshots || "—"}
          sub="Daily price observations"
          color="text-green-400"
        />
        <StatCard
          label="Active Sources"
          value={activeSources}
          sub={`${sources.length} total configured`}
        />
        <StatCard
          label="Collector Runs"
          value={totalRuns}
          sub={failedRuns > 0 ? `${failedRuns} failed` : "All healthy"}
          color={failedRuns > 0 ? "text-yellow-400" : "text-gray-100"}
        />
      </div>

      {/* 24-Hour Stats + System Health */}
      <div className="grid gap-4 md:grid-cols-4">
        <StatCard
          label="Database"
          value={health?.database.ok ? "Connected" : "Down"}
          sub={health ? `${health.database.latency}ms latency` : ""}
          color={health?.database.ok ? "text-green-400" : "text-red-400"}
        />
        <StatCard label="Uptime" value={uptimeStr} sub={health ? formatDateTime(health.timestamp) : ""} />
        <StatCard
          label="24h Inserted"
          value={s24?.inserted_24h || 0}
          sub={s24 ? `${s24.runs_24h} runs (${s24.failed_24h} failed)` : ""}
          color="text-blue-400"
        />
        <StatCard
          label="24h Quarantined"
          value={s24?.quarantined_24h || 0}
          sub="Flagged for review"
          color={parseInt(s24?.quarantined_24h || "0") > 0 ? "text-yellow-400" : "text-green-400"}
        />
      </div>

      {/* Charts Grid */}
      <div className="grid gap-4 md:grid-cols-2">
        <CollectionPriceChart
          data={(dashboard?.collection_summary || [])
            .filter((c: any) => c.avg_market_price)
            .map((c: any) => ({
              collection: c.collection,
              avg: parseFloat(c.avg_market_price),
              min: parseFloat(c.min_price),
              max: parseFloat(c.max_price),
              count: parseInt(c.event_count),
            }))}
          title="Collection Price Comparison"
        />
        <MarketActivityChart
          data={chartData?.daily_activity || []}
          title="Market Activity (30 Days)"
        />
        <TopMoversChart
          data={(dashboard?.top_movers || []).map((m: any) => ({
            label: m.reference_number,
            collection: m.collection,
            model: m.model_name,
            stddev: parseFloat(m.price_stddev) || 0,
            avg_price: parseFloat(m.avg_price),
            events: parseInt(m.events_30d),
          }))}
          title="Top Price Movers (30d Volatility)"
        />
        <SourceHealthDonut
          data={chartData?.source_health || []}
          title="Source Health Overview"
        />
      </div>

      {/* Collection Price Summary + Recent Activity */}
      <div className="grid gap-4 md:grid-cols-2">
        {/* Collection Summary */}
        <div className="card border-navy-700">
          <h2 className="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400">
            Collection Price Summary
          </h2>
          <div className="space-y-2">
            {(dashboard?.collection_summary || []).map((c: any) => (
              <div key={c.collection} className="flex items-center justify-between rounded bg-navy-900 p-3">
                <div>
                  <div className="text-sm font-medium text-gray-200">{c.collection}</div>
                  <div className="text-xs text-gray-500">
                    {c.ref_count} refs · {c.event_count} events
                  </div>
                </div>
                <div className="text-right">
                  {c.avg_market_price ? (
                    <>
                      <div className="text-sm font-bold text-omega-400">
                        {formatCurrency(parseFloat(c.avg_market_price))}
                      </div>
                      <div className="text-xs text-gray-500">
                        {formatCurrency(parseFloat(c.min_price))} – {formatCurrency(parseFloat(c.max_price))}
                      </div>
                    </>
                  ) : (
                    <span className="text-xs text-gray-500">No market data</span>
                  )}
                </div>
              </div>
            ))}
            {(!dashboard?.collection_summary || dashboard.collection_summary.length === 0) && (
              <div className="p-4 text-center text-sm text-gray-500">No collection data yet</div>
            )}
          </div>
        </div>

        {/* Recent Activity */}
        <div className="card border-navy-700">
          <h2 className="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400">
            Recent Collector Runs
          </h2>
          <div className="space-y-2">
            {(dashboard?.recent_runs || []).slice(0, 8).map((r: any) => (
              <div key={r.run_id} className="flex items-center justify-between rounded bg-navy-900 p-2">
                <div className="flex items-center gap-2">
                  <span className={`badge text-xs ${
                    r.status === "completed" ? "badge-success" :
                    r.status === "running" ? "badge-info" :
                    r.status === "failed" ? "badge-error" : "badge-warning"
                  }`}>
                    {r.status}
                  </span>
                  <div>
                    <span className="text-sm text-gray-200">{r.source_name}</span>
                    <span className="ml-2 text-xs text-gray-500">{r.job_type}</span>
                  </div>
                </div>
                <div className="text-right">
                  <div className="text-xs text-gray-400">
                    +{r.records_inserted || 0} records
                    {r.records_quarantined > 0 && (
                      <span className="ml-1 text-yellow-400">({r.records_quarantined} flagged)</span>
                    )}
                  </div>
                  <div className="text-xs text-gray-600">
                    {r.started_at ? formatDateTime(r.started_at) : "—"}
                  </div>
                </div>
              </div>
            ))}
            {(!dashboard?.recent_runs || dashboard.recent_runs.length === 0) && (
              <div className="p-4 text-center text-sm text-gray-500">No runs yet</div>
            )}
          </div>
        </div>
      </div>

      {/* Top Movers + Quarantine Alerts */}
      <div className="grid gap-4 md:grid-cols-2">
        {/* Top Movers */}
        <div className="card border-navy-700">
          <h2 className="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400">
            Top Price Movers (30d)
          </h2>
          <div className="space-y-2">
            {(dashboard?.top_movers || []).map((m: any) => (
              <div key={m.reference_number} className="flex items-center justify-between rounded bg-navy-900 p-3">
                <div>
                  <div className="font-mono text-sm text-omega-400">{m.reference_number}</div>
                  <div className="text-xs text-gray-500">{m.collection} — {m.model_name}</div>
                </div>
                <div className="text-right">
                  <div className="text-sm text-gray-200">{formatCurrency(parseFloat(m.avg_price))}</div>
                  <div className="text-xs text-gray-500">
                    {m.events_30d} events · {m.price_stddev ? `\u00B1${formatCurrency(parseFloat(m.price_stddev))}` : "—"}
                  </div>
                </div>
              </div>
            ))}
            {(!dashboard?.top_movers || dashboard.top_movers.length === 0) && (
              <div className="p-4 text-center text-sm text-gray-500">Not enough data for variance analysis</div>
            )}
          </div>
        </div>

        {/* Quarantine Alerts */}
        <div className="card border-navy-700">
          <h2 className="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400">
            Quarantine Alerts
          </h2>
          <div className="space-y-2">
            {(dashboard?.recent_alerts || []).map((a: any) => (
              <div key={a.id} className="flex items-center justify-between rounded bg-navy-900 p-3">
                <div className="flex items-center gap-2">
                  <span className={`badge text-xs ${
                    a.severity === "critical" ? "badge-error" :
                    a.severity === "high" ? "badge-warning" : "badge-info"
                  }`}>
                    {a.severity}
                  </span>
                  <div>
                    <div className="text-sm text-gray-200">{a.field_name}</div>
                    <div className="text-xs text-gray-500">
                      {a.reference_number_raw} · {a.source_name}
                    </div>
                  </div>
                </div>
                <div className="text-xs text-gray-600">
                  {a.created_at ? formatDateTime(a.created_at) : "—"}
                </div>
              </div>
            ))}
            {(!dashboard?.recent_alerts || dashboard.recent_alerts.length === 0) && (
              <div className="p-4 text-center text-sm text-gray-500">No unresolved alerts</div>
            )}
          </div>
        </div>
      </div>

      {/* Source Catalog */}
      <div className="card border-navy-700">
        <h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400">
          Source Catalog ({sources.length})
        </h2>
        <div className="overflow-x-auto">
          <table className="w-full text-sm">
            <thead>
              <tr className="border-b border-navy-700 text-left text-xs uppercase text-gray-500">
                <th className="pb-2 pr-4">Source</th>
                <th className="pb-2 pr-4">Type</th>
                <th className="pb-2 pr-4">Access</th>
                <th className="pb-2 pr-4">Reliability</th>
                <th className="pb-2 pr-4">Runs</th>
                <th className="pb-2 pr-4">Success</th>
                <th className="pb-2 pr-4">Records</th>
                <th className="pb-2 pr-4">ToS</th>
                <th className="pb-2">Status</th>
              </tr>
            </thead>
            <tbody>
              {sources.map((s: any) => (
                <tr key={s.id} className="border-b border-navy-800 hover:bg-navy-900">
                  <td className="py-2 pr-4 font-medium text-gray-200">{s.display_name}</td>
                  <td className="py-2 pr-4 text-gray-400">{s.source_type}</td>
                  <td className="py-2 pr-4">
                    <span className={`badge ${s.access_method === "api" ? "badge-success" : s.access_method === "manual" ? "badge-warning" : "badge-info"}`}>
                      {s.access_method}
                    </span>
                  </td>
                  <td className="py-2 pr-4">
                    <div className="flex gap-0.5">
                      {[1, 2, 3, 4, 5].map((n) => (
                        <span key={n} className={`inline-block h-2 w-2 rounded-full ${n <= s.reliability_score ? "bg-omega-400" : "bg-navy-700"}`} />
                      ))}
                    </div>
                  </td>
                  <td className="py-2 pr-4 text-gray-400">{s.total_runs || 0}</td>
                  <td className="py-2 pr-4">
                    <span className={parseInt(s.success_rate_pct) >= 90 ? "text-green-400" : parseInt(s.success_rate_pct) >= 70 ? "text-yellow-400" : "text-red-400"}>
                      {s.success_rate_pct || 0}%
                    </span>
                  </td>
                  <td className="py-2 pr-4 text-gray-400">{s.total_records_inserted || 0}</td>
                  <td className="py-2 pr-4">
                    {s.tos_reviewed ? (
                      s.tos_allows_collection === false ? (
                        <span className="badge-error">Blocked</span>
                      ) : (
                        <span className="badge-success">OK</span>
                      )
                    ) : (
                      <span className="badge-warning">Pending</span>
                    )}
                  </td>
                  <td className="py-2">
                    <span className={`badge ${s.is_active ? "badge-success" : "badge-error"}`}>
                      {s.is_active ? "Active" : "Disabled"}
                    </span>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>

      {/* Database Connections */}
      <div className="card border-navy-700">
        <h2 className="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400">
          Database Connections
        </h2>
        {health && (
          <div className="grid grid-cols-3 gap-4 text-center">
            <div>
              <div className="text-2xl font-bold text-gray-200">{health.database.connections.total}</div>
              <div className="text-xs text-gray-500">Total</div>
            </div>
            <div>
              <div className="text-2xl font-bold text-green-400">{health.database.connections.idle}</div>
              <div className="text-xs text-gray-500">Idle</div>
            </div>
            <div>
              <div className="text-2xl font-bold text-yellow-400">{health.database.connections.waiting}</div>
              <div className="text-xs text-gray-500">Waiting</div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}