feat(home): add Recent Requests panel + excludeTests allowlist fix (#10900)

Merged — reimplementation extracting the non-conflicting Recent Requests panel + excludeTests allowlist fix from #8450 (see PR body for the full scoping rationale, including why the topology UX rework was deliberately excluded — it contradicts the already-shipped #8428). typecheck/file-size/changelog/complexity/cognitive-complexity/i18n-coverage gates all clean, 2/2 unit + 1/1 vitest passing.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-20 18:26:02 -03:00
committed by GitHub
parent 7afafcecc9
commit e968d11b1c
7 changed files with 366 additions and 6 deletions

View File

@@ -0,0 +1 @@
- **feat(home):** add a live **Recent Requests** panel beside the home Provider Topology (polls `GET /api/usage/call-logs?excludeTests=1` every ~3s, gated by the topology appearance toggle + page visibility). `excludeTests` is now an allowlist of real provider inference (`/v1/%` or `/api/v1/%`), applied before `LIMIT`, so connection-test/model-sync/management rows can never leak into the feed ([#10897](https://github.com/diegosouzapw/OmniRoute/pull/10897), extracted from [#8450](https://github.com/diegosouzapw/OmniRoute/pull/8450)) — thanks @nguyenha935

View File

@@ -19,6 +19,7 @@ import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel";
import { useIsElectron, useOpenExternal } from "@/shared/hooks/useElectron";
import { HomeProviderTopologySection } from "./HomeProviderTopologySection";
import { shouldShowProviderTopologyOnHome } from "./homeAppearance";
import HomeRecentRequests from "../home/HomeRecentRequests";
type UpdateStep = {
step: string;
@@ -1126,12 +1127,15 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
)}
{showProviderTopologyOnHome && (
<HomeProviderTopologySection
providers={topologyProviders}
lastProvider={lastProvider}
errorProvider={errorProvider}
enabled={showProviderTopologyOnHome}
/>
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-3">
<HomeProviderTopologySection
providers={topologyProviders}
lastProvider={lastProvider}
errorProvider={errorProvider}
enabled={showProviderTopologyOnHome}
/>
<HomeRecentRequests enabled={showProviderTopologyOnHome} />
</div>
)}
{/* Provider Models Modal */}

View File

@@ -0,0 +1,207 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { Card } from "@/shared/components";
import { fmtCompact } from "@/shared/utils/formatting";
/**
* Home-page "Recent Requests" panel — the live request feed that sits beside the
* Provider Topology graph (parity with 9Router's Usage view).
*
* ## Data source
* Fed by POLLING `GET /api/usage/call-logs?limit=N` every ~3s, NOT by the live
* WebSocket. The WS is used elsewhere for the in-flight beam (it emits
* `request.started` and, since the stuck-latch fix, `request.completed`/`request.failed`),
* but its payload carries no tokens/latency/status and no persisted history — so it
* can't back a "recent requests" table on its own. The call-logs endpoint merges the
* in-memory active/completed entries with persisted rows and returns them newest-first
* (active on top), which is exactly this feed.
*
* The poll is gated by `enabled` (the same `showProviderTopologyOnHome` flag that
* gates the topology section) AND page visibility, so a backgrounded tab pauses.
*/
const POLL_INTERVAL_MS = 3000;
// Rows shown after client-side filtering of connection-test rows.
const RECENT_LIMIT = 20;
// Fetch a wider window than we display so filtering out connection-test rows
// (a burst of "Test connection" clicks) can't starve the feed below RECENT_LIMIT.
const FETCH_LIMIT = 60;
type CallLogRow = {
id?: string;
timestamp?: string;
status?: number;
model?: string;
provider?: string;
providerDisplay?: string | null;
path?: string;
sourceFormat?: string | null;
targetFormat?: string | null;
tokens?: { in?: number; out?: number };
error?: string | null;
active?: boolean;
completed?: boolean;
};
/**
* Connection tests write real `call_logs` rows (provider "Test connection" button →
* `/api/providers/[id]/test`) with fixed markers: model `connection-test`, path
* `/api/providers/test`, sourceFormat/targetFormat `test`. Those are health probes,
* not user traffic, so they must not clutter the Recent Requests feed (matching how
* 9Router keeps its Usage list to real calls). Drop any row carrying a test marker.
*/
function isConnectionTestRow(row: CallLogRow): boolean {
return (
row.model === "connection-test" ||
row.sourceFormat === "test" ||
row.targetFormat === "test" ||
row.path === "/api/providers/test"
);
}
type RequestState = "active" | "error" | "ok";
function requestState(row: CallLogRow): RequestState {
if (row.active || row.status === 0) return "active";
if (row.error || (typeof row.status === "number" && row.status >= 400)) return "error";
return "ok";
}
function timeAgo(timestamp: string | undefined, nowMs: number): string {
if (!timestamp) return "";
const then = Date.parse(timestamp);
if (!Number.isFinite(then)) return "";
const diff = Math.max(0, Math.floor((nowMs - then) / 1000));
if (diff < 60) return `${diff}s`;
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
return `${Math.floor(diff / 86400)}d`;
}
const STATE_DOT: Record<RequestState, string> = {
active: "bg-primary animate-pulse",
error: "bg-red-500",
ok: "bg-green-500",
};
export default function HomeRecentRequests({ enabled = true }: { enabled?: boolean }) {
const t = useTranslations("home");
const [rows, setRows] = useState<CallLogRow[]>([]);
const [loaded, setLoaded] = useState(false);
// A ticking clock so the relative "When" column updates without re-fetching.
const [nowMs, setNowMs] = useState(() => Date.now());
useEffect(() => {
if (!enabled) return;
const id = setInterval(() => setNowMs(Date.now()), 1000);
return () => clearInterval(id);
}, [enabled]);
const load = useCallback(async (signal: AbortSignal) => {
try {
const res = await fetch(`/api/usage/call-logs?limit=${FETCH_LIMIT}&excludeTests=1`, {
cache: "no-store",
signal,
});
if (!res.ok) return;
const data = await res.json();
if (signal.aborted) return;
const filtered = Array.isArray(data)
? (data as CallLogRow[]).filter((row) => !isConnectionTestRow(row)).slice(0, RECENT_LIMIT)
: [];
setRows(filtered);
setLoaded(true);
} catch (error) {
const isAbort = error instanceof DOMException && error.name === "AbortError";
if (!isAbort) console.error("Failed to load recent requests:", error);
}
}, []);
useEffect(() => {
if (!enabled) return;
let cancelled = false;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let controller: AbortController | null = null;
const tick = async () => {
// Pause polling while the tab is backgrounded; resume on next tick.
if (document.visibilityState === "visible") {
const currentController = new AbortController();
controller = currentController;
await load(currentController.signal);
if (controller === currentController) controller = null;
}
if (!cancelled) timeoutId = setTimeout(tick, POLL_INTERVAL_MS);
};
tick();
return () => {
cancelled = true;
if (timeoutId) clearTimeout(timeoutId);
controller?.abort();
};
}, [enabled, load]);
return (
<Card padding="sm" className="flex min-w-0 flex-col overflow-hidden h-[300px] sm:h-[420px]">
<div className="pb-2 mb-1 border-b border-border shrink-0">
<span className="text-xs font-semibold uppercase tracking-wide text-text-muted">
{t("recentRequests")}
</span>
</div>
{loaded && rows.length === 0 ? (
<div className="flex-1 flex items-center justify-center text-sm text-text-muted">
{t("recentRequestsEmpty")}
</div>
) : (
<div className="flex-1 overflow-y-auto -mx-1 px-1">
<table className="w-full min-w-0 border-collapse text-xs">
<thead className="sticky top-0 z-10 bg-surface">
<tr className="border-b border-border text-text-muted">
<th className="w-2 py-1.5" />
<th className="py-1.5 text-left font-semibold">{t("recentRequestsModel")}</th>
<th className="py-1.5 text-right font-semibold whitespace-nowrap">
{t("recentRequestsTokens")}
</th>
<th className="py-1.5 text-right font-semibold">{t("recentRequestsWhen")}</th>
</tr>
</thead>
<tbody className="divide-y divide-border/50">
{rows.map((row, i) => {
const state = requestState(row);
return (
<tr key={row.id || i} className="hover:bg-bg-subtle transition-colors">
<td className="py-1.5">
<span className={`block size-1.5 rounded-full ${STATE_DOT[state]}`} />
</td>
<td
className="py-1.5 font-mono truncate max-w-[140px]"
title={row.model || ""}
>
{row.model || "—"}
</td>
<td className="py-1.5 text-right whitespace-nowrap">
<span className="text-primary">{fmtCompact(row.tokens?.in)}</span>{" "}
<span className="text-green-500">{fmtCompact(row.tokens?.out)}</span>
</td>
<td className="py-1.5 text-right whitespace-nowrap text-text-muted">
{state === "active" ? (
<span className="text-primary"></span>
) : (
timeAgo(row.timestamp, nowMs)
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</Card>
);
}

View File

@@ -143,6 +143,9 @@ export async function GET(request: Request) {
if (searchParams.get("correlationId")) filter.correlationId = searchParams.get("correlationId");
if (searchParams.get("limit")) filter.limit = parseInt(searchParams.get("limit"));
if (searchParams.get("offset")) filter.offset = parseInt(searchParams.get("offset"));
// Home Recent Requests feed sets excludeTests=1 so connection-test probe rows
// are dropped at the SQL layer (before LIMIT), not client-side after slicing.
if (searchParams.get("excludeTests") === "1") filter.excludeTests = true;
const [logs, connections, providerNodes] = await Promise.all([
getCallLogs(filter),

View File

@@ -1800,6 +1800,11 @@
"updateStarted": "Update started...",
"reloadingPageAutomatically": "Reloading page automatically...",
"providerTopology": "Provider Topology",
"recentRequests": "Recent Requests",
"recentRequestsEmpty": "No requests yet.",
"recentRequestsModel": "Model",
"recentRequestsTokens": "In / Out",
"recentRequestsWhen": "When",
"downloadDmg": "Download DMG (macOS)",
"downloadDmgDescription": "A new version of the OmniRoute desktop app is available. Please download and install the macOS DMG installer to update (current: v{version}).",
"downloadExe": "Download EXE (Windows)",

View File

@@ -700,6 +700,14 @@ export async function getCallLogs(filter: any = {}) {
if (filter.combo) {
conditions.push("cl.combo_name IS NOT NULL");
}
if (filter.excludeTests) {
// Home "Recent Requests" is an allowlist of real provider inference, not a
// blacklist of known backend log types. Persisted provider requests enter via
// the public gateway namespaces (/v1/* or /api/v1/*); internal management work
// (connection tests, model sync, and future /api/providers/* jobs) does not.
// Apply this before LIMIT so backend rows can never displace real traffic.
conditions.push(`(cl.path LIKE '/v1/%' OR cl.path LIKE '/api/v1/%')`);
}
if (filter.since) {
conditions.push("cl.timestamp >= @since");
params.since = filter.since instanceof Date ? filter.since.toISOString() : String(filter.since);

View File

@@ -0,0 +1,132 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
/**
* Home "Recent Requests" feed passes `excludeTests` to getCallLogs so the panel shows
* ONLY real provider inference — never backend/management log rows. The filter is an
* ALLOWLIST of the public gateway namespaces (`/v1/%` and `/api/v1/%`), applied before
* LIMIT, rather than a blacklist of individual known noise types. This guards against
* the reported regression where model-sync rows (request_type 'model-sync', path
* `/api/providers/*`) leaked into the feed because the old blacklist only dropped
* connection-test rows.
*/
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-calllogs-allowlist-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.CALL_LOG_RETENTION_DAYS = "3650";
const core = await import("../../src/lib/db/core.ts");
const callLogs = await import("../../src/lib/usage/callLogs.ts");
type SeedRow = {
id: string;
timestamp: string;
path: string;
model: string;
provider: string;
source_format?: string;
request_type?: string | null;
};
function insertCallLog(row: SeedRow) {
const db = core.getDbInstance();
db.prepare(
`
INSERT INTO call_logs (
id, timestamp, method, path, status, model, provider, source_format, request_type, detail_state
)
VALUES (
@id, @timestamp, 'POST', @path, 200, @model, @provider, @source_format, @request_type, 'none'
)
`
).run({
source_format: row.source_format ?? null,
request_type: row.request_type ?? null,
...row,
});
}
test.beforeEach(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("excludeTests keeps only /v1 and /api/v1 inference rows, drops all backend/management rows", async () => {
const base = Date.parse("2026-01-01T00:00:00.000Z");
const iso = (i: number) => new Date(base + i * 1000).toISOString();
// Two REAL provider inference rows — the only rows the feed should keep.
insertCallLog({
id: "real-v1",
timestamp: iso(4),
path: "/v1/chat/completions",
model: "openai/gpt-4.1",
provider: "openai",
});
insertCallLog({
id: "real-api-v1",
timestamp: iso(3),
path: "/api/v1/chat/completions",
model: "anthropic/claude-opus-4-8",
provider: "anthropic",
});
// Backend/management NOISE — must never appear in the feed.
insertCallLog({
id: "noise-model-sync",
timestamp: iso(2),
path: "/api/providers/openai/models",
model: "model-sync",
provider: "openai",
source_format: "-",
request_type: "model-sync",
});
insertCallLog({
id: "noise-connection-test",
timestamp: iso(1),
path: "/api/providers/test",
model: "connection-test",
provider: "openai",
source_format: "test",
});
const rows = await callLogs.getCallLogs({ excludeTests: true, limit: 50 });
const ids = rows.map((row) => row.id).sort();
assert.deepEqual(
ids,
["real-api-v1", "real-v1"],
"only the /v1 and /api/v1 inference rows survive the allowlist"
);
});
test("without excludeTests every row is returned (allowlist is opt-in)", async () => {
const base = Date.parse("2026-02-01T00:00:00.000Z");
insertCallLog({
id: "real",
timestamp: new Date(base).toISOString(),
path: "/v1/chat/completions",
model: "openai/gpt-4.1",
provider: "openai",
});
insertCallLog({
id: "sync",
timestamp: new Date(base + 1000).toISOString(),
path: "/api/providers/openai/models",
model: "model-sync",
provider: "openai",
request_type: "model-sync",
});
const rows = await callLogs.getCallLogs({ limit: 50 });
assert.equal(rows.length, 2, "no allowlist → backend rows are not filtered out");
});