← back to Omega Watches 2

src/app/(dashboard)/snapshots/page.tsx

212 lines

"use client";

import { useEffect, useState } from "react";
import { formatDateTime } from "@/lib/utils";

function formatBytes(bytes: number): string {
  if (!bytes) return "0 B";
  const sizes = ["B", "KB", "MB", "GB"];
  const i = Math.floor(Math.log(bytes) / Math.log(1024));
  return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
}

export default function RawSnapshotsPage() {
  const [snapshots, setSnapshots] = useState<any[]>([]);
  const [stats, setStats] = useState<any>(null);
  const [bySource, setBySource] = useState<any[]>([]);
  const [sourceFilter, setSourceFilter] = useState("");
  const [error, setError] = useState("");

  useEffect(() => {
    loadData();
  }, [sourceFilter]);

  async function loadData() {
    try {
      const token = localStorage.getItem("omega_token");
      const qs = sourceFilter ? `?source=${sourceFilter}` : "";
      const res = await fetch(`/api/snapshots${qs}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      const json = await res.json();
      if (json.success) {
        setSnapshots(json.data.snapshots);
        setStats(json.data.stats);
        setBySource(json.data.by_source);
      }
    } catch (e: any) {
      setError(e.message);
    }
  }

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-xl font-bold text-gray-100">Raw Snapshot Archive</h1>
        <div className="flex gap-2">
          <select
            className="input w-48"
            value={sourceFilter}
            onChange={(e) => setSourceFilter(e.target.value)}
          >
            <option value="">All Sources</option>
            {bySource.map((s) => (
              <option key={s.source_name} value={s.source_name}>
                {s.source_name} ({s.count})
              </option>
            ))}
          </select>
          <button onClick={loadData} className="btn-secondary text-xs">
            Refresh
          </button>
        </div>
      </div>

      {error && <div className="rounded bg-red-900/30 p-3 text-red-300">{error}</div>}

      {/* Stats */}
      {stats && (
        <div className="grid gap-4 md:grid-cols-4">
          <div className="card border-navy-700 text-center">
            <div className="text-3xl font-bold text-gray-200">{stats.total}</div>
            <div className="text-xs text-gray-500">Total Snapshots</div>
          </div>
          <div className="card border-navy-700 text-center">
            <div className="text-3xl font-bold text-blue-400">{stats.sources}</div>
            <div className="text-xs text-gray-500">Sources</div>
          </div>
          <div className="card border-navy-700 text-center">
            <div className="text-3xl font-bold text-omega-400">
              {formatBytes(parseInt(stats.total_bytes) || 0)}
            </div>
            <div className="text-xs text-gray-500">Total Storage</div>
          </div>
          <div className="card border-navy-700 text-center">
            <div className="text-sm font-medium text-gray-200">
              {stats.last_snapshot ? formatDateTime(stats.last_snapshot) : "Never"}
            </div>
            <div className="text-xs text-gray-500">Last Snapshot</div>
          </div>
        </div>
      )}

      {/* Per-source breakdown */}
      {bySource.length > 0 && (
        <div className="card border-navy-700">
          <h2 className="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400">
            Storage by Source
          </h2>
          <div className="grid gap-2 md:grid-cols-4">
            {bySource.map((s) => (
              <div
                key={s.source_name}
                onClick={() => setSourceFilter(s.source_name === sourceFilter ? "" : s.source_name)}
                className={`cursor-pointer rounded bg-navy-900 p-3 transition-colors hover:bg-navy-800 ${
                  sourceFilter === s.source_name ? "ring-1 ring-omega-500" : ""
                }`}
              >
                <div className="text-sm font-medium text-gray-200">{s.source_name}</div>
                <div className="mt-1 flex items-center justify-between text-xs text-gray-500">
                  <span>{s.count} snapshots</span>
                  <span>{formatBytes(parseInt(s.total_bytes) || 0)}</span>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Snapshot table */}
      <div className="card border-navy-700 p-0">
        <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="p-3">Source</th>
                <th className="p-3">URL</th>
                <th className="p-3">Content</th>
                <th className="p-3 text-right">Size</th>
                <th className="p-3">Hash</th>
                <th className="p-3">Parser</th>
                <th className="p-3">Captured</th>
              </tr>
            </thead>
            <tbody>
              {snapshots.length === 0 ? (
                <tr>
                  <td colSpan={7} className="p-8 text-center text-gray-500">
                    {stats?.total === "0" || stats?.total === 0
                      ? "No snapshots archived yet. Snapshots are created automatically by collectors during runs."
                      : "No snapshots match the current filter."}
                  </td>
                </tr>
              ) : (
                snapshots.map((s) => (
                  <tr key={s.snapshot_id} className="border-b border-navy-800 hover:bg-navy-900">
                    <td className="p-3 text-gray-200">{s.source_name}</td>
                    <td className="p-3 max-w-xs">
                      <div className="truncate text-xs text-blue-400" title={s.url}>
                        {s.url}
                      </div>
                    </td>
                    <td className="p-3">
                      <span className={`badge text-xs ${s.content_type?.includes("html") ? "badge-info" : "badge-warning"}`}>
                        {s.content_type || "unknown"}
                      </span>
                    </td>
                    <td className="p-3 text-right text-gray-400">
                      {formatBytes(s.content_size_bytes || 0)}
                    </td>
                    <td className="p-3 font-mono text-xs text-gray-500">
                      {s.content_hash?.substring(0, 12)}...
                    </td>
                    <td className="p-3 font-mono text-xs text-gray-500">{s.parser_version}</td>
                    <td className="p-3 text-xs text-gray-500">
                      {s.scraped_at ? formatDateTime(s.scraped_at) : "-"}
                    </td>
                  </tr>
                ))
              )}
            </tbody>
          </table>
        </div>
      </div>

      {/* Schema reference */}
      <div className="card border-navy-700">
        <h2 className="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400">
          Snapshot Schema Reference
        </h2>
        <div className="overflow-x-auto">
          <table className="w-full text-xs">
            <thead>
              <tr className="border-b border-navy-700 text-gray-500">
                <th className="p-2 text-left">Column</th>
                <th className="p-2 text-left">Type</th>
                <th className="p-2 text-left">Purpose</th>
              </tr>
            </thead>
            <tbody className="text-gray-400">
              {[
                ["source_name", "TEXT", "Which source this snapshot is from"],
                ["url", "TEXT", "Original URL captured"],
                ["content_hash", "SHA256", "Content dedup/change detection"],
                ["content_type", "TEXT", "text/html or application/pdf"],
                ["content_size_bytes", "INTEGER", "Size of archived content"],
                ["parser_version", "TEXT", "Parser version at capture time"],
                ["scraped_at", "TIMESTAMPTZ", "When the snapshot was taken"],
              ].map(([col, type, purpose]) => (
                <tr key={col} className="border-b border-navy-800">
                  <td className="p-2 font-mono text-omega-400">{col}</td>
                  <td className="p-2">{type}</td>
                  <td className="p-2">{purpose}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}