From 606824d2821161a18ff87e4c00afebf16e784c56 Mon Sep 17 00:00:00 2001 From: Randi <55005611+rdself@users.noreply.github.com> Date: Sat, 4 Apr 2026 06:32:31 -0400 Subject: [PATCH] fix: providers filter persistence and settings i18n (#970) --- .../(dashboard)/dashboard/providers/page.tsx | 13 ++++++ .../providers/providerPageStorage.ts | 42 +++++++++++++++++++ src/i18n/messages/en.json | 8 ++++ src/i18n/messages/zh-CN.json | 14 ++++++- tests/unit/providers-page-utils.test.mjs | 34 +++++++++++++++ tests/unit/settings-i18n-keys.test.mjs | 29 +++++++++++++ 6 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/app/(dashboard)/dashboard/providers/providerPageStorage.ts create mode 100644 tests/unit/settings-i18n-keys.test.mjs diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx index fcebe0f76b..cad056e06e 100644 --- a/src/app/(dashboard)/dashboard/providers/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/page.tsx @@ -31,6 +31,7 @@ import { buildProviderEntries, filterConfiguredProviderEntries, } from "./providerPageUtils"; +import { readConfiguredOnlyPreference, writeConfiguredOnlyPreference } from "./providerPageStorage"; const CC_COMPATIBLE_LABEL = "CC Compatible"; const ADD_CC_COMPATIBLE_LABEL = "Add CC Compatible"; @@ -115,10 +116,16 @@ export default function ProvidersPage() { const [testResults, setTestResults] = useState(null); const [importingZed, setImportingZed] = useState(false); const [showConfiguredOnly, setShowConfiguredOnly] = useState(false); + const [configuredOnlyPreferenceReady, setConfiguredOnlyPreferenceReady] = useState(false); const notify = useNotificationStore(); const t = useTranslations("providers"); const tc = useTranslations("common"); + useEffect(() => { + setShowConfiguredOnly(readConfiguredOnlyPreference()); + setConfiguredOnlyPreferenceReady(true); + }, []); + useEffect(() => { const fetchData = async () => { try { @@ -145,6 +152,12 @@ export default function ProvidersPage() { fetchData(); }, []); + useEffect(() => { + if (!configuredOnlyPreferenceReady) return; + + writeConfiguredOnlyPreference(showConfiguredOnly); + }, [configuredOnlyPreferenceReady, showConfiguredOnly]); + const handleZedImport = async () => { setImportingZed(true); try { diff --git a/src/app/(dashboard)/dashboard/providers/providerPageStorage.ts b/src/app/(dashboard)/dashboard/providers/providerPageStorage.ts new file mode 100644 index 0000000000..ff7df5b817 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/providerPageStorage.ts @@ -0,0 +1,42 @@ +export const SHOW_CONFIGURED_ONLY_STORAGE_KEY = "omniroute-providers-show-configured-only"; + +interface StorageReader { + getItem(key: string): string | null; +} + +interface StorageWriter extends StorageReader { + setItem(key: string, value: string): void; + removeItem(key: string): void; +} + +export function parseConfiguredOnlyPreference(value: string | null | undefined): boolean { + return value === "true"; +} + +function getBrowserStorage(): StorageWriter | null { + try { + return globalThis.localStorage ?? null; + } catch { + return null; + } +} + +export function readConfiguredOnlyPreference(storage: StorageReader | null = getBrowserStorage()) { + if (!storage) return false; + + return parseConfiguredOnlyPreference(storage.getItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY)); +} + +export function writeConfiguredOnlyPreference( + enabled: boolean, + storage: StorageWriter | null = getBrowserStorage() +) { + if (!storage) return; + + if (enabled) { + storage.setItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY, "true"); + return; + } + + storage.removeItem(SHOW_CONFIGURED_ONLY_STORAGE_KEY); +} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 0fb43524eb..7a554b35bc 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1834,6 +1834,8 @@ "cacheTTL": "Cache TTL", "maxCacheSize": "Max Cache Size", "clearCache": "Clear Cache", + "cacheCleared": "Cache cleared successfully", + "clearCacheFailed": "Failed to clear cache", "cacheHits": "Cache Hits", "cacheMisses": "Cache Misses", "hitRate": "Hit Rate", @@ -2248,9 +2250,15 @@ "themeCoral": "Coral", "adaptiveVolumeRouting": "Adaptive Volume Routing", "adaptiveVolumeRoutingDesc": "Scale connections dynamically based on payload volume and throughput pressure.", + "lkgpToggleTitle": "Last Known Good Provider (LKGP)", + "lkgpToggleDesc": "When enabled, the router remembers which provider last served a successful response and tries it first on subsequent requests.", + "clearLkgpCache": "Clear LKGP Cache", + "lkgpCacheCleared": "LKGP cache cleared successfully", + "lkgpCacheClearFailed": "Failed to clear LKGP cache", "days": "Days", "lkgp": "LKGP Mode", "lkgpDesc": "Last Known Good Provider (Predictable resilience)", + "maintenance": "Maintenance", "purgeExpiredLogs": "Purge Expired Logs", "purgeLogsFailed": "Failed to purge logs" }, diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 4db9f3c441..0c10142fd8 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1753,6 +1753,8 @@ "cacheTTL": "缓存生存时间", "maxCacheSize": "最大缓存大小", "clearCache": "清除缓存", + "cacheCleared": "缓存已成功清除", + "clearCacheFailed": "清除缓存失败", "cacheHits": "缓存命中", "cacheMisses": "缓存未命中", "hitRate": "命中率", @@ -2164,7 +2166,17 @@ "customPricingNote": "你可以覆盖特定模型的默认定价。自定义覆盖会优先于自动检测到的定价。", "editPricing": "编辑定价", "viewFullDetails": "查看完整详情", - "themeCoral": "珊瑚色" + "themeCoral": "珊瑚色", + "adaptiveVolumeRouting": "自适应流量路由", + "adaptiveVolumeRoutingDesc": "根据实时负载量和吞吐压力,动态调整各提供商连接承载的流量。", + "lkgpToggleTitle": "最后已知良好提供商(LKGP)", + "lkgpToggleDesc": "启用后,路由器会记住上一次成功返回响应的提供商,并在后续请求中优先尝试它。", + "clearLkgpCache": "清除 LKGP 缓存", + "lkgpCacheCleared": "LKGP 缓存已成功清除", + "lkgpCacheClearFailed": "清除 LKGP 缓存失败", + "maintenance": "维护", + "purgeExpiredLogs": "清理过期日志", + "purgeLogsFailed": "清理日志失败" }, "translator": { "title": "翻译者", diff --git a/tests/unit/providers-page-utils.test.mjs b/tests/unit/providers-page-utils.test.mjs index 1347201e8d..b1b89bd1d5 100644 --- a/tests/unit/providers-page-utils.test.mjs +++ b/tests/unit/providers-page-utils.test.mjs @@ -3,6 +3,8 @@ import assert from "node:assert/strict"; const providerPageUtils = await import("../../src/app/(dashboard)/dashboard/providers/providerPageUtils.ts"); +const providerPageStorage = + await import("../../src/app/(dashboard)/dashboard/providers/providerPageStorage.ts"); const providers = await import("../../src/shared/constants/providers.ts"); test("merged OAuth providers keep free-tier providers in the OAuth section", () => { @@ -72,3 +74,35 @@ test("configured-only filter keeps only providers with saved connections", () => ); assert.equal(providerPageUtils.filterConfiguredProviderEntries(entries, false).length, 3); }); + +test("configured-only preference parser only enables explicit true values", () => { + assert.equal(providerPageStorage.parseConfiguredOnlyPreference("true"), true); + assert.equal(providerPageStorage.parseConfiguredOnlyPreference("false"), false); + assert.equal(providerPageStorage.parseConfiguredOnlyPreference(null), false); + assert.equal(providerPageStorage.parseConfiguredOnlyPreference(undefined), false); +}); + +test("configured-only preference storage round-trips correctly", () => { + const storage = new Map(); + const mockStorage = { + getItem(key) { + return storage.has(key) ? storage.get(key) : null; + }, + setItem(key, value) { + storage.set(key, value); + }, + removeItem(key) { + storage.delete(key); + }, + }; + + assert.equal(providerPageStorage.readConfiguredOnlyPreference(mockStorage), false); + + providerPageStorage.writeConfiguredOnlyPreference(true, mockStorage); + assert.equal(storage.get(providerPageStorage.SHOW_CONFIGURED_ONLY_STORAGE_KEY), "true"); + assert.equal(providerPageStorage.readConfiguredOnlyPreference(mockStorage), true); + + providerPageStorage.writeConfiguredOnlyPreference(false, mockStorage); + assert.equal(storage.has(providerPageStorage.SHOW_CONFIGURED_ONLY_STORAGE_KEY), false); + assert.equal(providerPageStorage.readConfiguredOnlyPreference(mockStorage), false); +}); diff --git a/tests/unit/settings-i18n-keys.test.mjs b/tests/unit/settings-i18n-keys.test.mjs new file mode 100644 index 0000000000..56efc1098e --- /dev/null +++ b/tests/unit/settings-i18n-keys.test.mjs @@ -0,0 +1,29 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const en = require("../../src/i18n/messages/en.json"); +const zhCn = require("../../src/i18n/messages/zh-CN.json"); + +const requiredSettingsKeys = [ + "adaptiveVolumeRouting", + "adaptiveVolumeRoutingDesc", + "lkgpToggleTitle", + "lkgpToggleDesc", + "clearLkgpCache", + "lkgpCacheCleared", + "lkgpCacheClearFailed", + "maintenance", + "cacheCleared", + "clearCacheFailed", + "purgeExpiredLogs", + "purgeLogsFailed", +]; + +test("settings translations include LKGP and maintenance keys in English and Simplified Chinese", () => { + for (const key of requiredSettingsKeys) { + assert.equal(typeof en.settings?.[key], "string", `en.settings.${key} should exist`); + assert.equal(typeof zhCn.settings?.[key], "string", `zh-CN.settings.${key} should exist`); + } +});