diff --git a/extensions/boltbot/dashboard/index.html b/extensions/boltbot/dashboard/index.html index 894a99526..bb842f803 100644 --- a/extensions/boltbot/dashboard/index.html +++ b/extensions/boltbot/dashboard/index.html @@ -1,8 +1,9 @@ - + + Boltbot Dashboard diff --git a/extensions/boltbot/dashboard/src/App.tsx b/extensions/boltbot/dashboard/src/App.tsx index d8e3c805c..1e8528cf6 100644 --- a/extensions/boltbot/dashboard/src/App.tsx +++ b/extensions/boltbot/dashboard/src/App.tsx @@ -12,12 +12,32 @@ import SessionView from "./components/SessionView"; const LIMIT = 50; +function getSearchParams() { + return new URLSearchParams(window.location.search); +} + +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 [selectedTiers, setSelectedTiers] = useState([]); - const [anomalyOnly, setAnomalyOnly] = useState(false); + const initialParams = getSearchParams(); + 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">("list"); + 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); @@ -27,6 +47,19 @@ export default function App() { const { stats, isLoading: statsLoading, error: statsError } = useStats(); const { receipts, isLoading: receiptsLoading, error: receiptsError } = useReceipts(LIMIT, 0); + // Sync URL with state changes + useEffect(() => { + setSearchParams({ view: viewMode === "list" ? "" : viewMode }); + }, [viewMode]); + + useEffect(() => { + setSearchParams({ tiers: selectedTiers.join(",") }); + }, [selectedTiers]); + + useEffect(() => { + setSearchParams({ anomalies: anomalyOnly ? "1" : "" }); + }, [anomalyOnly]); + // Polling: merge new receipts at the top, do NOT touch loadedCount or hasMore useEffect(() => { if (receipts) { @@ -82,9 +115,12 @@ export default function App() { return (
+ + Skip to main content +
-
+
diff --git a/extensions/boltbot/dashboard/src/components/ReceiptDetail.tsx b/extensions/boltbot/dashboard/src/components/ReceiptDetail.tsx index 6e9f457bd..c87579fdb 100644 --- a/extensions/boltbot/dashboard/src/components/ReceiptDetail.tsx +++ b/extensions/boltbot/dashboard/src/components/ReceiptDetail.tsx @@ -124,7 +124,7 @@ export default function ReceiptDetail({ receipt, onClose }: Props) { role="dialog" aria-modal="true" aria-labelledby="receipt-detail-title" - className="fixed inset-y-0 right-0 w-[420px] max-w-full bg-neutral-900 border-l border-neutral-800 z-50 overflow-y-auto" + className="fixed inset-y-0 right-0 w-[420px] max-w-full bg-neutral-900 border-l border-neutral-800 z-50 overflow-y-auto [overscroll-behavior:contain]" >
@@ -167,7 +167,7 @@ export default function ReceiptDetail({ receipt, onClose }: Props) {
Duration
-
{formatDuration(receipt.durationMs)}
+
{formatDuration(receipt.durationMs)}
Session
diff --git a/extensions/boltbot/dashboard/src/components/ReceiptList.tsx b/extensions/boltbot/dashboard/src/components/ReceiptList.tsx index da0241b73..b681f5d8c 100644 --- a/extensions/boltbot/dashboard/src/components/ReceiptList.tsx +++ b/extensions/boltbot/dashboard/src/components/ReceiptList.tsx @@ -39,7 +39,7 @@ export default function ReceiptList({ if (isLoading && receipts.length === 0) { return ( -
+
{Array.from({ length: 5 }).map((_, i) => (
))} @@ -85,7 +85,7 @@ export default function ReceiptList({ : "hover:bg-neutral-800/30 border-l-2 border-transparent", )} > - {r.toolName} + {r.toolName} {r.tier} @@ -119,7 +119,7 @@ export default function ReceiptList({ loadingMore ? "opacity-50 cursor-not-allowed" : "hover:bg-neutral-700", )} > - {loadingMore ? "Loading..." : "Load More"} + {loadingMore ? "Loading…" : "Load More"}
)} diff --git a/extensions/boltbot/dashboard/src/components/SessionView.tsx b/extensions/boltbot/dashboard/src/components/SessionView.tsx index 78b528bf2..d7f637c82 100644 --- a/extensions/boltbot/dashboard/src/components/SessionView.tsx +++ b/extensions/boltbot/dashboard/src/components/SessionView.tsx @@ -77,10 +77,10 @@ function SessionCard({ )} aria-hidden="true" /> - + {group.sessionKey} - + {group.receipts.length} diff --git a/extensions/boltbot/dashboard/src/components/StatsCards.tsx b/extensions/boltbot/dashboard/src/components/StatsCards.tsx index 53348bbc6..36a546173 100644 --- a/extensions/boltbot/dashboard/src/components/StatsCards.tsx +++ b/extensions/boltbot/dashboard/src/components/StatsCards.tsx @@ -45,7 +45,7 @@ export default function StatsCards({ stats, isLoading, error }: Props) { {isLoading && !stats ? (
) : ( -
{getValue(stats, card.key)}
+
{getValue(stats, card.key)}
)}
))} diff --git a/extensions/boltbot/dashboard/src/index.css b/extensions/boltbot/dashboard/src/index.css index 28a5d1f23..0003da816 100644 --- a/extensions/boltbot/dashboard/src/index.css +++ b/extensions/boltbot/dashboard/src/index.css @@ -5,4 +5,14 @@ body { color: #f5f5f5; /* neutral-100 */ font-family: "Space Grotesk", sans-serif; margin: 0; + touch-action: manipulation; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } } diff --git a/extensions/boltbot/dashboard/src/utils.ts b/extensions/boltbot/dashboard/src/utils.ts index 2a830648e..a80a97916 100644 --- a/extensions/boltbot/dashboard/src/utils.ts +++ b/extensions/boltbot/dashboard/src/utils.ts @@ -8,27 +8,31 @@ export function formatRelativeTime(iso: string): string { const seconds = Math.floor(diffMs / 1000); if (seconds < 60) return "just now"; + const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); + const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes}m ago`; + if (minutes < 60) return rtf.format(-minutes, "minute"); const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours}h ago`; + if (hours < 24) return rtf.format(-hours, "hour"); const days = Math.floor(hours / 24); - if (days < 30) return `${days}d ago`; + if (days < 30) return rtf.format(-days, "day"); const months = Math.floor(days / 30); - if (months < 12) return `${months}mo ago`; + if (months < 12) return rtf.format(-months, "month"); const years = Math.floor(months / 12); - return `${years}y ago`; + return rtf.format(-years, "year"); } export function formatDuration(ms: number): string { + const nf = new Intl.NumberFormat("en", { maximumFractionDigits: 1 }); if (ms >= 1000) { - return `${(ms / 1000).toFixed(1)}s`; + return `${nf.format(ms / 1000)}s`; } - return `${ms}ms`; + const intFormatter = new Intl.NumberFormat("en", { maximumFractionDigits: 0 }); + return `${intFormatter.format(ms)}ms`; } export function cn(...classes: (string | false | undefined | null)[]): string {