Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 3 additions & 22 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>

---
Expand Down
20 changes: 11 additions & 9 deletions src/app/(dashboard)/dashboard/HomePageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>
),
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/ui/home-update-error-render-5991.test.ts
Original file line number Diff line number Diff line change
@@ -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)"
);
});
Loading