import { useState, useEffect, useCallback, useRef } from "react"; import type { ActionReceipt } from "./types"; import { useStats, useReceipts } from "./hooks"; import { cn } from "./utils"; import Header from "./components/Header"; import Sidebar from "./components/Sidebar"; import StatsCards from "./components/StatsCards"; import FilterControls from "./components/FilterControls"; import ReceiptList from "./components/ReceiptList"; import ReceiptDetail from "./components/ReceiptDetail"; import SessionView from "./components/SessionView"; const LIMIT = 50; function setSearchParams(params: Record) { const url = new URL(window.location.href); for (const [k, v] of Object.entries(params)) { if (v) url.searchParams.set(k, v); else url.searchParams.delete(k); } window.history.replaceState({}, "", url.toString()); } export default function App() { const initialParams = useRef(new URLSearchParams(window.location.search)).current; const [selectedTiers, setSelectedTiers] = useState(() => { const tiers = initialParams.get("tiers"); return tiers ? tiers.split(",").filter(Boolean) : []; }); const [anomalyOnly, setAnomalyOnly] = useState(() => initialParams.get("anomalies") === "1"); const [allReceipts, setAllReceipts] = useState([]); const [selectedReceipt, setSelectedReceipt] = useState(null); const [viewMode, setViewMode] = useState<"list" | "sessions">(() => { const view = initialParams.get("view"); return view === "sessions" ? "sessions" : "list"; }); const [hasMore, setHasMore] = useState(false); const [loadedCount, setLoadedCount] = useState(0); const [loadingMore, setLoadingMore] = useState(false); const [loadMoreError, setLoadMoreError] = useState(null); const initialLoadDone = useRef(false); const { stats, isLoading: statsLoading, error: statsError } = useStats(); const { receipts, isLoading: receiptsLoading, error: receiptsError } = useReceipts(LIMIT, 0); function updateViewMode(v: "list" | "sessions") { setViewMode(v); setSearchParams({ view: v === "list" ? "" : v }); } function updateSelectedTiers(tiers: string[]) { setSelectedTiers(tiers); setSearchParams({ tiers: tiers.join(",") }); } function updateAnomalyOnly(v: boolean) { setAnomalyOnly(v); setSearchParams({ anomalies: v ? "1" : "" }); } // Polling: merge new receipts at the top, do NOT touch loadedCount or hasMore useEffect(() => { if (receipts) { setAllReceipts((prev) => { const ids = new Set(prev.map((r) => r.id)); const newOnes = receipts.filter((r) => !ids.has(r.id)); if (newOnes.length === 0) return prev; return [...newOnes, ...prev]; }); } }, [receipts]); // Initial load: set hasMore and loadedCount once useEffect(() => { if (receipts && !initialLoadDone.current) { initialLoadDone.current = true; setLoadedCount(receipts.length); setHasMore(receipts.length === LIMIT); } }, [receipts]); const handleLoadMore = useCallback(() => { if (loadingMore) return; setLoadingMore(true); setLoadMoreError(null); fetch(`/boltbot/receipts?limit=${LIMIT}&offset=${loadedCount}`) .then((r) => { if (!r.ok) throw new Error(r.statusText); return r.json(); }) .then((data: { receipts: ActionReceipt[] }) => { setAllReceipts((prev) => { const ids = new Set(prev.map((r) => r.id)); const newOnes = data.receipts.filter((r) => !ids.has(r.id)); return [...prev, ...newOnes]; }); setHasMore(data.receipts.length === LIMIT); setLoadedCount((prev) => prev + data.receipts.length); }) .catch((err) => { setLoadMoreError(err instanceof Error ? err.message : "Failed to load more"); }) .finally(() => { setLoadingMore(false); }); }, [loadedCount, loadingMore]); const filtered = allReceipts.filter((r) => { if (selectedTiers.length > 0 && !selectedTiers.includes(r.tier)) return false; if (anomalyOnly && r.anomalies.length === 0) return false; return true; }); return (
Skip to main content
{viewMode === "list" ? ( ) : ( )}
setSelectedReceipt(null)} />
); }