ocb-cli 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/proxy.js +181 -35
  2. package/package.json +1 -1
  3. package/src/proxy.ts +182 -35
package/dist/proxy.js CHANGED
@@ -115,6 +115,9 @@ app.use((req, res, next) => {
115
115
  next();
116
116
  });
117
117
  app.get("/", (req, res) => res.send(generateHTML()));
118
+ app.get("/health", (req, res) => {
119
+ res.json({ status: "ok", timestamp: Date.now(), models: availableModels.length, providers: Object.keys(providers).length });
120
+ });
118
121
  app.get("/api/status", (req, res) => {
119
122
  res.json({
120
123
  proxyPort: PROXY_PORT,
@@ -198,58 +201,166 @@ function generateHTML() {
198
201
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
199
202
  <title>OCB - OpenCode Bridge</title>
200
203
  <script src="https://cdn.tailwindcss.com"></script>
201
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
204
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
202
205
  <style>
203
- * { margin: 0; padding: 0; box-sizing: border-box; }
204
- body { font-family: 'Inter', -apple-system, sans-serif; background: #09090b; color: #fafafa; min-height: 100vh; }
206
+ * { box-sizing: border-box; margin: 0; padding: 0; }
207
+ body { font-family: 'Inter', -apple-system, sans-serif; background: #0f0f11; color: #fafafa; min-height: 100vh; }
205
208
  ::-webkit-scrollbar { width: 6px; }
206
209
  ::-webkit-scrollbar-track { background: transparent; }
207
210
  ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
211
+ .tab-active { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; }
212
+ .model-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px -5px rgba(99, 102, 241, 0.3); }
208
213
  </style>
209
214
  </head>
210
215
  <body>
211
216
  <div class="min-h-screen flex flex-col">
212
- <header class="bg-zinc-900 border-b border-zinc-800 px-6 py-4">
217
+ <header class="bg-[#18181b] border-b border-zinc-800 px-6 py-4">
213
218
  <div class="max-w-7xl mx-auto flex items-center justify-between">
214
- <div class="flex items-center gap-3">
215
- <div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center text-xl font-bold">⚡</div>
219
+ <div class="flex items-center gap-4">
220
+ <div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center text-2xl shadow-lg shadow-indigo-500/20">⚡</div>
216
221
  <div>
217
- <h1 class="text-xl font-semibold text-white">OCB</h1>
218
- <p class="text-xs text-zinc-500">OpenCode Bridge</p>
222
+ <h1 class="text-2xl font-bold bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent">OCB</h1>
223
+ <p class="text-xs text-zinc-500 font-medium">OpenCode Bridge</p>
224
+ </div>
225
+ <div class="ml-8 flex items-center gap-2 px-4 py-2 bg-zinc-900 rounded-xl border border-zinc-800">
226
+ <div id="healthDot" class="w-2.5 h-2.5 bg-green-500 rounded-full animate-pulse"></div>
227
+ <span id="connectionStatus" class="text-sm text-zinc-300">Connected</span>
219
228
  </div>
220
229
  </div>
221
230
  <div class="flex items-center gap-3">
222
- <div class="flex items-center gap-2 px-3 py-1.5 bg-zinc-800 rounded-lg">
223
- <div id="statusDot" class="w-2 h-2 bg-green-500 rounded-full"></div>
224
- <span id="currentModelDisplay" class="text-sm text-zinc-300">Loading...</span>
231
+ <div class="text-right mr-4">
232
+ <p class="text-xs text-zinc-500">Current Model</p>
233
+ <p id="currentModelDisplay" class="text-sm font-medium text-white">Loading...</p>
225
234
  </div>
226
- <button onclick="refreshModels()" class="px-3 py-1.5 text-sm bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition">↻</button>
227
- <button onclick="resetSession()" class="px-3 py-1.5 text-sm bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition">Reset</button>
235
+ <button onclick="refreshModels()" class="p-2.5 bg-zinc-800 hover:bg-zinc-700 rounded-xl transition text-zinc-300 hover:text-white" title="Refresh Models">
236
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
237
+ </button>
228
238
  </div>
229
239
  </div>
230
240
  </header>
231
- <main class="flex-1 flex max-w-7xl mx-auto w-full">
232
- <aside class="w-64 bg-zinc-900 border-r border-zinc-800 flex flex-col">
233
- <div class="p-4">
234
- <input type="text" id="searchInput" placeholder="Search models..." oninput="filterData()" class="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-indigo-500">
241
+
242
+ <nav class="bg-[#18181b] border-b border-zinc-800 px-6">
243
+ <div class="max-w-7xl mx-auto flex gap-1">
244
+ <button onclick="switchTab('models')" id="tab-models" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all tab-active">Models</button>
245
+ <button onclick="switchTab('status')" id="tab-status" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Status</button>
246
+ <button onclick="switchTab('settings')" id="tab-settings" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Settings</button>
247
+ </div>
248
+ </nav>
249
+
250
+ <main class="flex-1 max-w-7xl mx-auto w-full p-6">
251
+ <div id="models-view" class="flex gap-6 h-[calc(100vh-220px)]">
252
+ <aside class="w-72 bg-[#18181b] rounded-2xl border border-zinc-800 flex flex-col overflow-hidden">
253
+ <div class="p-4 border-b border-zinc-800">
254
+ <div class="relative">
255
+ <input type="text" id="searchInput" placeholder="Search models..." oninput="filterData()" class="w-full px-4 py-2.5 bg-zinc-900 border border-zinc-700 rounded-xl text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20">
256
+ </div>
257
+ </div>
258
+ <div id="providerList" class="flex-1 overflow-y-auto p-2"></div>
259
+ </aside>
260
+ <section class="flex-1 overflow-y-auto">
261
+ <div class="flex items-center justify-between mb-4">
262
+ <h2 id="sectionTitle" class="text-lg font-semibold text-white">All Models</h2>
263
+ <span id="modelCount" class="text-sm text-zinc-500"></span>
264
+ </div>
265
+ <div id="modelGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div>
266
+ </section>
267
+ </div>
268
+
269
+ <div id="status-view" class="hidden space-y-6">
270
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
271
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
272
+ <div class="flex items-center gap-3 mb-4">
273
+ <div class="w-10 h-10 bg-indigo-500/20 rounded-xl flex items-center justify-center text-indigo-400">📊</div>
274
+ <div>
275
+ <p class="text-xs text-zinc-500">Total Requests</p>
276
+ <p id="totalRequests" class="text-2xl font-bold text-white">0</p>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
281
+ <div class="flex items-center gap-3 mb-4">
282
+ <div class="w-10 h-10 bg-purple-500/20 rounded-xl flex items-center justify-center text-purple-400">🎯</div>
283
+ <div>
284
+ <p class="text-xs text-zinc-500">Total Tokens</p>
285
+ <p id="totalTokens" class="text-2xl font-bold text-white">0</p>
286
+ </div>
287
+ </div>
288
+ </div>
289
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
290
+ <div class="flex items-center gap-3 mb-4">
291
+ <div class="w-10 h-10 bg-green-500/20 rounded-xl flex items-center justify-center text-green-400">🔗</div>
292
+ <div>
293
+ <p class="text-xs text-zinc-500">Session</p>
294
+ <p id="sessionStatus" class="text-lg font-semibold text-green-400">Active</p>
295
+ </div>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
300
+ <h3 class="text-lg font-semibold text-white mb-4">Model Usage</h3>
301
+ <div class="space-y-3">
302
+ <div class="flex justify-between items-center py-2 border-b border-zinc-800">
303
+ <span class="text-zinc-400">Active Model</span>
304
+ <span id="activeModelName" class="text-white font-medium">-</span>
305
+ </div>
306
+ <div class="flex justify-between items-center py-2 border-b border-zinc-800">
307
+ <span class="text-zinc-400">Available Models</span>
308
+ <span id="totalModelsDisplay" class="text-white font-medium">-</span>
309
+ </div>
310
+ <div class="flex justify-between items-center py-2 border-b border-zinc-800">
311
+ <span class="text-zinc-400">Providers</span>
312
+ <span id="totalProviders" class="text-white font-medium">-</span>
313
+ </div>
314
+ <div class="flex justify-between items-center py-2">
315
+ <span class="text-zinc-400">OpenCode Server</span>
316
+ <span class="text-green-400 font-medium">localhost:4096</span>
317
+ </div>
318
+ </div>
319
+ </div>
320
+ <div class="flex gap-4">
321
+ <button onclick="resetSession()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Session</button>
322
+ <button onclick="resetStats()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Stats</button>
323
+ </div>
324
+ </div>
325
+
326
+ <div id="settings-view" class="hidden space-y-6">
327
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
328
+ <h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
329
+ <p class="text-zinc-400 text-sm mb-4">Add this to your Claude Code settings to use OCB:</p>
330
+ <div class="bg-zinc-950 rounded-xl p-4 font-mono text-sm text-zinc-300 overflow-x-auto">
331
+ <pre id="configJson">{
332
+ "env": {
333
+ "ANTHROPIC_BASE_URL": "http://localhost:8300/v1",
334
+ "ANTHROPIC_AUTH_TOKEN": "test",
335
+ "ANTHROPIC_MODEL": "minimax-m2.5-free"
336
+ }
337
+ }</pre>
338
+ </div>
339
+ <button onclick="copyConfig()" class="mt-4 px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 rounded-xl text-white font-medium transition flex items-center gap-2">
340
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
341
+ Copy Config
342
+ </button>
235
343
  </div>
236
- <div id="providerList" class="flex-1 overflow-y-auto px-2 pb-4"></div>
237
- </aside>
238
- <section class="flex-1 p-6 overflow-y-auto">
239
- <div class="flex items-center justify-between mb-4">
240
- <h2 id="sectionTitle" class="text-lg font-medium text-white">All Models</h2>
241
- <span id="modelCount" class="text-sm text-zinc-500"></span>
344
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
345
+ <h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
346
+ <div class="space-y-2 font-mono text-sm">
347
+ <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Health</span><span class="text-indigo-400">http://localhost:8300/health</span></div>
348
+ <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Status</span><span class="text-indigo-400">http://localhost:8300/api/status</span></div>
349
+ <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Models</span><span class="text-indigo-400">http://localhost:8300/api/models</span></div>
350
+ <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Set Model</span><span class="text-indigo-400">POST /api/model</span></div>
351
+ <div class="flex justify-between py-2"><span class="text-zinc-400">Messages</span><span class="text-indigo-400">http://localhost:8300/v1/messages</span></div>
352
+ </div>
242
353
  </div>
243
- <div id="modelGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"></div>
244
- </section>
354
+ </div>
245
355
  </main>
246
- <footer class="bg-zinc-900 border-t border-zinc-800 px-6 py-3">
356
+
357
+ <footer class="bg-[#18181b] border-t border-zinc-800 px-6 py-3">
247
358
  <div class="max-w-7xl mx-auto flex items-center justify-between text-sm text-zinc-500">
248
359
  <div class="flex items-center gap-6">
249
- <span>Requests: <span id="totalRequests" class="text-white font-medium">0</span></span>
250
- <span>Tokens: <span id="totalTokens" class="text-white font-medium">0</span></span>
360
+ <span>OCB v1.0.1</span>
361
+ <span>Proxy Port: <span class="text-white">8300</span></span>
251
362
  </div>
252
- <span id="totalModelsDisplay">Loading...</span>
363
+ <span>OpenCode Bridge for Claude Code</span>
253
364
  </div>
254
365
  </footer>
255
366
  </div>
@@ -259,6 +370,31 @@ function generateHTML() {
259
370
  let currentProvider = 'all';
260
371
  let currentModelId = null;
261
372
 
373
+ function switchTab(tab) {
374
+ document.querySelectorAll('[id^="tab-"]').forEach(el => {
375
+ el.classList.remove('tab-active');
376
+ el.classList.add('text-zinc-400', 'hover:bg-zinc-800');
377
+ });
378
+ document.getElementById('tab-' + tab).classList.add('tab-active');
379
+ document.getElementById('tab-' + tab).classList.remove('text-zinc-400', 'hover:bg-zinc-800');
380
+
381
+ document.getElementById('models-view').classList.add('hidden');
382
+ document.getElementById('status-view').classList.add('hidden');
383
+ document.getElementById('settings-view').classList.add('hidden');
384
+ document.getElementById(tab + '-view').classList.remove('hidden');
385
+ }
386
+
387
+ function copyConfig() {
388
+ const config = document.getElementById('configJson').textContent;
389
+ navigator.clipboard.writeText(config);
390
+ alert('Config copied to clipboard!');
391
+ }
392
+
393
+ async function resetStats() {
394
+ await fetch('/api/reset-stats', { method: 'POST' });
395
+ loadStatus();
396
+ }
397
+
262
398
  async function loadModels() {
263
399
  const res = await fetch('/api/models');
264
400
  const data = await res.json();
@@ -276,9 +412,9 @@ function generateHTML() {
276
412
  if (searchTerm) {
277
413
  providersToShow = providersToShow.filter(([name, models]) => name.toLowerCase().includes(searchTerm) || models.some(m => m.name.toLowerCase().includes(searchTerm)));
278
414
  }
279
- let html = '<div onclick="selectProvider('all')" class="px-3 py-2 rounded-lg cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === 'all' ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span>🏠 All</span><span class="text-xs opacity-60">' + allModels.length + '</span></div>';
415
+ let html = '<div onclick="selectProvider('all')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === 'all' ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span>🏠 All Models</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + allModels.length + '</span></div>';
280
416
  for (const [provider, models] of providersToShow) {
281
- html += '<div onclick="selectProvider(\\'' + provider + '\\')" class="px-3 py-2 rounded-lg cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === provider ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span class="truncate">' + provider + '</span><span class="text-xs opacity-60">' + models.length + '</span></div>';
417
+ html += '<div onclick="selectProvider(\\'' + provider.replace(/'/g, "\\\\'") + '\\')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === provider ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span class="truncate">' + provider + '</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + models.length + '</span></div>';
282
418
  }
283
419
  container.innerHTML = html;
284
420
  }
@@ -302,14 +438,14 @@ function generateHTML() {
302
438
  container.innerHTML = '<div class="col-span-full text-center text-zinc-500 py-12">No models found</div>';
303
439
  return;
304
440
  }
305
- container.innerHTML = filtered.map(m => '<div onclick="selectModel(\\'' + m.id + '\\')" class="p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02] ' + (m.id === currentModelId ? 'bg-indigo-600/20 border-indigo-500' : 'bg-zinc-900 border-zinc-800 hover:border-zinc-700') + '"><div class="flex items-start justify-between mb-2"><h3 class="font-medium text-white truncate">' + m.name + '</h3>' + (m.id === currentModelId ? '<span class="text-xs bg-indigo-500 text-white px-2 py-0.5 rounded">Active</span>' : '') + '</div><p class="text-xs text-zinc-500 font-mono truncate mb-2">' + m.id + '</p>' + (m.cost ? '<p class="text-xs text-zinc-400">$' + m.cost.input + '/M $' + m.cost.output + '/M</p>' : '') + '</div>').join('');
441
+ container.innerHTML = filtered.slice(0, 100).map(m => '<div onclick="selectModel(\\'' + m.id.replace(/'/g, "\\\\'") + '\\')" class="model-card p-5 rounded-2xl border cursor-pointer transition-all ' + (m.id === currentModelId ? 'bg-indigo-600/20 border-indigo-500 shadow-lg shadow-indigo-500/20' : 'bg-[#18181b] border-zinc-800 hover:border-zinc-700') + '"><div class="flex items-start justify-between mb-3"><h3 class="font-semibold text-white truncate text-base">' + m.name + '</h3>' + (m.id === currentModelId ? '<span class="text-xs bg-indigo-500 text-white px-3 py-1 rounded-full font-medium">Active</span>' : '') + '</div><p class="text-xs text-zinc-500 font-mono truncate mb-3">' + m.id + '</p>' + (m.cost ? '<div class="flex items-center gap-2 text-xs text-zinc-400"><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.input + '/M</span><span>→</span><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.output + '/M</span></div>' : '<div class="text-xs text-zinc-600">Free</div>') + '</div>').join('');
306
442
  }
307
443
 
308
444
  function filterData() {
309
445
  const searchTerm = document.getElementById('searchInput').value.toLowerCase();
310
446
  if (searchTerm) {
311
447
  const filtered = allModels.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
312
- document.getElementById('sectionTitle').textContent = 'Search Results';
448
+ document.getElementById('sectionTitle').textContent = 'Search Results (' + filtered.length + ')';
313
449
  renderModels(filtered);
314
450
  renderProviders();
315
451
  } else {
@@ -324,6 +460,7 @@ function generateHTML() {
324
460
  currentModelId = modelId;
325
461
  loadStatus();
326
462
  renderModels(currentProvider === 'all' ? allModels : (groupedModels[currentProvider] || []));
463
+ document.getElementById('configJson').textContent = JSON.stringify({ env: { ANTHROPIC_BASE_URL: "http://localhost:8300/v1", ANTHROPIC_AUTH_TOKEN: "test", ANTHROPIC_MODEL: modelId } }, null, 2);
327
464
  }
328
465
  }
329
466
 
@@ -332,10 +469,15 @@ function generateHTML() {
332
469
  const data = await res.json();
333
470
  const model = allModels.find(m => m.id === data.currentModel);
334
471
  document.getElementById('currentModelDisplay').textContent = model?.name || data.currentModel;
335
- document.getElementById('statusDot').className = 'w-2 h-2 rounded-full ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
472
+ document.getElementById('healthDot').className = 'w-2.5 h-2.5 rounded-full animate-pulse ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
473
+ document.getElementById('connectionStatus').textContent = data.sessionId === 'active' ? 'Connected' : 'Disconnected';
336
474
  document.getElementById('totalRequests').textContent = data.totalRequests.toLocaleString();
337
475
  document.getElementById('totalTokens').textContent = data.totalTokensUsed.toLocaleString();
338
- document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
476
+ document.getElementById('sessionStatus').textContent = data.sessionId === 'active' ? 'Active' : 'Inactive';
477
+ document.getElementById('sessionStatus').className = 'text-lg font-semibold ' + (data.sessionId === 'active' ? 'text-green-400' : 'text-red-400');
478
+ document.getElementById('activeModelName').textContent = model?.name || data.currentModel;
479
+ document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models';
480
+ document.getElementById('totalProviders').textContent = Object.keys(groupedModels).length + ' providers';
339
481
  currentModelId = data.currentModel;
340
482
  }
341
483
 
@@ -349,6 +491,10 @@ function generateHTML() {
349
491
  loadModels();
350
492
  }
351
493
 
494
+ function updateStats() {
495
+ document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
496
+ }
497
+
352
498
  loadModels();
353
499
  loadStatus();
354
500
  setInterval(loadStatus, 3000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocb-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "OpenCode Bridge - Use OpenCode AI models in Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/proxy.js",
package/src/proxy.ts CHANGED
@@ -143,6 +143,10 @@ app.use((req, res, next) => {
143
143
 
144
144
  app.get("/", (req, res) => res.send(generateHTML()));
145
145
 
146
+ app.get("/health", (req, res) => {
147
+ res.json({ status: "ok", timestamp: Date.now(), models: availableModels.length, providers: Object.keys(providers).length });
148
+ });
149
+
146
150
  app.get("/api/status", (req, res) => {
147
151
  res.json({
148
152
  proxyPort: PROXY_PORT,
@@ -232,58 +236,166 @@ function generateHTML() {
232
236
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
233
237
  <title>OCB - OpenCode Bridge</title>
234
238
  <script src="https://cdn.tailwindcss.com"></script>
235
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
239
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
236
240
  <style>
237
- * { margin: 0; padding: 0; box-sizing: border-box; }
238
- body { font-family: 'Inter', -apple-system, sans-serif; background: #09090b; color: #fafafa; min-height: 100vh; }
241
+ * { box-sizing: border-box; margin: 0; padding: 0; }
242
+ body { font-family: 'Inter', -apple-system, sans-serif; background: #0f0f11; color: #fafafa; min-height: 100vh; }
239
243
  ::-webkit-scrollbar { width: 6px; }
240
244
  ::-webkit-scrollbar-track { background: transparent; }
241
245
  ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
246
+ .tab-active { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; }
247
+ .model-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px -5px rgba(99, 102, 241, 0.3); }
242
248
  </style>
243
249
  </head>
244
250
  <body>
245
251
  <div class="min-h-screen flex flex-col">
246
- <header class="bg-zinc-900 border-b border-zinc-800 px-6 py-4">
252
+ <header class="bg-[#18181b] border-b border-zinc-800 px-6 py-4">
247
253
  <div class="max-w-7xl mx-auto flex items-center justify-between">
248
- <div class="flex items-center gap-3">
249
- <div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center text-xl font-bold">⚡</div>
254
+ <div class="flex items-center gap-4">
255
+ <div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center text-2xl shadow-lg shadow-indigo-500/20">⚡</div>
250
256
  <div>
251
- <h1 class="text-xl font-semibold text-white">OCB</h1>
252
- <p class="text-xs text-zinc-500">OpenCode Bridge</p>
257
+ <h1 class="text-2xl font-bold bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent">OCB</h1>
258
+ <p class="text-xs text-zinc-500 font-medium">OpenCode Bridge</p>
259
+ </div>
260
+ <div class="ml-8 flex items-center gap-2 px-4 py-2 bg-zinc-900 rounded-xl border border-zinc-800">
261
+ <div id="healthDot" class="w-2.5 h-2.5 bg-green-500 rounded-full animate-pulse"></div>
262
+ <span id="connectionStatus" class="text-sm text-zinc-300">Connected</span>
253
263
  </div>
254
264
  </div>
255
265
  <div class="flex items-center gap-3">
256
- <div class="flex items-center gap-2 px-3 py-1.5 bg-zinc-800 rounded-lg">
257
- <div id="statusDot" class="w-2 h-2 bg-green-500 rounded-full"></div>
258
- <span id="currentModelDisplay" class="text-sm text-zinc-300">Loading...</span>
266
+ <div class="text-right mr-4">
267
+ <p class="text-xs text-zinc-500">Current Model</p>
268
+ <p id="currentModelDisplay" class="text-sm font-medium text-white">Loading...</p>
259
269
  </div>
260
- <button onclick="refreshModels()" class="px-3 py-1.5 text-sm bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition">↻</button>
261
- <button onclick="resetSession()" class="px-3 py-1.5 text-sm bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition">Reset</button>
270
+ <button onclick="refreshModels()" class="p-2.5 bg-zinc-800 hover:bg-zinc-700 rounded-xl transition text-zinc-300 hover:text-white" title="Refresh Models">
271
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
272
+ </button>
262
273
  </div>
263
274
  </div>
264
275
  </header>
265
- <main class="flex-1 flex max-w-7xl mx-auto w-full">
266
- <aside class="w-64 bg-zinc-900 border-r border-zinc-800 flex flex-col">
267
- <div class="p-4">
268
- <input type="text" id="searchInput" placeholder="Search models..." oninput="filterData()" class="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-indigo-500">
276
+
277
+ <nav class="bg-[#18181b] border-b border-zinc-800 px-6">
278
+ <div class="max-w-7xl mx-auto flex gap-1">
279
+ <button onclick="switchTab('models')" id="tab-models" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all tab-active">Models</button>
280
+ <button onclick="switchTab('status')" id="tab-status" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Status</button>
281
+ <button onclick="switchTab('settings')" id="tab-settings" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Settings</button>
282
+ </div>
283
+ </nav>
284
+
285
+ <main class="flex-1 max-w-7xl mx-auto w-full p-6">
286
+ <div id="models-view" class="flex gap-6 h-[calc(100vh-220px)]">
287
+ <aside class="w-72 bg-[#18181b] rounded-2xl border border-zinc-800 flex flex-col overflow-hidden">
288
+ <div class="p-4 border-b border-zinc-800">
289
+ <div class="relative">
290
+ <input type="text" id="searchInput" placeholder="Search models..." oninput="filterData()" class="w-full px-4 py-2.5 bg-zinc-900 border border-zinc-700 rounded-xl text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20">
291
+ </div>
292
+ </div>
293
+ <div id="providerList" class="flex-1 overflow-y-auto p-2"></div>
294
+ </aside>
295
+ <section class="flex-1 overflow-y-auto">
296
+ <div class="flex items-center justify-between mb-4">
297
+ <h2 id="sectionTitle" class="text-lg font-semibold text-white">All Models</h2>
298
+ <span id="modelCount" class="text-sm text-zinc-500"></span>
299
+ </div>
300
+ <div id="modelGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div>
301
+ </section>
302
+ </div>
303
+
304
+ <div id="status-view" class="hidden space-y-6">
305
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
306
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
307
+ <div class="flex items-center gap-3 mb-4">
308
+ <div class="w-10 h-10 bg-indigo-500/20 rounded-xl flex items-center justify-center text-indigo-400">📊</div>
309
+ <div>
310
+ <p class="text-xs text-zinc-500">Total Requests</p>
311
+ <p id="totalRequests" class="text-2xl font-bold text-white">0</p>
312
+ </div>
313
+ </div>
314
+ </div>
315
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
316
+ <div class="flex items-center gap-3 mb-4">
317
+ <div class="w-10 h-10 bg-purple-500/20 rounded-xl flex items-center justify-center text-purple-400">🎯</div>
318
+ <div>
319
+ <p class="text-xs text-zinc-500">Total Tokens</p>
320
+ <p id="totalTokens" class="text-2xl font-bold text-white">0</p>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
325
+ <div class="flex items-center gap-3 mb-4">
326
+ <div class="w-10 h-10 bg-green-500/20 rounded-xl flex items-center justify-center text-green-400">🔗</div>
327
+ <div>
328
+ <p class="text-xs text-zinc-500">Session</p>
329
+ <p id="sessionStatus" class="text-lg font-semibold text-green-400">Active</p>
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
335
+ <h3 class="text-lg font-semibold text-white mb-4">Model Usage</h3>
336
+ <div class="space-y-3">
337
+ <div class="flex justify-between items-center py-2 border-b border-zinc-800">
338
+ <span class="text-zinc-400">Active Model</span>
339
+ <span id="activeModelName" class="text-white font-medium">-</span>
340
+ </div>
341
+ <div class="flex justify-between items-center py-2 border-b border-zinc-800">
342
+ <span class="text-zinc-400">Available Models</span>
343
+ <span id="totalModelsDisplay" class="text-white font-medium">-</span>
344
+ </div>
345
+ <div class="flex justify-between items-center py-2 border-b border-zinc-800">
346
+ <span class="text-zinc-400">Providers</span>
347
+ <span id="totalProviders" class="text-white font-medium">-</span>
348
+ </div>
349
+ <div class="flex justify-between items-center py-2">
350
+ <span class="text-zinc-400">OpenCode Server</span>
351
+ <span class="text-green-400 font-medium">localhost:4096</span>
352
+ </div>
353
+ </div>
269
354
  </div>
270
- <div id="providerList" class="flex-1 overflow-y-auto px-2 pb-4"></div>
271
- </aside>
272
- <section class="flex-1 p-6 overflow-y-auto">
273
- <div class="flex items-center justify-between mb-4">
274
- <h2 id="sectionTitle" class="text-lg font-medium text-white">All Models</h2>
275
- <span id="modelCount" class="text-sm text-zinc-500"></span>
355
+ <div class="flex gap-4">
356
+ <button onclick="resetSession()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Session</button>
357
+ <button onclick="resetStats()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Stats</button>
276
358
  </div>
277
- <div id="modelGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"></div>
278
- </section>
359
+ </div>
360
+
361
+ <div id="settings-view" class="hidden space-y-6">
362
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
363
+ <h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
364
+ <p class="text-zinc-400 text-sm mb-4">Add this to your Claude Code settings to use OCB:</p>
365
+ <div class="bg-zinc-950 rounded-xl p-4 font-mono text-sm text-zinc-300 overflow-x-auto">
366
+ <pre id="configJson">{
367
+ "env": {
368
+ "ANTHROPIC_BASE_URL": "http://localhost:8300/v1",
369
+ "ANTHROPIC_AUTH_TOKEN": "test",
370
+ "ANTHROPIC_MODEL": "minimax-m2.5-free"
371
+ }
372
+ }</pre>
373
+ </div>
374
+ <button onclick="copyConfig()" class="mt-4 px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 rounded-xl text-white font-medium transition flex items-center gap-2">
375
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
376
+ Copy Config
377
+ </button>
378
+ </div>
379
+ <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
380
+ <h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
381
+ <div class="space-y-2 font-mono text-sm">
382
+ <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Health</span><span class="text-indigo-400">http://localhost:8300/health</span></div>
383
+ <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Status</span><span class="text-indigo-400">http://localhost:8300/api/status</span></div>
384
+ <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Models</span><span class="text-indigo-400">http://localhost:8300/api/models</span></div>
385
+ <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Set Model</span><span class="text-indigo-400">POST /api/model</span></div>
386
+ <div class="flex justify-between py-2"><span class="text-zinc-400">Messages</span><span class="text-indigo-400">http://localhost:8300/v1/messages</span></div>
387
+ </div>
388
+ </div>
389
+ </div>
279
390
  </main>
280
- <footer class="bg-zinc-900 border-t border-zinc-800 px-6 py-3">
391
+
392
+ <footer class="bg-[#18181b] border-t border-zinc-800 px-6 py-3">
281
393
  <div class="max-w-7xl mx-auto flex items-center justify-between text-sm text-zinc-500">
282
394
  <div class="flex items-center gap-6">
283
- <span>Requests: <span id="totalRequests" class="text-white font-medium">0</span></span>
284
- <span>Tokens: <span id="totalTokens" class="text-white font-medium">0</span></span>
395
+ <span>OCB v1.0.1</span>
396
+ <span>Proxy Port: <span class="text-white">8300</span></span>
285
397
  </div>
286
- <span id="totalModelsDisplay">Loading...</span>
398
+ <span>OpenCode Bridge for Claude Code</span>
287
399
  </div>
288
400
  </footer>
289
401
  </div>
@@ -293,6 +405,31 @@ function generateHTML() {
293
405
  let currentProvider = 'all';
294
406
  let currentModelId = null;
295
407
 
408
+ function switchTab(tab) {
409
+ document.querySelectorAll('[id^="tab-"]').forEach(el => {
410
+ el.classList.remove('tab-active');
411
+ el.classList.add('text-zinc-400', 'hover:bg-zinc-800');
412
+ });
413
+ document.getElementById('tab-' + tab).classList.add('tab-active');
414
+ document.getElementById('tab-' + tab).classList.remove('text-zinc-400', 'hover:bg-zinc-800');
415
+
416
+ document.getElementById('models-view').classList.add('hidden');
417
+ document.getElementById('status-view').classList.add('hidden');
418
+ document.getElementById('settings-view').classList.add('hidden');
419
+ document.getElementById(tab + '-view').classList.remove('hidden');
420
+ }
421
+
422
+ function copyConfig() {
423
+ const config = document.getElementById('configJson').textContent;
424
+ navigator.clipboard.writeText(config);
425
+ alert('Config copied to clipboard!');
426
+ }
427
+
428
+ async function resetStats() {
429
+ await fetch('/api/reset-stats', { method: 'POST' });
430
+ loadStatus();
431
+ }
432
+
296
433
  async function loadModels() {
297
434
  const res = await fetch('/api/models');
298
435
  const data = await res.json();
@@ -310,9 +447,9 @@ function generateHTML() {
310
447
  if (searchTerm) {
311
448
  providersToShow = providersToShow.filter(([name, models]) => name.toLowerCase().includes(searchTerm) || models.some(m => m.name.toLowerCase().includes(searchTerm)));
312
449
  }
313
- let html = '<div onclick="selectProvider('all')" class="px-3 py-2 rounded-lg cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === 'all' ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span>🏠 All</span><span class="text-xs opacity-60">' + allModels.length + '</span></div>';
450
+ let html = '<div onclick="selectProvider('all')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === 'all' ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span>🏠 All Models</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + allModels.length + '</span></div>';
314
451
  for (const [provider, models] of providersToShow) {
315
- html += '<div onclick="selectProvider(\\'' + provider + '\\')" class="px-3 py-2 rounded-lg cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === provider ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span class="truncate">' + provider + '</span><span class="text-xs opacity-60">' + models.length + '</span></div>';
452
+ html += '<div onclick="selectProvider(\\'' + provider.replace(/'/g, "\\\\'") + '\\')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === provider ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span class="truncate">' + provider + '</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + models.length + '</span></div>';
316
453
  }
317
454
  container.innerHTML = html;
318
455
  }
@@ -336,14 +473,14 @@ function generateHTML() {
336
473
  container.innerHTML = '<div class="col-span-full text-center text-zinc-500 py-12">No models found</div>';
337
474
  return;
338
475
  }
339
- container.innerHTML = filtered.map(m => '<div onclick="selectModel(\\'' + m.id + '\\')" class="p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02] ' + (m.id === currentModelId ? 'bg-indigo-600/20 border-indigo-500' : 'bg-zinc-900 border-zinc-800 hover:border-zinc-700') + '"><div class="flex items-start justify-between mb-2"><h3 class="font-medium text-white truncate">' + m.name + '</h3>' + (m.id === currentModelId ? '<span class="text-xs bg-indigo-500 text-white px-2 py-0.5 rounded">Active</span>' : '') + '</div><p class="text-xs text-zinc-500 font-mono truncate mb-2">' + m.id + '</p>' + (m.cost ? '<p class="text-xs text-zinc-400">$' + m.cost.input + '/M $' + m.cost.output + '/M</p>' : '') + '</div>').join('');
476
+ container.innerHTML = filtered.slice(0, 100).map(m => '<div onclick="selectModel(\\'' + m.id.replace(/'/g, "\\\\'") + '\\')" class="model-card p-5 rounded-2xl border cursor-pointer transition-all ' + (m.id === currentModelId ? 'bg-indigo-600/20 border-indigo-500 shadow-lg shadow-indigo-500/20' : 'bg-[#18181b] border-zinc-800 hover:border-zinc-700') + '"><div class="flex items-start justify-between mb-3"><h3 class="font-semibold text-white truncate text-base">' + m.name + '</h3>' + (m.id === currentModelId ? '<span class="text-xs bg-indigo-500 text-white px-3 py-1 rounded-full font-medium">Active</span>' : '') + '</div><p class="text-xs text-zinc-500 font-mono truncate mb-3">' + m.id + '</p>' + (m.cost ? '<div class="flex items-center gap-2 text-xs text-zinc-400"><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.input + '/M</span><span>→</span><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.output + '/M</span></div>' : '<div class="text-xs text-zinc-600">Free</div>') + '</div>').join('');
340
477
  }
341
478
 
342
479
  function filterData() {
343
480
  const searchTerm = document.getElementById('searchInput').value.toLowerCase();
344
481
  if (searchTerm) {
345
482
  const filtered = allModels.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
346
- document.getElementById('sectionTitle').textContent = 'Search Results';
483
+ document.getElementById('sectionTitle').textContent = 'Search Results (' + filtered.length + ')';
347
484
  renderModels(filtered);
348
485
  renderProviders();
349
486
  } else {
@@ -358,6 +495,7 @@ function generateHTML() {
358
495
  currentModelId = modelId;
359
496
  loadStatus();
360
497
  renderModels(currentProvider === 'all' ? allModels : (groupedModels[currentProvider] || []));
498
+ document.getElementById('configJson').textContent = JSON.stringify({ env: { ANTHROPIC_BASE_URL: "http://localhost:8300/v1", ANTHROPIC_AUTH_TOKEN: "test", ANTHROPIC_MODEL: modelId } }, null, 2);
361
499
  }
362
500
  }
363
501
 
@@ -366,10 +504,15 @@ function generateHTML() {
366
504
  const data = await res.json();
367
505
  const model = allModels.find(m => m.id === data.currentModel);
368
506
  document.getElementById('currentModelDisplay').textContent = model?.name || data.currentModel;
369
- document.getElementById('statusDot').className = 'w-2 h-2 rounded-full ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
507
+ document.getElementById('healthDot').className = 'w-2.5 h-2.5 rounded-full animate-pulse ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
508
+ document.getElementById('connectionStatus').textContent = data.sessionId === 'active' ? 'Connected' : 'Disconnected';
370
509
  document.getElementById('totalRequests').textContent = data.totalRequests.toLocaleString();
371
510
  document.getElementById('totalTokens').textContent = data.totalTokensUsed.toLocaleString();
372
- document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
511
+ document.getElementById('sessionStatus').textContent = data.sessionId === 'active' ? 'Active' : 'Inactive';
512
+ document.getElementById('sessionStatus').className = 'text-lg font-semibold ' + (data.sessionId === 'active' ? 'text-green-400' : 'text-red-400');
513
+ document.getElementById('activeModelName').textContent = model?.name || data.currentModel;
514
+ document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models';
515
+ document.getElementById('totalProviders').textContent = Object.keys(groupedModels).length + ' providers';
373
516
  currentModelId = data.currentModel;
374
517
  }
375
518
 
@@ -383,6 +526,10 @@ function generateHTML() {
383
526
  loadModels();
384
527
  }
385
528
 
529
+ function updateStats() {
530
+ document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
531
+ }
532
+
386
533
  loadModels();
387
534
  loadStatus();
388
535
  setInterval(loadStatus, 3000);