Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67fa2592b5 | |||
| fed445c991 | |||
| 62069dac98 | |||
| 5a65585c16 | |||
| 4e3b363ba6 | |||
| f1d421bd8a | |||
| fb840d6392 |
+18
-1
@@ -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
|
||||
>
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 ────────────────
|
||||
|
||||
Reference in New Issue
Block a user