From 619e92500286c2b610fe250dc8ab9c4e55bc2024 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:06:41 -0300 Subject: [PATCH] fix(dashboard): restore active/total model count badge in Available Models (#5264) (#5344) Integrated into release/v3.8.41 (restore active/total model badge, #5264). Test: modelVisibilityToolbarActiveCount green (2/2). --- CHANGELOG.md | 2 +- ...modelVisibilityToolbarActiveCount.test.tsx | 82 +++++++++++++++++++ .../providers/[id]/components/ModelRow.tsx | 10 ++- 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/__tests__/modelVisibilityToolbarActiveCount.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 8adc263e98..b715f7bcea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ _In development — bullets added per PR; finalized at release._ ### 🔧 Bug Fixes -- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still OOM`'`d at the old cap. Now mirrors the Electron/standalone launchers: a user-pinned heap flag wins (duplicate CLI arg suppressed); otherwise the calibrated value is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238)) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar. It was dropped during the v3.8.13 god-file decomposition (#3327) — `ModelVisibilityToolbar` still received the counts but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over. Re-wired the existing props to the still-present `modelsActiveCount` key. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) - **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) --- diff --git a/src/app/(dashboard)/dashboard/providers/[id]/__tests__/modelVisibilityToolbarActiveCount.test.tsx b/src/app/(dashboard)/dashboard/providers/[id]/__tests__/modelVisibilityToolbarActiveCount.test.tsx new file mode 100644 index 0000000000..6728d72911 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/__tests__/modelVisibilityToolbarActiveCount.test.tsx @@ -0,0 +1,82 @@ +// @vitest-environment jsdom +// +// Regression test for issue #5264 — the "{active}/{total} active" model-count +// badge in the provider DETAIL page's "Available Models" toolbar. +// +// During the god-file decomposition in v3.8.13 (commit a25d5f1ef / PR #3327), +// ModelVisibilityToolbar kept receiving `activeCount`/`totalCount` props but the +// that rendered them was never carried over. The props were left +// orphaned (destructured as `_activeCount`/`_totalCount` to silence lint), so +// the count badge silently disappeared from the toolbar. +// +// This test renders the toolbar with activeCount={3} totalCount={5} and asserts +// the rendered output surfaces the `modelsActiveCount` interpolation ("3/5 +// active"). It fails against the pre-fix component (props unused → nothing +// rendered) and passes once the badge is restored. + +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + ModelVisibilityToolbar, + type ModelVisibilityToolbarProps, +} from "../components/ModelRow"; + +// Minimal translator stub: no `has`, so providerText() falls back to +// interpolating the values into the fallback string ("{active}/{total} active"). +const t = ((key: string) => key) as ModelVisibilityToolbarProps["t"]; + +function buildProps( + overrides: Partial +): ModelVisibilityToolbarProps { + return { + t, + filterValue: "", + onFilterChange: vi.fn(), + activeCount: 0, + totalCount: 0, + onSelectAll: vi.fn(), + onDeselectAll: vi.fn(), + ...overrides, + }; +} + +const roots: Array<{ root: ReturnType; el: HTMLDivElement }> = []; + +function render(props: ModelVisibilityToolbarProps): HTMLDivElement { + const el = document.createElement("div"); + document.body.appendChild(el); + const root = createRoot(el); + act(() => { + root.render(); + }); + roots.push({ root, el }); + return el; +} + +beforeEach(() => { + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; +}); + +afterEach(() => { + for (const { root, el } of roots.splice(0)) { + act(() => root.unmount()); + el.remove(); + } + vi.clearAllMocks(); +}); + +describe("ModelVisibilityToolbar active/total count badge (#5264)", () => { + it("renders the {active}/{total} active count", () => { + const el = render(buildProps({ activeCount: 3, totalCount: 5 })); + expect(el.textContent).toContain("3/5 active"); + }); + + it("reflects updated counts", () => { + const el = render(buildProps({ activeCount: 7, totalCount: 12 })); + expect(el.textContent).toContain("7/12 active"); + }); +}); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx index a24b686c23..79e3d658e7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx @@ -103,8 +103,8 @@ export function ModelVisibilityToolbar({ t, filterValue, onFilterChange, - activeCount: _activeCount, - totalCount: _totalCount, + activeCount, + totalCount, onSelectAll, onDeselectAll, selectAllDisabled, @@ -245,6 +245,12 @@ export function ModelVisibilityToolbar({ visibility_off {providerText(t, "hideAllModels", "Hide all")} + + {providerText(t, "modelsActiveCount", "{active}/{total} active", { + active: activeCount, + total: totalCount, + })} + ); }