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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { useDisplayBaseUrl } from "@/shared/hooks";
import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
import { getProviderDisplayName } from "@/lib/display/names";
import { SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY } from "@/shared/constants/homeWidgets";
import { useTranslations } from "next-intl";
import A2ADashboardPage from "./components/A2ADashboard";
import McpDashboardPage from "./components/MCPDashboard";
Expand Down Expand Up @@ -186,6 +187,7 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
const [ngrokNotice, setNgrokNotice] = useState<TunnelNotice | null>(null);
const [ngrokToken, setNgrokToken] = useState("");
const [showNgrokTunnel, setShowNgrokTunnel] = useState(true);
const [showTokenSaverOnEndpoint, setShowTokenSaverOnEndpoint] = useState(true);
const [expandedTunnel, setExpandedTunnel] = useState<string | null>(null);
const [lanUrls, setLanUrls] = useState<string[]>([]);
const [tailscaleIpUrl, setTailscaleIpUrl] = useState<string | null>(null);
Expand Down Expand Up @@ -496,6 +498,7 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
setShowCloudflaredTunnel(tunnelVisibility.showCloudflaredTunnel);
setShowTailscaleFunnel(tunnelVisibility.showTailscaleFunnel);
setShowNgrokTunnel(tunnelVisibility.showNgrokTunnel);
setShowTokenSaverOnEndpoint(data[SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY] !== false);
if (data.ngrokAuthToken) setNgrokToken(data.ngrokAuthToken);

if (!tunnelVisibility.showCloudflaredTunnel) {
Expand Down Expand Up @@ -1752,7 +1755,7 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
</div>
</Card>

<TokenSaverCard />
{showTokenSaverOnEndpoint && <TokenSaverCard />}

<Card>
<div className="flex items-center justify-between mb-5">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ function getRequestPath(input: RequestInfo | URL) {

const cleanupCallbacks: Array<() => void> = [];

function installLocalStorageStub() {
const store = new Map<string, string>();

vi.stubGlobal("localStorage", {
getItem: vi.fn((key: string) => store.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => {
store.set(key, value);
}),
removeItem: vi.fn((key: string) => {
store.delete(key);
}),
clear: vi.fn(() => {
store.clear();
}),
});
}
Comment on lines +36 to +51
Comment on lines +36 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Object.defineProperty to stub window.localStorage can pollute the global state and cause side effects or flakiness in other tests. Since Vitest is used here, it is highly recommended to use vi.stubGlobal to mock global objects. This ensures that the stub is properly tracked and can be automatically cleaned up via vi.unstubAllGlobals() in afterEach.

function installLocalStorageStub() {
  const store = new Map<string, string>();
  vi.stubGlobal("localStorage", {
    getItem: vi.fn((key: string) => store.get(key) ?? null),
    setItem: vi.fn((key: string, value: string) => {
      store.set(key, value);
    }),
    removeItem: vi.fn((key: string) => {
      store.delete(key);
    }),
    clear: vi.fn(() => {
      store.clear();
    }),
  });
}


async function waitForText(text: string) {
const startedAt = Date.now();
while (!document.body.textContent?.includes(text)) {
Expand Down Expand Up @@ -119,10 +136,14 @@ vi.mock("next-intl", () => ({
"endpoint.categoryMedia": "Media APIs",
"endpoint.categoryUtility": "Utility APIs",
"endpoint.vscodeAliasTitle": "VS Code Token Alias",
"endpoint.vscodeAliasDescriptionReady": "Ready-to-paste compatibility URLs using the /api/v1/vscode/{token}/... endpoint.",
"endpoint.vscodeAliasDescriptionError": "Showing placeholder URLs because CLI keys could not be loaded in this session.",
"endpoint.vscodeAliasDescriptionLoading": "Loading CLI keys. Placeholder URLs are shown until a key is available.",
"endpoint.vscodeAliasDescriptionPlaceholder": "Showing placeholder URLs. Create or activate an API key in CLI Tools to replace {token}.",
"endpoint.vscodeAliasDescriptionReady":
"Ready-to-paste compatibility URLs using the /api/v1/vscode/{token}/... endpoint.",
"endpoint.vscodeAliasDescriptionError":
"Showing placeholder URLs because CLI keys could not be loaded in this session.",
"endpoint.vscodeAliasDescriptionLoading":
"Loading CLI keys. Placeholder URLs are shown until a key is available.",
"endpoint.vscodeAliasDescriptionPlaceholder":
"Showing placeholder URLs. Create or activate an API key in CLI Tools to replace {token}.",
"endpoint.vscodeAliasManage": "CLI Tools",
"endpoint.vscodeAliasBaseLabel": "VS Code base",
"endpoint.vscodeAliasModelsLabel": "VS Code models",
Expand Down Expand Up @@ -157,6 +178,7 @@ describe("EndpointPageClient", () => {
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.stubGlobal("fetch", fetchMock);
installLocalStorageStub();
});

afterEach(() => {
Expand Down Expand Up @@ -286,4 +308,47 @@ describe("EndpointPageClient", () => {
expect(requestPaths.length).toBeGreaterThan(0);
expect(requestPaths.every((path) => path === "/api/settings")).toBe(true);
});

it("hides the Token Saver card when endpoint visibility is disabled", async () => {
fetchMock.mockImplementation((input: RequestInfo | URL) => {
const path = getRequestPath(input);
if (path === "/api/settings") {
return Promise.resolve(
jsonResponse({
cloudEnabled: false,
cloudConfigured: false,
hideEndpointCloudflaredTunnel: true,
hideEndpointTailscaleFunnel: true,
hideEndpointNgrokTunnel: true,
showTokenSaverOnEndpoint: false,
})
);
}
if (path === "/v1/models") {
return Promise.resolve(jsonResponse({ data: [] }));
}
if (path === "/api/mcp/status") {
return Promise.resolve(jsonResponse({ online: false }));
}
if (path === "/api/a2a/status") {
return Promise.resolve(jsonResponse({ status: "ok", tasks: { activeStreams: 0 } }));
}
if (path === "/api/search/providers") {
return Promise.resolve(jsonResponse({ providers: [] }));
}
if (path === "/api/cli-tools/keys") {
return Promise.resolve(jsonResponse({ keys: [] }));
}
throw new Error(`Unexpected request: ${path}`);
});

renderEndpointPage();

await waitForText("Endpoint");
await waitForText("0 models across");

const requestPaths = fetchMock.mock.calls.map(([input]) => getRequestPath(input));
expect(document.body.textContent).not.toContain("Token Saver");
expect(requestPaths).not.toContain("/api/settings/compression");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
normalizeComboConfigMode,
type ComboConfigMode,
} from "@/shared/constants/comboConfigMode";
import { PIN_PROVIDER_QUOTA_TO_HOME_KEY } from "@/shared/constants/homeWidgets";
import {
PIN_PROVIDER_QUOTA_TO_HOME_KEY,
SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY,
} from "@/shared/constants/homeWidgets";

export default function AppearanceTab() {
const { theme, setTheme, isDark } = useTheme();
Expand All @@ -38,8 +41,11 @@ export default function AppearanceTab() {
const pinProviderQuotaToHome = settings.pinProviderQuotaToHome === true;
const showQuickStartOnHome = settings.showQuickStartOnHome !== false;
const showProviderTopologyOnHome = settings.showProviderTopologyOnHome !== false;
const showTokenSaverOnEndpoint = settings[SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY] !== false;
const autoRefreshProviderQuota = settings.autoRefreshProviderQuota === true;
const autoRefreshProviderQuotaInterval = Number.isFinite(settings.autoRefreshProviderQuotaInterval)
const autoRefreshProviderQuotaInterval = Number.isFinite(
settings.autoRefreshProviderQuotaInterval
)
? Number(settings.autoRefreshProviderQuotaInterval)
: 180;
const comboConfigMode = normalizeComboConfigMode(settings[COMBO_CONFIG_MODE_SETTING_KEY]);
Expand Down Expand Up @@ -254,6 +260,27 @@ export default function AppearanceTab() {
disabled={loading}
/>
</div>

<div className="flex items-start justify-between gap-4 px-4 py-3">
<div>
<p className="font-medium">
{getSettingsLabel("endpointTokenSaver", "Token Saver")}
</p>
<p className="text-sm text-text-muted">
{getSettingsLabel(
"endpointTokenSaverDesc",
"Show the Token Saver panel on the Endpoint page."
)}
</p>
</div>
<Toggle
checked={showTokenSaverOnEndpoint}
onChange={async (checked) => {
await updateSetting(SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY, checked);
}}
disabled={loading}
/>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ export default function GlobalConfigTab() {
<div className="p-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-xl text-violet-500" aria-hidden="true">
<span
className="material-symbols-outlined text-xl text-violet-500"
aria-hidden="true"
>
key
</span>
<div>
Expand All @@ -170,11 +173,9 @@ export default function GlobalConfigTab() {
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
network_check
</span>
<h2 className="text-lg font-bold">Bulk Healthcheck</h2>
<h2 className="text-lg font-bold">{t("bulkHealthcheck")}</h2>
</div>
<p className="text-sm text-text-muted mb-4">
Test all configured proxies against a target URL to find which ones work.
</p>
<p className="text-sm text-text-muted mb-4">{t("bulkHealthcheckDesc")}</p>
<div className="flex items-center gap-3 mb-4">
<input
type="text"
Expand All @@ -190,7 +191,7 @@ export default function GlobalConfigTab() {
disabled={testing}
onClick={runHealthcheck}
>
{testing ? "Testing..." : "Healthcheck All"}
{testing ? t("healthcheckTesting") : t("healthcheckAll")}
</Button>
</div>

Expand All @@ -203,20 +204,20 @@ export default function GlobalConfigTab() {
{testing && !results && (
<div className="flex items-center gap-2 text-sm text-text-muted py-2">
<span className="material-symbols-outlined animate-spin text-lg">refresh</span>
Testing all proxies in parallel...
{t("healthcheckTestingAll")}
</div>
)}

{summary && (
<div className="flex items-center gap-4 mb-3 text-sm">
<span className="text-text-muted">
Total: <strong>{summary.total}</strong>
{t("healthcheckTotal")}: <strong>{summary.total}</strong>
</span>
<span className="text-emerald-400">
Working: <strong>{summary.working}</strong>
{t("healthcheckWorking")}: <strong>{summary.working}</strong>
</span>
<span className="text-red-400">
Failed: <strong>{summary.failed}</strong>
{t("healthcheckFailedLabel")}: <strong>{summary.failed}</strong>
</span>
</div>
)}
Expand All @@ -226,9 +227,15 @@ export default function GlobalConfigTab() {
<table className="w-full text-xs">
<thead>
<tr className="bg-black/5 dark:bg-white/5">
<th className="text-left px-3 py-2 font-medium text-text-muted">Status</th>
<th className="text-left px-3 py-2 font-medium text-text-muted">Proxy URL</th>
<th className="text-right px-3 py-2 font-medium text-text-muted">Latency</th>
<th className="text-left px-3 py-2 font-medium text-text-muted">
{t("healthcheckStatus")}
</th>
<th className="text-left px-3 py-2 font-medium text-text-muted">
{t("healthcheckProxyUrl")}
</th>
<th className="text-right px-3 py-2 font-medium text-text-muted">
{t("healthcheckLatency")}
</th>
</tr>
</thead>
<tbody>
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -4667,6 +4667,18 @@
"globalProxy": "الوكيل العالمي",
"globalProxyDesc": "قم بتكوين وكيل خارجي عالمي لجميع مكالمات API. يمكن للموفرين الفرديين والمجموعات والمفاتيح تجاوز هذا.",
"noGlobalProxy": "لم يتم تكوين وكيل عالمي",
"bulkHealthcheck": "Bulk Healthcheck",
"bulkHealthcheckDesc": "Test all configured proxies against a target URL to find which ones work.",
"healthcheckTesting": "Testing...",
"healthcheckAll": "Healthcheck All",
"healthcheckFailed": "Failed to run healthcheck",
"healthcheckTestingAll": "Testing all proxies in parallel...",
"healthcheckTotal": "Total",
"healthcheckWorking": "Working",
"healthcheckFailedLabel": "Failed",
"healthcheckStatus": "Status",
"healthcheckProxyUrl": "Proxy URL",
"healthcheckLatency": "Latency",
"globalLabel": "عالمي",
"configure": "تكوين",
"globalSystemPrompt": "موجه النظام العالمي",
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/messages/az.json
Original file line number Diff line number Diff line change
Expand Up @@ -4667,6 +4667,18 @@
"globalProxy": "Global Proxy",
"globalProxyDesc": "Configure a global outbound proxy for all API calls. Individual providers, combos, and keys can override this.",
"noGlobalProxy": "No global proxy configured",
"bulkHealthcheck": "Bulk Healthcheck",
"bulkHealthcheckDesc": "Test all configured proxies against a target URL to find which ones work.",
"healthcheckTesting": "Testing...",
"healthcheckAll": "Healthcheck All",
"healthcheckFailed": "Failed to run healthcheck",
"healthcheckTestingAll": "Testing all proxies in parallel...",
"healthcheckTotal": "Total",
"healthcheckWorking": "Working",
"healthcheckFailedLabel": "Failed",
"healthcheckStatus": "Status",
"healthcheckProxyUrl": "Proxy URL",
"healthcheckLatency": "Latency",
"globalLabel": "Global",
"configure": "Configure",
"globalSystemPrompt": "Global System Prompt",
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/messages/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -4667,6 +4667,18 @@
"globalProxy": "Глобален прокси",
"globalProxyDesc": "Конфигурирайте глобален изходящ прокси за всички извиквания на API. Индивидуалните доставчици, комбинации и ключове могат да отменят това.",
"noGlobalProxy": "Няма конфигуриран глобален прокси",
"bulkHealthcheck": "Bulk Healthcheck",
"bulkHealthcheckDesc": "Test all configured proxies against a target URL to find which ones work.",
"healthcheckTesting": "Testing...",
"healthcheckAll": "Healthcheck All",
"healthcheckFailed": "Failed to run healthcheck",
"healthcheckTestingAll": "Testing all proxies in parallel...",
"healthcheckTotal": "Total",
"healthcheckWorking": "Working",
"healthcheckFailedLabel": "Failed",
"healthcheckStatus": "Status",
"healthcheckProxyUrl": "Proxy URL",
"healthcheckLatency": "Latency",
"globalLabel": "Глобален",
"configure": "Конфигуриране",
"globalSystemPrompt": "Глобална системна подкана",
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/messages/bn.json
Original file line number Diff line number Diff line change
Expand Up @@ -4667,6 +4667,18 @@
"globalProxy": "Global Proxy",
"globalProxyDesc": "Configure a global outbound proxy for all API calls. Individual providers, combos, and keys can override this.",
"noGlobalProxy": "No global proxy configured",
"bulkHealthcheck": "Bulk Healthcheck",
"bulkHealthcheckDesc": "Test all configured proxies against a target URL to find which ones work.",
"healthcheckTesting": "Testing...",
"healthcheckAll": "Healthcheck All",
"healthcheckFailed": "Failed to run healthcheck",
"healthcheckTestingAll": "Testing all proxies in parallel...",
"healthcheckTotal": "Total",
"healthcheckWorking": "Working",
"healthcheckFailedLabel": "Failed",
"healthcheckStatus": "Status",
"healthcheckProxyUrl": "Proxy URL",
"healthcheckLatency": "Latency",
"globalLabel": "Global",
"configure": "Configure",
"globalSystemPrompt": "Global System Prompt",
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4667,6 +4667,18 @@
"globalProxy": "Globální Proxy",
"globalProxyDesc": "Nastavte globální odchozí proxy pro všechna API volání. Jednotliví poskytovatelé, komba a klíče mohou toto přepsat.",
"noGlobalProxy": "Není nastavena globální proxy",
"bulkHealthcheck": "Bulk Healthcheck",
"bulkHealthcheckDesc": "Test all configured proxies against a target URL to find which ones work.",
"healthcheckTesting": "Testing...",
"healthcheckAll": "Healthcheck All",
"healthcheckFailed": "Failed to run healthcheck",
"healthcheckTestingAll": "Testing all proxies in parallel...",
"healthcheckTotal": "Total",
"healthcheckWorking": "Working",
"healthcheckFailedLabel": "Failed",
"healthcheckStatus": "Status",
"healthcheckProxyUrl": "Proxy URL",
"healthcheckLatency": "Latency",
"globalLabel": "Globální",
"configure": "Nastavit",
"globalSystemPrompt": "Globální systémový Prompt",
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/messages/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -4667,6 +4667,18 @@
"globalProxy": "Global proxy",
"globalProxyDesc": "Konfigurer en global udgående proxy for alle API-kald. Individuelle udbydere, kombinationer og nøgler kan tilsidesætte dette.",
"noGlobalProxy": "Ingen global proxy konfigureret",
"bulkHealthcheck": "Bulk Healthcheck",
"bulkHealthcheckDesc": "Test all configured proxies against a target URL to find which ones work.",
"healthcheckTesting": "Testing...",
"healthcheckAll": "Healthcheck All",
"healthcheckFailed": "Failed to run healthcheck",
"healthcheckTestingAll": "Testing all proxies in parallel...",
"healthcheckTotal": "Total",
"healthcheckWorking": "Working",
"healthcheckFailedLabel": "Failed",
"healthcheckStatus": "Status",
"healthcheckProxyUrl": "Proxy URL",
"healthcheckLatency": "Latency",
"globalLabel": "Globalt",
"configure": "Konfigurer",
"globalSystemPrompt": "Global System Prompt",
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -4672,6 +4672,18 @@
"globalProxy": "Globaler Proxy",
"globalProxyDesc": "Konfigurieren Sie einen globalen ausgehenden Proxy für alle API-Aufrufe. Einzelne Anbieter, Kombinationen und Schlüssel können dies außer Kraft setzen.",
"noGlobalProxy": "Kein globaler Proxy konfiguriert",
"bulkHealthcheck": "Bulk Healthcheck",
"bulkHealthcheckDesc": "Test all configured proxies against a target URL to find which ones work.",
"healthcheckTesting": "Testing...",
"healthcheckAll": "Healthcheck All",
"healthcheckFailed": "Failed to run healthcheck",
"healthcheckTestingAll": "Testing all proxies in parallel...",
"healthcheckTotal": "Total",
"healthcheckWorking": "Working",
"healthcheckFailedLabel": "Failed",
"healthcheckStatus": "Status",
"healthcheckProxyUrl": "Proxy URL",
"healthcheckLatency": "Latency",
"globalLabel": "Global",
"configure": "Konfigurieren",
"globalSystemPrompt": "Globale Systemaufforderung",
Expand Down
Loading