refactor: decompose usageDb, handleSingleModelChat, UI components (T-15, T-28, T-29)

T-15 — Decompose usageDb.js (969→40 lines):
- Extract src/lib/usage/migrations.js (legacy + JSON→SQLite migration)
- Extract src/lib/usage/usageHistory.js (tracking, pending, log.txt)
- Extract src/lib/usage/costCalculator.js (pure cost calculation)
- Extract src/lib/usage/usageStats.js (dashboard aggregation)
- Extract src/lib/usage/callLogs.js (structured logs, CRUD, rotation)
- usageDb.js is now a thin facade re-exporting all functions

T-28 — Decompose handleSingleModelChat (183→80 lines):
- Extract handleNoCredentials() — credential error responses
- Extract safeResolveProxy() — proxy resolution with error handling
- Extract safeLogEvents() — fire-and-forget proxy + translation logging
- Also created chatHelpers.js with standalone helper exports

T-29 — Extract shared UI primitives (3230 total lines):
- FilterBar.js — search input + filter chips dropdown
- ColumnToggle.js — table column visibility toggle
- DataTable.js — generic data table with sticky header, loading/empty

Tests: 88/88 pass (no regressions)
This commit is contained in:
diegosouzapw
2026-02-14 18:53:14 -03:00
parent 967689d0a1
commit 492afc4ff1
22 changed files with 3552 additions and 1058 deletions

View File

@@ -0,0 +1,100 @@
"use client";
/**
* ColumnToggle — Shared UI primitive (T-29)
*
* Dropdown menu for toggling table column visibility.
* Used by RequestLoggerV2, ProxyLogger, etc.
*
* Usage:
* <ColumnToggle
* columns={[{ key: 'model', label: 'Model' }, ...]}
* visible={{ model: true, provider: false, ... }}
* onToggle={(key) => setVisible({...visible, [key]: !visible[key]})}
* />
*/
import { useState, useRef, useEffect } from "react";
export default function ColumnToggle({ columns = [], visible = {}, onToggle }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
// Close on outside click
useEffect(() => {
if (!open) return;
const handler = (e) => {
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [open]);
return (
<div ref={ref} style={{ position: "relative" }}>
<button
onClick={() => setOpen(!open)}
title="Toggle columns"
style={{
padding: "6px 10px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.1)",
background: "rgba(255,255,255,0.05)",
color: "var(--text-secondary, #888)",
fontSize: "13px",
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "4px",
}}
>
<span style={{ fontSize: "14px" }}></span>
Columns
</button>
{open && (
<div
style={{
position: "absolute",
top: "100%",
right: 0,
marginTop: "4px",
background: "rgba(20,20,30,0.95)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: "8px",
padding: "8px",
zIndex: 50,
minWidth: "160px",
backdropFilter: "blur(12px)",
}}
>
{columns.map((col) => (
<label
key={col.key}
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "4px 8px",
cursor: "pointer",
fontSize: "12px",
color: visible[col.key]
? "var(--text-primary, #e0e0e0)"
: "var(--text-secondary, #888)",
borderRadius: "4px",
}}
>
<input
type="checkbox"
checked={visible[col.key] ?? true}
onChange={() => onToggle(col.key)}
style={{ accentColor: "#6366f1" }}
/>
{col.label}
</label>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,157 @@
"use client";
/**
* DataTable — Shared UI primitive (T-29)
*
* Configurable data table with sticky header, row click,
* and optional loading/empty states. Extracts the shared
* table rendering pattern from RequestLoggerV2 and ProxyLogger.
*
* Usage:
* <DataTable
* columns={visibleColumns}
* data={filteredLogs}
* renderCell={(row, column) => <span>{row[column.key]}</span>}
* onRowClick={(row) => openDetail(row)}
* selectedId={selectedLog?.id}
* loading={isLoading}
* emptyIcon="📋"
* emptyMessage="No logs found"
* />
*/
export default function DataTable({
columns = [],
data = [],
renderCell,
renderHeader,
onRowClick,
selectedId,
loading = false,
maxHeight = "calc(100vh - 320px)",
emptyIcon = "📭",
emptyMessage = "No data found",
}) {
if (loading) {
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "48px 24px",
color: "var(--text-secondary, #888)",
fontSize: "14px",
}}
>
<span style={{ animation: "spin 1s linear infinite", marginRight: "8px" }}></span>
Loading...
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
</div>
);
}
if (data.length === 0) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "48px 24px",
color: "var(--text-secondary, #888)",
fontSize: "14px",
}}
>
<span style={{ fontSize: "32px", marginBottom: "8px", opacity: 0.6 }}>{emptyIcon}</span>
{emptyMessage}
</div>
);
}
return (
<div style={{ overflow: "auto", maxHeight, borderRadius: "8px" }}>
<table
style={{
width: "100%",
borderCollapse: "collapse",
fontSize: "12px",
tableLayout: "auto",
}}
>
<thead>
<tr>
{columns.map((col) => (
<th
key={col.key}
style={{
padding: "8px 10px",
textAlign: "left",
fontWeight: 600,
color: "var(--text-secondary, #888)",
borderBottom: "1px solid rgba(255,255,255,0.08)",
position: "sticky",
top: 0,
background: "var(--bg-table-header, rgba(15,15,25,0.95))",
zIndex: 1,
whiteSpace: "nowrap",
fontSize: "11px",
textTransform: "uppercase",
letterSpacing: "0.5px",
}}
>
{renderHeader ? renderHeader(col) : col.label}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, idx) => (
<tr
key={row.id || idx}
onClick={() => onRowClick?.(row)}
style={{
cursor: onRowClick ? "pointer" : "default",
background:
row.id === selectedId
? "rgba(99,102,241,0.1)"
: idx % 2 === 0
? "transparent"
: "rgba(255,255,255,0.02)",
transition: "background 0.15s",
}}
onMouseEnter={(e) => {
if (row.id !== selectedId) {
e.currentTarget.style.background = "rgba(255,255,255,0.04)";
}
}}
onMouseLeave={(e) => {
if (row.id !== selectedId) {
e.currentTarget.style.background =
idx % 2 === 0 ? "transparent" : "rgba(255,255,255,0.02)";
}
}}
>
{columns.map((col) => (
<td
key={col.key}
style={{
padding: "6px 10px",
borderBottom: "1px solid rgba(255,255,255,0.04)",
whiteSpace: "nowrap",
maxWidth: col.maxWidth || "200px",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{renderCell(row, col)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -0,0 +1,205 @@
"use client";
/**
* FilterBar — Shared UI primitive (T-29)
*
* Reusable filter bar with search input and optional filter chips.
* Used by RequestLoggerV2, ProxyLogger, and similar data tables.
*
* Usage:
* <FilterBar
* searchValue={search}
* onSearchChange={setSearch}
* placeholder="Search logs..."
* filters={[
* { key: 'status', label: 'Status', options: ['ok', 'error'] },
* { key: 'provider', label: 'Provider', options: ['openai', 'anthropic'] },
* ]}
* activeFilters={activeFilters}
* onFilterChange={(key, value) => setFilters({ ...filters, [key]: value })}
* />
*/
import { useState, useCallback } from "react";
export default function FilterBar({
searchValue = "",
onSearchChange,
placeholder = "Search...",
filters = [],
activeFilters = {},
onFilterChange,
children,
}) {
const [expandedFilter, setExpandedFilter] = useState(null);
const handleClear = useCallback(() => {
onSearchChange("");
filters.forEach((f) => onFilterChange(f.key, ""));
setExpandedFilter(null);
}, [onSearchChange, filters, onFilterChange]);
const hasActiveFilters =
searchValue || Object.values(activeFilters).some((v) => v && v !== "");
return (
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "8px",
alignItems: "center",
padding: "8px 0",
}}
>
{/* Search input */}
<div style={{ position: "relative", flex: "1 1 200px", minWidth: "200px" }}>
<input
type="text"
value={searchValue}
onChange={(e) => onSearchChange(e.target.value)}
placeholder={placeholder}
style={{
width: "100%",
padding: "8px 12px 8px 32px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.1)",
background: "rgba(255,255,255,0.05)",
color: "var(--text-primary, #e0e0e0)",
fontSize: "13px",
outline: "none",
}}
/>
<span
style={{
position: "absolute",
left: "10px",
top: "50%",
transform: "translateY(-50%)",
opacity: 0.4,
fontSize: "14px",
pointerEvents: "none",
}}
>
🔍
</span>
</div>
{/* Filter chips */}
{filters.map((filter) => (
<div key={filter.key} style={{ position: "relative" }}>
<button
onClick={() =>
setExpandedFilter(expandedFilter === filter.key ? null : filter.key)
}
style={{
padding: "6px 12px",
borderRadius: "6px",
border: `1px solid ${activeFilters[filter.key] ? "rgba(99,102,241,0.5)" : "rgba(255,255,255,0.1)"}`,
background: activeFilters[filter.key]
? "rgba(99,102,241,0.15)"
: "rgba(255,255,255,0.05)",
color: activeFilters[filter.key]
? "#818cf8"
: "var(--text-secondary, #888)",
fontSize: "12px",
cursor: "pointer",
whiteSpace: "nowrap",
}}
>
{filter.label}
{activeFilters[filter.key] ? ` · ${activeFilters[filter.key]}` : ""}
</button>
{expandedFilter === filter.key && (
<div
style={{
position: "absolute",
top: "100%",
left: 0,
marginTop: "4px",
background: "rgba(20,20,30,0.95)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: "8px",
padding: "4px",
zIndex: 50,
minWidth: "120px",
backdropFilter: "blur(12px)",
}}
>
<button
onClick={() => {
onFilterChange(filter.key, "");
setExpandedFilter(null);
}}
style={{
display: "block",
width: "100%",
padding: "6px 12px",
textAlign: "left",
background: "none",
border: "none",
color: "#888",
fontSize: "12px",
cursor: "pointer",
borderRadius: "4px",
}}
>
All
</button>
{(filter.options || []).map((opt) => (
<button
key={opt}
onClick={() => {
onFilterChange(filter.key, opt);
setExpandedFilter(null);
}}
style={{
display: "block",
width: "100%",
padding: "6px 12px",
textAlign: "left",
background:
activeFilters[filter.key] === opt
? "rgba(99,102,241,0.2)"
: "none",
border: "none",
color:
activeFilters[filter.key] === opt
? "#818cf8"
: "var(--text-primary, #e0e0e0)",
fontSize: "12px",
cursor: "pointer",
borderRadius: "4px",
}}
>
{opt}
</button>
))}
</div>
)}
</div>
))}
{/* Clear all */}
{hasActiveFilters && (
<button
onClick={handleClear}
style={{
padding: "6px 12px",
borderRadius: "6px",
border: "1px solid rgba(239,68,68,0.3)",
background: "rgba(239,68,68,0.1)",
color: "#ef4444",
fontSize: "12px",
cursor: "pointer",
}}
>
Clear
</button>
)}
{/* Extra controls (e.g. refresh button) */}
{children}
</div>
);
}