From a30c28abb6b99d4f8864605883782fe7f0b69d33 Mon Sep 17 00:00:00 2001
From: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
Date: Thu, 20 Aug 2026 18:25:23 -0300
Subject: [PATCH] feat(home): add Recent Requests panel + excludeTests
allowlist fix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Reimplementation of the non-conflicting value from #8450: a live Recent
Requests panel beside the home Provider Topology (polls
GET /api/usage/call-logs every ~3s, gated by the topology appearance
toggle + page visibility), plus the excludeTests allowlist fix so
connection-test/model-sync/management rows can never leak into the feed
(only /v1/* and /api/v1/* real-inference rows survive, applied before
LIMIT at the SQL layer).
Deliberately excludes #8450's calm-at-rest topology UX rework (logo-only
router core, KameBeamEdge, no persistent glow on healthy-idle nodes),
which contradicts the already-shipped #8428 ("decouple healthy and
last-routed states", merged 2026-07-25) — a product decision outside
this extraction's scope.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---
.../features/10897-home-recent-requests.md | 1 +
.../(dashboard)/dashboard/HomePageClient.tsx | 16 +-
.../(dashboard)/home/HomeRecentRequests.tsx | 207 ++++++++++++++++++
src/app/api/usage/call-logs/route.ts | 3 +
src/i18n/messages/en.json | 5 +
src/lib/usage/callLogs.ts | 8 +
.../call-logs-exclude-tests-allowlist.test.ts | 132 +++++++++++
7 files changed, 366 insertions(+), 6 deletions(-)
create mode 100644 changelog.d/features/10897-home-recent-requests.md
create mode 100644 src/app/(dashboard)/home/HomeRecentRequests.tsx
create mode 100644 tests/unit/call-logs-exclude-tests-allowlist.test.ts
diff --git a/changelog.d/features/10897-home-recent-requests.md b/changelog.d/features/10897-home-recent-requests.md
new file mode 100644
index 0000000000..fd6bcc9abe
--- /dev/null
+++ b/changelog.d/features/10897-home-recent-requests.md
@@ -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
diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx
index d427f4fcc4..897445fd0c 100644
--- a/src/app/(dashboard)/dashboard/HomePageClient.tsx
+++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx
@@ -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 && (
-
+
+
+
+
)}
{/* Provider Models Modal */}
diff --git a/src/app/(dashboard)/home/HomeRecentRequests.tsx b/src/app/(dashboard)/home/HomeRecentRequests.tsx
new file mode 100644
index 0000000000..abe6f57ede
--- /dev/null
+++ b/src/app/(dashboard)/home/HomeRecentRequests.tsx
@@ -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 = {
+ 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([]);
+ 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 | 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 (
+
+
+
+ {t("recentRequests")}
+
+
+
+ {loaded && rows.length === 0 ? (
+
+ {t("recentRequestsEmpty")}
+
+ ) : (
+
+
+
+
+ |
+ {t("recentRequestsModel")} |
+
+ {t("recentRequestsTokens")}
+ |
+ {t("recentRequestsWhen")} |
+
+
+
+ {rows.map((row, i) => {
+ const state = requestState(row);
+ return (
+
+ |
+
+ |
+
+ {row.model || "—"}
+ |
+
+ {fmtCompact(row.tokens?.in)}↑{" "}
+ {fmtCompact(row.tokens?.out)}↓
+ |
+
+ {state === "active" ? (
+ •••
+ ) : (
+ timeAgo(row.timestamp, nowMs)
+ )}
+ |
+
+ );
+ })}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/api/usage/call-logs/route.ts b/src/app/api/usage/call-logs/route.ts
index 3b3b8ddcc0..0a737ea1f9 100644
--- a/src/app/api/usage/call-logs/route.ts
+++ b/src/app/api/usage/call-logs/route.ts
@@ -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),
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 8147dfcb65..bdccf17e23 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -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)",
diff --git a/src/lib/usage/callLogs.ts b/src/lib/usage/callLogs.ts
index cea52ba196..5e3c878c0c 100644
--- a/src/lib/usage/callLogs.ts
+++ b/src/lib/usage/callLogs.ts
@@ -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);
diff --git a/tests/unit/call-logs-exclude-tests-allowlist.test.ts b/tests/unit/call-logs-exclude-tests-allowlist.test.ts
new file mode 100644
index 0000000000..78aa5728af
--- /dev/null
+++ b/tests/unit/call-logs-exclude-tests-allowlist.test.ts
@@ -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");
+});