mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
QuotaCardGrid stacked every provider group vertically in a single flex flex-col container, and each group's own card grid didn't go multi-column until the md breakpoint. Provider groups now flow into a 2-column CSS multi-column layout on very wide (2xl) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately, filling horizontal whitespace sooner on narrower-but-not-mobile viewports. Regression guard: tests/unit/quota-card-grid-horizontal-layout.test.ts
This commit is contained in:
committed by
GitHub
parent
7fae224315
commit
6d3d122b84
@@ -25,6 +25,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
|
||||
- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`.
|
||||
- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna)
|
||||
- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`.
|
||||
- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi).
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
@@ -47,16 +47,16 @@ export default function QuotaCardGrid({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="columns-1 2xl:columns-2 gap-6 [column-fill:_balance]">
|
||||
{[...groups.entries()].map(([provider, conns]) => (
|
||||
<div key={provider} className="flex flex-col gap-3">
|
||||
<div key={provider} className="flex flex-col gap-3 break-inside-avoid mb-6">
|
||||
<h3 className="text-sm font-semibold text-text-main flex items-center gap-2">
|
||||
{providerLabels[provider] || provider}
|
||||
<span className="text-xs font-normal text-text-muted">
|
||||
({conns.length} account{conns.length !== 1 ? "s" : ""})
|
||||
</span>
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{conns.map((conn) => (
|
||||
<QuotaCard
|
||||
key={conn.id}
|
||||
|
||||
120
tests/unit/quota-card-grid-horizontal-layout.test.ts
Normal file
120
tests/unit/quota-card-grid-horizontal-layout.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
// #3520 — Provider Quota page should use horizontal whitespace better.
|
||||
//
|
||||
// QuotaCardGrid previously stacked provider groups vertically via a single
|
||||
// `flex flex-col` container and kept cards to a conservative 1/2/3/4-column
|
||||
// breakpoint ladder starting at `grid-cols-1`. This regression guard asserts
|
||||
// the shipped JSX structure and grouping logic directly:
|
||||
// 1. Grouping still produces one header per distinct provider with the
|
||||
// correct account count ("N account(s)").
|
||||
// 2. The per-group card grid starts multi-column (`grid-cols-2`), not
|
||||
// single-column, so cards fill horizontal space sooner.
|
||||
// 3. Provider groups themselves flow into multiple columns on wide screens
|
||||
// (`columns-*`) instead of an unconditional vertical `flex flex-col`
|
||||
// stack.
|
||||
//
|
||||
// Note: QuotaCardGrid's sibling QuotaCard pulls in next/image + provider-icon
|
||||
// resolution that only works inside the real Next.js runtime, so this file
|
||||
// exercises the two testable seams directly instead of full SSR-rendering the
|
||||
// tree: (a) the pure grouping function extracted below, mirroring the
|
||||
// component's own grouping logic, and (b) the literal className contract of
|
||||
// the component's JSX (static string literals, not derived at runtime),
|
||||
// parsed from source via the TypeScript compiler API so the assertions track
|
||||
// the real shipped markup rather than a hand-copied string.
|
||||
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import ts from "typescript";
|
||||
|
||||
const COMPONENT_PATH = path.resolve(
|
||||
import.meta.dirname,
|
||||
"../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx"
|
||||
);
|
||||
|
||||
function groupByProvider<T extends { provider: string }>(connections: T[]): Map<string, T[]> {
|
||||
const groups = new Map<string, T[]>();
|
||||
for (const conn of connections) {
|
||||
const list = groups.get(conn.provider) ?? [];
|
||||
list.push(conn);
|
||||
groups.set(conn.provider, list);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
test("QuotaCardGrid (#3520) — groups connections by provider with correct counts", () => {
|
||||
const groups = groupByProvider([
|
||||
{ id: "conn-a1", provider: "openai" },
|
||||
{ id: "conn-a2", provider: "openai" },
|
||||
{ id: "conn-b1", provider: "anthropic" },
|
||||
]);
|
||||
assert.deepEqual([...groups.keys()], ["openai", "anthropic"]);
|
||||
assert.equal(groups.get("openai")!.length, 2);
|
||||
assert.equal(groups.get("anthropic")!.length, 1);
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract the string literal passed to `className={...}` (or `className="..."`)
|
||||
* for every JSX `<div>` opening element in the component's `return (...)` JSX,
|
||||
* in source order, via the TypeScript compiler API (not a hand-rolled regex —
|
||||
* tracks the real AST so it can't be fooled by comments/whitespace).
|
||||
*/
|
||||
function extractDivClassNames(sourcePath: string): string[] {
|
||||
const sourceText = fs.readFileSync(sourcePath, "utf8");
|
||||
const sourceFile = ts.createSourceFile(
|
||||
sourcePath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TSX
|
||||
);
|
||||
const classNames: string[] = [];
|
||||
|
||||
function visit(node: ts.Node) {
|
||||
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
||||
const tagName = node.tagName.getText(sourceFile);
|
||||
if (tagName === "div") {
|
||||
for (const attr of node.attributes.properties) {
|
||||
if (ts.isJsxAttribute(attr) && attr.name.getText(sourceFile) === "className") {
|
||||
const init = attr.initializer;
|
||||
if (init && ts.isStringLiteral(init)) {
|
||||
classNames.push(init.text);
|
||||
} else if (
|
||||
init &&
|
||||
ts.isJsxExpression(init) &&
|
||||
init.expression &&
|
||||
ts.isStringLiteral(init.expression)
|
||||
) {
|
||||
classNames.push(init.expression.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(sourceFile);
|
||||
return classNames;
|
||||
}
|
||||
|
||||
test("QuotaCardGrid (#3520) — outer container flows groups into multiple columns, not a single vertical stack", () => {
|
||||
const [outerClassName] = extractDivClassNames(COMPONENT_PATH);
|
||||
assert.ok(outerClassName, "expected the component to render an outer <div className=...>");
|
||||
assert.match(outerClassName, /\bcolumns-/);
|
||||
assert.notEqual(outerClassName, "flex flex-col gap-6");
|
||||
});
|
||||
|
||||
test("QuotaCardGrid (#3520) — per-group card grid starts multi-column (grid-cols-2), not single-column", () => {
|
||||
const classNames = extractDivClassNames(COMPONENT_PATH);
|
||||
const cardGridClassName = classNames.find(
|
||||
(c) => /\bgrid\b/.test(c) && /grid-cols-/.test(c)
|
||||
);
|
||||
assert.ok(cardGridClassName, "expected to find the per-group card grid's className");
|
||||
assert.match(cardGridClassName!, /\bgrid-cols-2\b/);
|
||||
assert.doesNotMatch(cardGridClassName!, /\bgrid-cols-1\b/);
|
||||
});
|
||||
|
||||
test("QuotaCardGrid (#3520) — early-returns null when there are no connections", () => {
|
||||
const sourceText = fs.readFileSync(COMPONENT_PATH, "utf8");
|
||||
assert.match(sourceText, /if\s*\(\s*connections\.length\s*===\s*0\s*\)\s*return\s*null;/);
|
||||
});
|
||||
Reference in New Issue
Block a user