icebear0828 Claude Opus 4.6 commited on
Commit
bbf1e14
Β·
1 Parent(s): 87cb62c

fix: make dashboard config readonly, fix copy buttons for HTTP

Browse files

- Config fields (Base URL, API Key, Model) now readonly, driven by backend
- Copy buttons use fallback (textarea+execCommand) for HTTP contexts
- Added visual feedback for all copy buttons (success/fail states)
- Removed localStorage config override system (saveOverrides, restoreOverrides)
- Removed "Reset to defaults" button
- Added i18n keys for copy feedback (copied/copyFailed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. public/dashboard.html +56 -78
public/dashboard.html CHANGED
@@ -119,14 +119,13 @@
119
  <svg class="size-5 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
120
  <h2 class="text-[0.95rem] font-bold" data-i18n="apiConfig">API Configuration</h2>
121
  </div>
122
- <button onclick="resetConfigDefaults()" class="text-xs text-slate-400 dark:text-text-dim hover:text-slate-600 dark:hover:text-text-main transition-colors" data-i18n="resetDefaults">Reset to defaults</button>
123
  </div>
124
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
125
  <!-- Base URL -->
126
  <div class="space-y-1.5">
127
  <label class="text-xs font-semibold text-slate-700 dark:text-text-main" data-i18n="baseProxyUrl">Base Proxy URL</label>
128
  <div class="relative flex items-center">
129
- <input id="baseUrl" class="w-full pl-3 pr-10 py-2.5 bg-slate-50 dark:bg-bg-dark border border-gray-200 dark:border-border-dark rounded-lg text-[0.78rem] font-mono text-slate-600 dark:text-text-main focus:ring-1 focus:ring-primary focus:border-primary outline-none transition-all" type="text" value="Loading..."/>
130
  <button onclick="copyField('baseUrl', this)" class="absolute right-2 p-1.5 text-slate-400 dark:text-text-dim hover:text-primary transition-colors rounded-md hover:bg-slate-100 dark:hover:bg-border-dark" data-i18n-title="copyUrl" title="Copy URL">
131
  <svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
132
  </button>
@@ -136,12 +135,7 @@
136
  <div class="space-y-1.5">
137
  <label class="text-xs font-semibold text-slate-700 dark:text-text-main" data-i18n="defaultModel">Default Model</label>
138
  <div class="relative">
139
- <select id="defaultModel" class="w-full appearance-none pl-3 pr-10 py-2.5 bg-white dark:bg-bg-dark border border-gray-200 dark:border-border-dark rounded-lg text-[0.78rem] text-slate-700 dark:text-text-main font-medium focus:ring-1 focus:ring-primary focus:border-primary outline-none cursor-pointer transition-colors">
140
- <option value="codex">codex</option>
141
- </select>
142
- <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-slate-500 dark:text-text-dim">
143
- <svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/></svg>
144
- </div>
145
  </div>
146
  </div>
147
  <!-- API Key -->
@@ -151,7 +145,7 @@
151
  <div class="absolute left-3 text-slate-400 dark:text-text-dim">
152
  <svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
153
  </div>
154
- <input id="apiKey" class="w-full pl-10 pr-10 py-2.5 bg-slate-50 dark:bg-bg-dark border border-gray-200 dark:border-border-dark rounded-lg text-[0.78rem] font-mono text-slate-600 dark:text-text-main focus:ring-1 focus:ring-primary focus:border-primary outline-none transition-all tracking-wider" type="text" value="Loading..."/>
155
  <button onclick="copyField('apiKey', this)" class="absolute right-2 p-1.5 text-slate-400 dark:text-text-dim hover:text-primary transition-colors rounded-md hover:bg-slate-100 dark:hover:bg-border-dark" data-i18n-title="copyApiKey" title="Copy API Key">
156
  <svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
157
  </button>
@@ -231,7 +225,6 @@ const LANG = {
231
  ok: "OK",
232
  resetsAt: "Resets at",
233
  apiConfig: "API Configuration",
234
- resetDefaults: "Reset to defaults",
235
  baseProxyUrl: "Base Proxy URL",
236
  defaultModel: "Default Model",
237
  yourApiKey: "Your API Key",
@@ -249,6 +242,8 @@ const LANG = {
249
  failedExchangeCode: "Failed to exchange code",
250
  failedDeleteAccount: "Failed to delete account.",
251
  networkError: "Network error: ",
 
 
252
  footer: "\u00a9 2025 Codex Proxy. All rights reserved.",
253
  },
254
  zh: {
@@ -276,7 +271,6 @@ const LANG = {
276
  ok: "\u6b63\u5e38",
277
  resetsAt: "\u91cd\u7f6e\u65f6\u95f4",
278
  apiConfig: "API \u914d\u7f6e",
279
- resetDefaults: "\u6062\u590d\u9ed8\u8ba4",
280
  baseProxyUrl: "\u4ee3\u7406 URL",
281
  defaultModel: "\u9ed8\u8ba4\u6a21\u578b",
282
  yourApiKey: "API \u5bc6\u94a5",
@@ -294,6 +288,8 @@ const LANG = {
294
  failedExchangeCode: "\u6388\u6743\u7801\u4ea4\u6362\u5931\u8d25",
295
  failedDeleteAccount: "\u5220\u9664\u8d26\u6237\u5931\u8d25\u3002",
296
  networkError: "\u7f51\u7edc\u9519\u8bef\uff1a",
 
 
297
  footer: "\u00a9 2025 Codex Proxy\u3002\u4fdd\u7559\u6240\u6709\u6743\u5229\u3002",
298
  },
299
  };
@@ -622,7 +618,6 @@ async function loadStatus() {
622
  document.getElementById('apiKey').value = serverApiKey;
623
 
624
  await populateModelDropdown();
625
- restoreOverrides();
626
  updateCodeExamples();
627
  } catch (err) {
628
  console.error('Status load error:', err);
@@ -630,24 +625,17 @@ async function loadStatus() {
630
  }
631
 
632
  async function populateModelDropdown() {
633
- const sel = document.getElementById('defaultModel');
634
  try {
635
  const resp = await fetch('/v1/models');
636
  const data = await resp.json();
637
  const models = data.data.map(m => m.id);
638
  if (models.length > 0) {
639
- sel.innerHTML = '';
640
- models.forEach(id => {
641
- const opt = document.createElement('option');
642
- opt.value = id;
643
- opt.textContent = id;
644
- sel.appendChild(opt);
645
- });
646
- const preferred = models.find(n => n.includes('5.3-codex'));
647
- if (preferred) sel.value = preferred;
648
  }
649
  } catch {
650
- sel.innerHTML = '<option value="codex">codex</option>';
651
  }
652
  }
653
 
@@ -797,73 +785,63 @@ function switchLang(lang, el) {
797
  }
798
 
799
  // ── Copy ───────────────────────────────────────────────────────
800
- function copyField(id, btn) {
801
- const el = document.getElementById(id);
802
- const text = el.value !== undefined ? el.value : el.textContent;
803
- navigator.clipboard.writeText(text);
804
- const svg = btn.querySelector('svg');
805
- if (svg) {
806
- btn.innerHTML = SVG_CHECK;
807
- btn.classList.add('text-primary');
808
- setTimeout(() => { btn.innerHTML = SVG_COPY; btn.classList.remove('text-primary'); }, 2000);
809
- }
810
  }
811
 
812
- function copyCurrentCode() {
813
- const key = `${currentProtocol}-${currentLang}`;
814
- const code = window._codeExamples?.[key] || '';
815
- navigator.clipboard.writeText(code);
816
- }
817
-
818
- // ── Config Overrides (localStorage) ────────────────────────────
819
- function saveOverrides() {
820
- try {
821
- localStorage.setItem('codex-proxy-config-overrides', JSON.stringify({
822
- baseUrl: document.getElementById('baseUrl').value,
823
- apiKey: document.getElementById('apiKey').value,
824
- model: document.getElementById('defaultModel').value,
825
- }));
826
- } catch {}
827
- }
828
-
829
- function restoreOverrides() {
830
- try {
831
- const raw = localStorage.getItem('codex-proxy-config-overrides');
832
- if (!raw) return;
833
- const o = JSON.parse(raw);
834
- if (o.baseUrl) document.getElementById('baseUrl').value = o.baseUrl;
835
- if (o.apiKey) document.getElementById('apiKey').value = o.apiKey;
836
- if (o.model) {
837
- const sel = document.getElementById('defaultModel');
838
- const opts = Array.from(sel.options).map(op => op.value);
839
- if (opts.includes(o.model)) sel.value = o.model;
840
- }
841
- } catch {}
842
  }
843
 
844
- function resetConfigDefaults() {
845
- try { localStorage.removeItem('codex-proxy-config-overrides'); } catch {}
846
- document.getElementById('baseUrl').value = serverBaseUrl;
847
- document.getElementById('apiKey').value = serverApiKey;
848
- const sel = document.getElementById('defaultModel');
849
- const preferred = Array.from(sel.options).find(o => o.value.includes('5.3-codex'));
850
- if (preferred) sel.value = preferred.value;
851
- else if (sel.options.length) sel.selectedIndex = 0;
852
- updateCodeExamples();
853
  }
854
 
855
- // ── Reactive bindings ──────────────────────────────────────────
856
- function setupReactiveBindings() {
857
- document.getElementById('baseUrl').addEventListener('input', () => { updateCodeExamples(); saveOverrides(); });
858
- document.getElementById('apiKey').addEventListener('input', () => { updateCodeExamples(); saveOverrides(); });
859
- document.getElementById('defaultModel').addEventListener('change', () => { updateCodeExamples(); saveOverrides(); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
860
  }
861
 
862
  // ── Init ───────────────────────────────────────────────────────
 
863
  applyI18n();
864
  updateLangToggle();
865
  loadStatus();
866
- setupReactiveBindings();
867
  loadAccounts();
868
  </script>
869
  </body></html>
 
119
  <svg class="size-5 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
120
  <h2 class="text-[0.95rem] font-bold" data-i18n="apiConfig">API Configuration</h2>
121
  </div>
 
122
  </div>
123
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
124
  <!-- Base URL -->
125
  <div class="space-y-1.5">
126
  <label class="text-xs font-semibold text-slate-700 dark:text-text-main" data-i18n="baseProxyUrl">Base Proxy URL</label>
127
  <div class="relative flex items-center">
128
+ <input id="baseUrl" class="w-full pl-3 pr-10 py-2.5 bg-slate-100 dark:bg-bg-dark border border-gray-200 dark:border-border-dark rounded-lg text-[0.78rem] font-mono text-slate-500 dark:text-text-dim outline-none cursor-default select-all" type="text" value="Loading..." readonly/>
129
  <button onclick="copyField('baseUrl', this)" class="absolute right-2 p-1.5 text-slate-400 dark:text-text-dim hover:text-primary transition-colors rounded-md hover:bg-slate-100 dark:hover:bg-border-dark" data-i18n-title="copyUrl" title="Copy URL">
130
  <svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
131
  </button>
 
135
  <div class="space-y-1.5">
136
  <label class="text-xs font-semibold text-slate-700 dark:text-text-main" data-i18n="defaultModel">Default Model</label>
137
  <div class="relative">
138
+ <input id="defaultModel" class="w-full pl-3 pr-3 py-2.5 bg-slate-100 dark:bg-bg-dark border border-gray-200 dark:border-border-dark rounded-lg text-[0.78rem] text-slate-500 dark:text-text-dim font-medium outline-none cursor-default" type="text" value="codex" readonly/>
 
 
 
 
 
139
  </div>
140
  </div>
141
  <!-- API Key -->
 
145
  <div class="absolute left-3 text-slate-400 dark:text-text-dim">
146
  <svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
147
  </div>
148
+ <input id="apiKey" class="w-full pl-10 pr-10 py-2.5 bg-slate-100 dark:bg-bg-dark border border-gray-200 dark:border-border-dark rounded-lg text-[0.78rem] font-mono text-slate-500 dark:text-text-dim outline-none cursor-default select-all tracking-wider" type="text" value="Loading..." readonly/>
149
  <button onclick="copyField('apiKey', this)" class="absolute right-2 p-1.5 text-slate-400 dark:text-text-dim hover:text-primary transition-colors rounded-md hover:bg-slate-100 dark:hover:bg-border-dark" data-i18n-title="copyApiKey" title="Copy API Key">
150
  <svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
151
  </button>
 
225
  ok: "OK",
226
  resetsAt: "Resets at",
227
  apiConfig: "API Configuration",
 
228
  baseProxyUrl: "Base Proxy URL",
229
  defaultModel: "Default Model",
230
  yourApiKey: "Your API Key",
 
242
  failedExchangeCode: "Failed to exchange code",
243
  failedDeleteAccount: "Failed to delete account.",
244
  networkError: "Network error: ",
245
+ copied: "Copied!",
246
+ copyFailed: "Failed",
247
  footer: "\u00a9 2025 Codex Proxy. All rights reserved.",
248
  },
249
  zh: {
 
271
  ok: "\u6b63\u5e38",
272
  resetsAt: "\u91cd\u7f6e\u65f6\u95f4",
273
  apiConfig: "API \u914d\u7f6e",
 
274
  baseProxyUrl: "\u4ee3\u7406 URL",
275
  defaultModel: "\u9ed8\u8ba4\u6a21\u578b",
276
  yourApiKey: "API \u5bc6\u94a5",
 
288
  failedExchangeCode: "\u6388\u6743\u7801\u4ea4\u6362\u5931\u8d25",
289
  failedDeleteAccount: "\u5220\u9664\u8d26\u6237\u5931\u8d25\u3002",
290
  networkError: "\u7f51\u7edc\u9519\u8bef\uff1a",
291
+ copied: "\u5df2\u590d\u5236\uff01",
292
+ copyFailed: "\u5931\u8d25",
293
  footer: "\u00a9 2025 Codex Proxy\u3002\u4fdd\u7559\u6240\u6709\u6743\u5229\u3002",
294
  },
295
  };
 
618
  document.getElementById('apiKey').value = serverApiKey;
619
 
620
  await populateModelDropdown();
 
621
  updateCodeExamples();
622
  } catch (err) {
623
  console.error('Status load error:', err);
 
625
  }
626
 
627
  async function populateModelDropdown() {
628
+ const el = document.getElementById('defaultModel');
629
  try {
630
  const resp = await fetch('/v1/models');
631
  const data = await resp.json();
632
  const models = data.data.map(m => m.id);
633
  if (models.length > 0) {
634
+ const preferred = models.find(n => n.includes('5.3-codex')) || models[0];
635
+ el.value = preferred;
 
 
 
 
 
 
 
636
  }
637
  } catch {
638
+ el.value = 'codex';
639
  }
640
  }
641
 
 
785
  }
786
 
787
  // ── Copy ───────────────────────────────────────────────────────
788
+ function fallbackCopy(text) {
789
+ const ta = document.createElement('textarea');
790
+ ta.value = text;
791
+ ta.style.cssText = 'position:fixed;left:-9999px;opacity:0';
792
+ document.body.appendChild(ta);
793
+ ta.select();
794
+ let ok = false;
795
+ try { ok = document.execCommand('copy'); } catch {}
796
+ document.body.removeChild(ta);
797
+ return ok;
798
  }
799
 
800
+ async function clipboardCopy(text) {
801
+ if (navigator.clipboard?.writeText) {
802
+ try { await navigator.clipboard.writeText(text); return true; } catch {}
803
+ }
804
+ return fallbackCopy(text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
  }
806
 
807
+ async function copyField(id, btn) {
808
+ const el = document.getElementById(id);
809
+ const text = el.value !== undefined ? el.value : el.textContent;
810
+ const ok = await clipboardCopy(text);
811
+ btn.innerHTML = ok ? SVG_CHECK : '<svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>';
812
+ btn.classList.add(ok ? 'text-primary' : 'text-red-500');
813
+ setTimeout(() => { btn.innerHTML = SVG_COPY; btn.classList.remove('text-primary', 'text-red-500'); }, 2000);
 
 
814
  }
815
 
816
+ async function copyCurrentCode() {
817
+ const key = `${currentProtocol}-${currentLang}`;
818
+ const code = window._codeExamples?.[key] || '';
819
+ const ok = await clipboardCopy(code);
820
+ const btn = document.querySelector('#codeContainer button');
821
+ if (!btn) return;
822
+ const label = btn.querySelector('span');
823
+ const svg = btn.querySelector('svg');
824
+ if (ok) {
825
+ if (label) label.textContent = t('copied');
826
+ btn.classList.add('bg-primary', 'hover:bg-primary-hover');
827
+ btn.classList.remove('bg-slate-700', 'hover:bg-slate-600');
828
+ } else {
829
+ if (label) label.textContent = t('copyFailed');
830
+ btn.classList.add('bg-red-600', 'hover:bg-red-700');
831
+ btn.classList.remove('bg-slate-700', 'hover:bg-slate-600');
832
+ }
833
+ setTimeout(() => {
834
+ if (label) label.textContent = t('copy');
835
+ btn.classList.remove('bg-primary', 'hover:bg-primary-hover', 'bg-red-600', 'hover:bg-red-700');
836
+ btn.classList.add('bg-slate-700', 'hover:bg-slate-600');
837
+ }, 2000);
838
  }
839
 
840
  // ── Init ───────────────────────────────────────────────────────
841
+ try { localStorage.removeItem('codex-proxy-config-overrides'); } catch {}
842
  applyI18n();
843
  updateLangToggle();
844
  loadStatus();
 
845
  loadAccounts();
846
  </script>
847
  </body></html>