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).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-29 11:06:41 -03:00
committed by GitHub
parent 48983fcf31
commit 619e925002
3 changed files with 91 additions and 3 deletions

View File

@@ -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 `<span>` 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))
---

View File

@@ -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
// <span> 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>
): ModelVisibilityToolbarProps {
return {
t,
filterValue: "",
onFilterChange: vi.fn(),
activeCount: 0,
totalCount: 0,
onSelectAll: vi.fn(),
onDeselectAll: vi.fn(),
...overrides,
};
}
const roots: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(props: ModelVisibilityToolbarProps): HTMLDivElement {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(<ModelVisibilityToolbar {...props} />);
});
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");
});
});

View File

@@ -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({
<span className="material-symbols-outlined text-[16px]">visibility_off</span>
<span>{providerText(t, "hideAllModels", "Hide all")}</span>
</button>
<span className="whitespace-nowrap text-xs text-text-muted">
{providerText(t, "modelsActiveCount", "{active}/{total} active", {
active: activeCount,
total: totalCount,
})}
</span>
</div>
);
}