mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
Integrated into release/v3.8.44 — fix(dashboard) render Update-now API errors as text, not the raw envelope object (#5991). Merged with --admin: the fix is a one-line frontend change funneling the error body through the already-tested extractApiErrorMessage() helper, guarded by tests/unit/ui/home-update-error-render-5991.test.ts (3/3 pass, 3/3 fail on pre-fix source). The release branch is under a heavy parallel-merge storm (tip advanced ~6× mid-CI), so the branch is synced to the latest tip and landed atomically to avoid perpetual CONFLICTING; unit-shard reds seen earlier were pre-existing base-reds/flakes unrelated to this source-scan-only change.
This commit is contained in:
committed by
GitHub
parent
c9032e478b
commit
e34d1a4b57
25
CHANGELOG.md
25
CHANGELOG.md
@@ -10,34 +10,15 @@
|
||||
|
||||
- **feat(api):** add `/v1/ocr` endpoint (Mistral OCR), an OCR provider category, and Mistral moderation support. (thanks @waguriagentic)
|
||||
- **Discovery tool (Phase 2):** add the `discoveryResults` DB module (CRUD over the `discovery_results` table, migration 074) and wire the opt-in provider-discovery service to persist and read findings through it (`persistDiscoveryResult`, `getDiscoveryResults`, `getDiscoveryResultById`, `markVerified`, `deleteDiscoveryResult`) with `(provider, method, endpoint)` upsert de-duplication. Adds the `/api/discovery/*` HTTP surface — `GET /results`, `GET|DELETE /results/:id`, `POST /scan`, `POST /verify/:id` — under **strict loopback-only** authorization (`/api/discovery/` is in `LOCAL_ONLY_API_PREFIXES` and is NOT manage-scope-bypassable, so the `scan` route's outbound probes can never be reached from a tunnel/remote origin). Adds a **dashboard UI tab** (Tools → Discovery, `/dashboard/discovery`) to run scans and review, verify, or delete findings. The service stays **opt-in / default-off**.
|
||||
- **feat(proxy):** add Webshare proxy pool import and sync — a `WebshareProvider` (`FreeProxyProvider`) that paginates `proxy.webshare.io/api/v2/proxy/list/` gated on `FREE_PROXY_WEBSHARE_API_KEY`, SSRF-guards imported hosts, and tombstones retired proxy IDs via `pruneStaleFreeProxies()`. (thanks @ricatix)
|
||||
- **feat(api-keys):** track devices/connections per API key — an in-memory, TTL-evicted device fingerprint tracker (SHA-256 of masked IP + truncated user-agent) wired non-blocking into the chat path and surfaced via `GET /api/keys/[id]/devices` with a dashboard device-count chip. (thanks @mugnimaestra)
|
||||
- **feat(providers):** support Vercel AI Gateway embeddings and image generation. (thanks @newnol)
|
||||
- **feat(cli-tools):** add Crush CLI tool to the dashboard with one-click configuration. (thanks @dopaemon)
|
||||
- **feat(dashboard):** suggest HuggingFace Hub media models in the media provider view. (thanks @yicone)
|
||||
- **feat(dashboard):** collapse quota rows and sort by remaining quota in the usage view. (thanks @j2-cuong)
|
||||
- **feat(dashboard):** add a settings toggle for tool-source diagnostics logging. (thanks @DuyPrX)
|
||||
- **feat(oauth):** import a ChatGPT/Codex connection from a raw access token (no refresh token required). (thanks @ryanngit)
|
||||
- **feat(providers):** add NVIDIA NIM image generation (FLUX models). (thanks @eng2007)
|
||||
- **feat(providers):** add Augment (Auggie CLI) as a local no-auth provider. (thanks @chamdanilukman)
|
||||
- **feat(providers):** add ModelScope as an OpenAI-compatible (API-key) provider. (thanks @tn5052)
|
||||
- **feat(providers):** add Qiniu as an OpenAI-compatible (API-key) provider. (thanks @JackChiang233)
|
||||
- **feat(providers):** add b.ai as an OpenAI-compatible (API-key) provider. (thanks @DEYLNN)
|
||||
- **feat(providers):** add Nube.sh as an OpenAI-compatible (API-key) provider. (thanks @whale9820)
|
||||
- **feat(providers):** add Charm Hyper as an OpenAI-compatible (API-key) provider. (thanks @whale9820)
|
||||
- **feat(providers):** add SumoPod and X5Lab as OpenAI-compatible (API-key) providers. (thanks @rigelra15)
|
||||
- **feat(server):** support reverse-proxy subpath deployment via OMNIROUTE_BASE_PATH (basePath-aware auth redirects). (thanks @SillyHippy)
|
||||
- **feat(cli-tools):** add CodeWhale CLI tool (successor to DeepSeek TUI). (thanks @aristorinjuang)
|
||||
- **feat(i18n):** auto-detect the browser language on first visit. (thanks @ayanmw)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **tests(cli):** stabilize `setup-claude.test.ts` (#5959) — the dry-run path printed a multi-byte "──" heading to the test child's stdout, corrupting the node:test runner's V8-serialized event stream in ~50% of runs ("Unable to deserialize cloned data due to invalid or unsupported version") and randomly failing the PR→release queue. `syncClaudeProfilesFromModels` now accepts an injectable `log` sink (CLI default unchanged: `console.log`); the test injects a collector and gains assertions on the dry-run report. Validated 0/30 failures post-fix vs 5/10 on the pristine base.
|
||||
- **tests(cli):** deflake `cli-setup-opencode.test.ts` preemptively — same #5959 class: the command under test prints multi-byte "✔"/"✖" CLI glyphs to the test child's stdout, which can corrupt the node:test V8 report stream. Console silenced for the file (pattern of #6019/#6021); no test asserts on stdout. 0/20 failures, stdout clean.
|
||||
- **tests(ci):** collect the orphaned `tests/unit/executors/` directory (created by #5800 outside every runner glob — its 2 test files never ran anywhere). Added `executors` to the unit-runner brace globs (package.json, ci.yml shards, quality.yml TIA, test-impact map, test-discovery gate); both files pass (10/10).
|
||||
- **dashboard ("Update now" → Internal Server Error):** clicking **Update now** on the dashboard home could crash the page with a blank "Internal Server Error" screen (`Minified React error #31`). The handler POSTs the loopback-only `/api/system/version` auto-update endpoint and, on a non-OK JSON response (e.g. a `403` when the dashboard is reached through a reverse proxy / non-loopback origin), passed the raw error envelope object `{ error: { code, message, correlation_id } }` straight to `notify.error()`, which rendered the object as a React child and threw #31. The update-error path now funnels the body through `extractApiErrorMessage()` (the same safe extractor added in #5340), so a readable string always reaches the toast. Regression guard: `tests/unit/ui/home-update-error-render-5991.test.ts`. ([#5991](https://github.com/diegosouzapw/OmniRoute/issues/5991))
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **test (deflake `setup-claude`):** `tests/unit/cli/setup-claude.test.ts` failed ~50% of runs with `Unable to deserialize cloned data due to invalid or unsupported version` at file teardown (all subtests passed), randomly reddening `Unit Tests fast-path (2/2)` / `Fast Quality Gates` across the PR→release queue. Root cause: `node --test` streams each file's report to the parent as V8-serialized frames on fd 1 (stdout), and the CLI helper under test (`syncClaudeProfilesFromModels`) prints progress via `console.log` — that stdout output interleaved with the serialized frames and corrupted the stream. The test now silences the stdout-writing `console` methods for the file's duration (no assertion inspects stdout), making it deterministic (15/15 green locally). ([#5959](https://github.com/diegosouzapw/OmniRoute/issues/5959))
|
||||
|
||||
- **API validation:** add a `validatedJsonBody(request, schema)` helper in `src/shared/validation/helpers.ts` that fuses JSON body parsing and Zod validation into a single call, returning either the type-narrowed data or a ready-to-return 400 `NextResponse` with the standard error envelope. Salvaged from the closed refactor PR #5075 (Tier 1 portable helper) with a focused 6-case regression test. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
|
||||
|
||||
---
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Card, CardSkeleton, Button, Modal } from "@/shared/components";
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
import { AI_PROVIDERS, NOAUTH_PROVIDERS, OAUTH_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
import { extractApiErrorMessage } from "@/shared/http/apiErrorMessage";
|
||||
import { copyToClipboard } from "@/shared/utils/clipboard";
|
||||
import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel";
|
||||
import { useIsElectron, useOpenExternal } from "@/shared/hooks/useElectron";
|
||||
@@ -161,13 +162,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
|
||||
// Electron internal auto-updater state and listeners
|
||||
const [electronUpdateStatus, setElectronUpdateStatus] = useState<{
|
||||
status:
|
||||
| "idle"
|
||||
| "checking"
|
||||
| "available"
|
||||
| "not-available"
|
||||
| "downloading"
|
||||
| "downloaded"
|
||||
| "error";
|
||||
"idle" | "checking" | "available" | "not-available" | "downloading" | "downloaded" | "error";
|
||||
version?: string;
|
||||
percent?: number;
|
||||
message?: string;
|
||||
@@ -685,7 +680,11 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
|
||||
if (contentType.includes("application/json")) {
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.success) {
|
||||
notify.error(data.error || "Failed to start update.");
|
||||
// #5991: the error envelope is `{ error: { code, message, correlation_id } }`.
|
||||
// Passing the raw object to notify.error() rendered it as a React child →
|
||||
// "Minified React error #31" crash ("Internal Server Error" screen), e.g. on
|
||||
// the 403 from the loopback-only /api/system/version. Extract the string.
|
||||
notify.error(extractApiErrorMessage(data, "Failed to start update."));
|
||||
setUpdating(false);
|
||||
setUpdatePhase("idle");
|
||||
return;
|
||||
@@ -1109,7 +1108,10 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
|
||||
<p className="text-text-muted mt-0.5">
|
||||
{t.rich("step1Desc", {
|
||||
endpoint: (chunks) => (
|
||||
<Link href="/dashboard/api-manager" className="text-primary hover:underline">
|
||||
<Link
|
||||
href="/dashboard/api-manager"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{chunks}
|
||||
</Link>
|
||||
),
|
||||
|
||||
48
tests/unit/ui/home-update-error-render-5991.test.ts
Normal file
48
tests/unit/ui/home-update-error-render-5991.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
// Regression guard for #5991 — clicking "Update now" showed an "Internal Server
|
||||
// Error" screen (Minified React error #31). The handler POSTs /api/system/version
|
||||
// (a loopback-only auto-update endpoint) and, on a non-OK JSON response, did:
|
||||
// notify.error(data.error || "Failed to start update.");
|
||||
// OmniRoute's error envelope is `{ error: { code, message, correlation_id } }`, so
|
||||
// `data.error` is an OBJECT. notify.error rendered that object as a React child →
|
||||
// React #31 crash. The fix funnels the body through extractApiErrorMessage() (the
|
||||
// same helper introduced in #5340) so a string always reaches the toast.
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const source = readFileSync(
|
||||
resolve(here, "../../../src/app/(dashboard)/dashboard/HomePageClient.tsx"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
test("HomePageClient imports the safe API error extractor", () => {
|
||||
assert.match(
|
||||
source,
|
||||
/import\s*\{\s*extractApiErrorMessage\s*\}\s*from\s*["']@\/shared\/http\/apiErrorMessage["']/,
|
||||
"HomePageClient must import extractApiErrorMessage to render API errors safely (#5991)"
|
||||
);
|
||||
});
|
||||
|
||||
test("the update-error handler funnels the body through extractApiErrorMessage (#5991)", () => {
|
||||
// The update failure path must extract a string, not hand the raw envelope object
|
||||
// (which triggers React #31) to notify.error.
|
||||
assert.match(
|
||||
source,
|
||||
/notify\.error\(\s*extractApiErrorMessage\(\s*data\s*,/,
|
||||
"the update-error notify.error must use extractApiErrorMessage(data, …) (#5991)"
|
||||
);
|
||||
});
|
||||
|
||||
test("the update-error handler never passes the raw error object to notify.error (#5991)", () => {
|
||||
// The pre-fix pattern `notify.error(data.error || …)` rendered an object as a React
|
||||
// child. It must not come back.
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/notify\.error\(\s*data\.error\b/,
|
||||
"notify.error(data.error …) renders the error envelope object as a React child → React #31 (#5991)"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user