← 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>
);
}