Compare commits

..

7 Commits

Author SHA1 Message Date
diegosouzapw 67fa2592b5 chore(release): bump version to v1.4.10 2026-02-25 18:43:20 -03:00
diegosouzapw fed445c991 docs: update changelog for v1.4.10 release 2026-02-25 18:43:18 -03:00
diegosouzapw 62069dac98 fix: handle EACCES on restricted home directories (#133)
Wrap fs.mkdirSync(DATA_DIR) in try/catch so OmniRoute doesn't crash
when the user's home directory is not writable (e.g. restricted
environments, npm global install with different user).
Prints a clear warning with DATA_DIR env var recommendation.
2026-02-25 18:41:54 -03:00
diegosouzapw 5a65585c16 feat: add provider-level proxy button in Connections header
- New 'Provider Proxy' button next to Connections heading
- Opens ProxyConfigModal at provider level
- Amber highlight when proxy is configured, shows IP
- Supports all 3 proxy levels: global, provider, per-connection
2026-02-25 17:44:38 -03:00
diegosouzapw 4e3b363ba6 feat: color-coded proxy badge with IP display
- Green: global proxy
- Yellow/amber: provider-level proxy
- Blue: per-connection proxy
- Always shows proxy host/IP in the badge text
- Tooltip shows proxy source and full address
2026-02-25 17:40:21 -03:00
diegosouzapw f1d421bd8a feat: proxy badge shows for all proxy levels (key/provider/global)
- hasProxy now checks keys/providers/global config levels
- Badge color changed to emerald green to match other status badges
- Tooltip shows proxy source: per-connection, per-provider, or global
- Previously only showed for per-connection proxy, missing global/provider
2026-02-25 17:32:28 -03:00
diegosouzapw fb840d6392 fix: CLI tools page hangs — add timeout to runtime status checks
Server-side: wrap getCliRuntimeStatus() in 5s Promise.race timeout
Client-side: add 8s AbortController timeout to fetchToolStatuses()
Prevents entire page from staying in skeleton state forever when
binary checks hang on VPS
2026-02-25 17:11:49 -03:00
7 changed files with 133 additions and 25 deletions
+18 -1
View File
@@ -7,7 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
---
## [1.4.9] — 2026-02-25
## [1.4.10] — 2026-02-25
> ### 🔒 Proxy Visibility + Bug Fixes
>
> Color-coded proxy badges, provider-level proxy configuration, CLI tools page fix, and EACCES fix for restricted environments.
### ✨ New Features
- **Color-Coded Proxy Badges** — Each provider connection now shows its proxy status with color-coded badges: 🟢 green (global proxy), 🟡 amber (provider-level proxy), 🔵 blue (per-connection proxy). Badge always displays the proxy IP/host
- **Provider-Level Proxy Button** — New "Provider Proxy" button in the Connections header of each provider detail page. Allows configuring a proxy that applies to all connections of that provider
- **Proxy IP Display** — The proxy badge now always shows the proxy host/IP address for quick identification
### 🐛 Bug Fixes
- **CLI Tools Page Stuck in Loading** — Fixed the `/api/cli-tools/status` endpoint hanging indefinitely when binary checks stall on VPS. Added 5s server-side timeout per tool and 8s client-side AbortController timeout (#cli-tools-hang)
- **EACCES on Restricted Home Directories** — Fixed crash when `~/.omniroute` directory cannot be created due to permission issues. Now gracefully warns and suggests using the `DATA_DIR` environment variable (#133)
---
> ### 🌐 Full Internationalization (i18n) + Multi-Account Fix
>
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "1.4.9",
"version": "1.4.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "1.4.9",
"version": "1.4.10",
"license": "MIT",
"workspaces": [
"open-sse"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "1.4.9",
"version": "1.4.10",
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
"type": "module",
"bin": {
@@ -66,13 +66,17 @@ export default function CLIToolsPageClient({ machineId }) {
const fetchToolStatuses = async () => {
try {
const res = await fetch("/api/cli-tools/status");
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000); // 8s client timeout
const res = await fetch("/api/cli-tools/status", { signal: controller.signal });
clearTimeout(timeoutId);
if (res.ok) {
const data = await res.json();
setToolStatuses(data || {});
}
} catch (error) {
console.log("Error fetching CLI tool statuses:", error);
// Timeout or network error — proceed without statuses
console.log("CLI tool status check timed out or failed:", error);
} finally {
setStatusesLoaded(true);
}
@@ -839,7 +839,34 @@ export default function ProviderDetailPage() {
{/* Connections */}
<Card>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Connections</h2>
<div className="flex items-center gap-3">
<h2 className="text-lg font-semibold">Connections</h2>
{/* Provider-level proxy indicator/button */}
<button
onClick={() =>
setProxyTarget({
level: "provider",
id: providerId,
label: providerInfo?.name || providerId,
})
}
className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all ${
proxyConfig?.providers?.[providerId]
? "bg-amber-500/15 text-amber-500 hover:bg-amber-500/25"
: "bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"
}`}
title={
proxyConfig?.providers?.[providerId]
? `Provider proxy: ${proxyConfig.providers[providerId].host || "configured"}`
: "Configure proxy for all connections of this provider"
}
>
<span className="material-symbols-outlined text-[14px]">vpn_lock</span>
{proxyConfig?.providers?.[providerId]
? proxyConfig.providers[providerId].host || "Provider Proxy"
: "Provider Proxy"}
</button>
</div>
{!isCompatible && (
<Button
size="sm"
@@ -899,7 +926,29 @@ export default function ProviderDetailPage() {
label: conn.name || conn.email || conn.id,
})
}
hasProxy={!!proxyConfig?.keys?.[conn.id]}
hasProxy={
!!(
proxyConfig?.keys?.[conn.id] ||
proxyConfig?.providers?.[providerId] ||
proxyConfig?.global
)
}
proxySource={
proxyConfig?.keys?.[conn.id]
? "key"
: proxyConfig?.providers?.[providerId]
? "provider"
: proxyConfig?.global
? "global"
: null
}
proxyHost={
(
proxyConfig?.keys?.[conn.id] ||
proxyConfig?.providers?.[providerId] ||
proxyConfig?.global
)?.host || null
}
/>
))}
</div>
@@ -1893,6 +1942,8 @@ function ConnectionRow({
onReauth,
onProxy,
hasProxy,
proxySource,
proxyHost,
}) {
const displayName = isOAuth
? connection.name || connection.email || connection.displayName || "OAuth Account"
@@ -1994,18 +2045,33 @@ function ConnectionRow({
<span className="material-symbols-outlined text-[13px]">shield</span>
{rateLimitEnabled ? "Protected" : "Unprotected"}
</button>
{hasProxy && (
<>
<span className="text-text-muted/30 select-none">|</span>
<span
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-xs font-medium bg-primary/15 text-primary"
title="Proxy configured"
>
<span className="material-symbols-outlined text-[13px]">vpn_lock</span>
Proxy
</span>
</>
)}
{hasProxy &&
(() => {
const colorClass =
proxySource === "global"
? "bg-emerald-500/15 text-emerald-500"
: proxySource === "provider"
? "bg-amber-500/15 text-amber-500"
: "bg-blue-500/15 text-blue-500";
const label =
proxySource === "global"
? "Global"
: proxySource === "provider"
? "Provider"
: "Key";
return (
<>
<span className="text-text-muted/30 select-none">|</span>
<span
className={`inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-xs font-medium ${colorClass}`}
title={`Proxy (${label}): ${proxyHost || "configured"}`}
>
<span className="material-symbols-outlined text-[13px]">vpn_lock</span>
{proxyHost || "Proxy"}
</span>
</>
);
})()}
</div>
</div>
</div>
+15 -3
View File
@@ -53,11 +53,23 @@ export async function GET() {
try {
const statuses = {};
// Run all runtime checks in parallel
// Run all runtime checks in parallel with individual timeouts
const RUNTIME_CHECK_TIMEOUT = 5000; // 5s per tool max
await Promise.all(
CLI_TOOL_IDS.map(async (toolId) => {
try {
const runtime = await getCliRuntimeStatus(toolId);
const runtime = (await Promise.race([
getCliRuntimeStatus(toolId),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), RUNTIME_CHECK_TIMEOUT)
),
])) as {
installed: boolean;
runnable: boolean;
command?: string;
commandPath?: string;
reason?: string;
};
statuses[toolId] = {
installed: runtime.installed,
runnable: runtime.runnable,
@@ -69,7 +81,7 @@ export async function GET() {
statuses[toolId] = {
installed: false,
runnable: false,
reason: error.message,
reason: error.message || "Check failed",
};
}
})
+11 -2
View File
@@ -24,9 +24,18 @@ export const SQLITE_FILE = isCloud ? null : path.join(DATA_DIR, "storage.sqlite"
const JSON_DB_FILE = isCloud ? null : path.join(DATA_DIR, "db.json");
export const DB_BACKUPS_DIR = isCloud ? null : path.join(DATA_DIR, "db_backups");
// Ensure data directory exists
// Ensure data directory exists — with fallback for restricted home directories (#133)
if (!isCloud && !fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
try {
fs.mkdirSync(DATA_DIR, { recursive: true });
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
console.warn(
`[DB] Cannot create data directory '${DATA_DIR}': ${msg}\n` +
`[DB] Set the DATA_DIR environment variable to a writable path, e.g.:\n` +
`[DB] DATA_DIR=/path/to/writable/dir omniroute`
);
}
}
// ──────────────── Schema ────────────────