smartcontext-proxy 0.2.0 → 0.2.1

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.
@@ -1,17 +1,28 @@
1
1
  import type { MetricsCollector } from '../metrics/collector.js';
2
- import type { StorageAdapter } from '../storage/types.js';
3
2
 
4
- export function renderDashboard(metrics: MetricsCollector, paused: boolean): string {
3
+ export interface DashboardState {
4
+ paused: boolean;
5
+ mode: 'transparent' | 'optimizing';
6
+ proxyType: 'connect' | 'legacy';
7
+ abTestEnabled: boolean;
8
+ debugHeaders: boolean;
9
+ caInstalled: boolean;
10
+ systemProxyActive: boolean;
11
+ }
12
+
13
+ export function renderDashboard(metrics: MetricsCollector, state: DashboardState): string {
5
14
  const stats = metrics.getStats();
6
15
  const recent = metrics.getRecent(20);
7
- const uptime = metrics.getUptime();
8
- const uptimeStr = formatDuration(uptime);
16
+ const uptimeStr = formatDuration(metrics.getUptime());
17
+ const savingsAmount = estimateCostSaved(stats.totalOriginalTokens - stats.totalOptimizedTokens);
9
18
 
10
- const statusBadge = paused
19
+ const stateBadge = state.paused
11
20
  ? '<span class="badge paused">PAUSED</span>'
12
21
  : '<span class="badge running">RUNNING</span>';
13
22
 
14
- const savingsAmount = estimateCostSaved(stats.totalOriginalTokens - stats.totalOptimizedTokens);
23
+ const modeBadge = state.mode === 'optimizing'
24
+ ? '<span class="badge opt">OPTIMIZING</span>'
25
+ : '<span class="badge transparent">TRANSPARENT</span>';
15
26
 
16
27
  return `<!DOCTYPE html>
17
28
  <html lang="en">
@@ -24,17 +35,23 @@ export function renderDashboard(metrics: MetricsCollector, paused: boolean): str
24
35
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e1e4e8; }
25
36
  .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
26
37
  header { display: flex; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px solid #21262d; margin-bottom: 24px; }
27
- h1 { font-size: 20px; font-weight: 600; }
38
+ h1 { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
28
39
  h2 { font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #8b949e; }
29
- .badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
40
+ .badge { padding: 4px 12px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
30
41
  .badge.running { background: #238636; color: #fff; }
31
42
  .badge.paused { background: #d29922; color: #000; }
32
- .controls { display: flex; gap: 8px; }
33
- .btn { padding: 6px 16px; border-radius: 6px; border: 1px solid #30363d; background: #21262d; color: #e1e4e8; cursor: pointer; font-size: 13px; }
34
- .btn:hover { background: #30363d; }
43
+ .badge.opt { background: #1f6feb; color: #fff; }
44
+ .badge.transparent { background: #30363d; color: #8b949e; }
45
+ .badge.on { background: #238636; color: #fff; }
46
+ .badge.off { background: #30363d; color: #8b949e; }
47
+ .controls { display: flex; gap: 8px; align-items: center; }
48
+ .btn { padding: 6px 16px; border-radius: 6px; border: 1px solid #30363d; background: #21262d; color: #e1e4e8; cursor: pointer; font-size: 13px; transition: all 0.15s; }
49
+ .btn:hover { background: #30363d; border-color: #484f58; }
35
50
  .btn.primary { background: #238636; border-color: #238636; }
51
+ .btn.primary:hover { background: #2ea043; }
36
52
  .btn.warn { background: #d29922; border-color: #d29922; color: #000; }
37
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }
53
+ .btn.danger { background: #da3633; border-color: #da3633; }
54
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }
38
55
  .card { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 20px; }
39
56
  .card .value { font-size: 32px; font-weight: 700; color: #58a6ff; }
40
57
  .card .label { font-size: 13px; color: #8b949e; margin-top: 4px; }
@@ -50,20 +67,46 @@ export function renderDashboard(metrics: MetricsCollector, paused: boolean): str
50
67
  .tab.active { color: #e1e4e8; border-bottom-color: #58a6ff; }
51
68
  .tab-content { display: none; }
52
69
  .tab-content.active { display: block; }
70
+ .settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
71
+ .setting { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 16px; display: flex; justify-content: space-between; align-items: center; }
72
+ .setting-info { flex: 1; }
73
+ .setting-name { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
74
+ .setting-desc { font-size: 12px; color: #8b949e; }
75
+ .toggle { position: relative; width: 44px; height: 24px; cursor: pointer; }
76
+ .toggle input { display: none; }
77
+ .toggle .slider { position: absolute; inset: 0; background: #30363d; border-radius: 12px; transition: 0.2s; }
78
+ .toggle .slider:before { content: ''; position: absolute; width: 18px; height: 18px; left: 3px; top: 3px; background: #8b949e; border-radius: 50%; transition: 0.2s; }
79
+ .toggle input:checked + .slider { background: #238636; }
80
+ .toggle input:checked + .slider:before { transform: translateX(20px); background: #fff; }
81
+ .status-row { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
82
+ .status-pill { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #8b949e; }
83
+ .dot { width: 8px; height: 8px; border-radius: 50%; }
84
+ .dot.green { background: #3fb950; }
85
+ .dot.yellow { background: #d29922; }
86
+ .dot.red { background: #f85149; }
87
+ .dot.gray { background: #484f58; }
53
88
  .refresh-note { font-size: 11px; color: #484f58; text-align: right; margin-top: 8px; }
54
89
  </style>
55
90
  </head>
56
91
  <body>
57
92
  <div class="container">
58
93
  <header>
59
- <h1>SmartContext Proxy ${statusBadge}</h1>
94
+ <h1>SmartContext Proxy ${stateBadge} ${modeBadge}</h1>
60
95
  <div class="controls">
61
- ${paused
96
+ ${state.paused
62
97
  ? '<button class="btn primary" onclick="api(\'/_sc/resume\')">Resume</button>'
63
98
  : '<button class="btn warn" onclick="api(\'/_sc/pause\')">Pause</button>'}
64
99
  </div>
65
100
  </header>
66
101
 
102
+ <div class="status-row">
103
+ <div class="status-pill"><div class="dot ${state.caInstalled ? 'green' : 'red'}"></div>CA Certificate</div>
104
+ <div class="status-pill"><div class="dot ${state.systemProxyActive ? 'green' : 'gray'}"></div>System Proxy</div>
105
+ <div class="status-pill"><div class="dot ${state.mode === 'optimizing' ? 'green' : 'gray'}"></div>Context Optimization</div>
106
+ <div class="status-pill"><div class="dot ${state.abTestEnabled ? 'yellow' : 'gray'}"></div>A/B Test</div>
107
+ <div class="status-pill"><div class="dot ${state.proxyType === 'connect' ? 'green' : 'gray'}"></div>${state.proxyType === 'connect' ? 'Transparent' : 'Legacy'} Mode</div>
108
+ </div>
109
+
67
110
  <div class="grid">
68
111
  <div class="card savings">
69
112
  <div class="value">$${savingsAmount}</div>
@@ -84,9 +127,10 @@ export function renderDashboard(metrics: MetricsCollector, paused: boolean): str
84
127
  </div>
85
128
 
86
129
  <div class="tab-bar">
87
- <div class="tab active" onclick="switchTab('feed')">Live Feed</div>
88
- <div class="tab" onclick="switchTab('providers')">By Provider</div>
89
- <div class="tab" onclick="switchTab('models')">By Model</div>
130
+ <div class="tab active" onclick="switchTab('feed',this)">Live Feed</div>
131
+ <div class="tab" onclick="switchTab('providers',this)">By Provider</div>
132
+ <div class="tab" onclick="switchTab('models',this)">By Model</div>
133
+ <div class="tab" onclick="switchTab('settings',this)">Settings</div>
90
134
  </div>
91
135
 
92
136
  <div id="tab-feed" class="tab-content active">
@@ -116,12 +160,7 @@ export function renderDashboard(metrics: MetricsCollector, paused: boolean): str
116
160
  <thead><tr><th>Provider</th><th>Requests</th><th>Tokens Saved</th><th>Savings %</th></tr></thead>
117
161
  <tbody>
118
162
  ${Object.entries(stats.byProvider).map(([name, s]) => `
119
- <tr>
120
- <td>${name}</td>
121
- <td class="mono">${s.requests}</td>
122
- <td class="mono">${formatTokens(s.tokensSaved)}</td>
123
- <td class="savings-pct">${s.savingsPercent}%</td>
124
- </tr>
163
+ <tr><td>${name}</td><td class="mono">${s.requests}</td><td class="mono">${formatTokens(s.tokensSaved)}</td><td class="savings-pct">${s.savingsPercent}%</td></tr>
125
164
  `).join('')}
126
165
  </tbody>
127
166
  </table>
@@ -133,31 +172,88 @@ export function renderDashboard(metrics: MetricsCollector, paused: boolean): str
133
172
  <thead><tr><th>Model</th><th>Requests</th><th>Tokens Saved</th><th>Savings %</th></tr></thead>
134
173
  <tbody>
135
174
  ${Object.entries(stats.byModel).map(([name, s]) => `
136
- <tr>
137
- <td>${name}</td>
138
- <td class="mono">${s.requests}</td>
139
- <td class="mono">${formatTokens(s.tokensSaved)}</td>
140
- <td class="savings-pct">${s.savingsPercent}%</td>
141
- </tr>
175
+ <tr><td>${name}</td><td class="mono">${s.requests}</td><td class="mono">${formatTokens(s.tokensSaved)}</td><td class="savings-pct">${s.savingsPercent}%</td></tr>
142
176
  `).join('')}
143
177
  </tbody>
144
178
  </table>
145
179
  </div>
146
180
 
147
- <div class="refresh-note">Uptime: ${uptimeStr} | Auto-refresh: 10s</div>
181
+ <div id="tab-settings" class="tab-content">
182
+ <h2>Controls</h2>
183
+ <div class="settings-grid">
184
+ <div class="setting">
185
+ <div class="setting-info">
186
+ <div class="setting-name">Context Optimization</div>
187
+ <div class="setting-desc">Optimize LLM context windows to reduce token usage</div>
188
+ </div>
189
+ <label class="toggle">
190
+ <input type="checkbox" ${!state.paused ? 'checked' : ''} onchange="api(this.checked ? '/_sc/resume' : '/_sc/pause')">
191
+ <span class="slider"></span>
192
+ </label>
193
+ </div>
194
+
195
+ <div class="setting">
196
+ <div class="setting-info">
197
+ <div class="setting-name">A/B Test Mode</div>
198
+ <div class="setting-desc">Send each request twice to compare quality</div>
199
+ </div>
200
+ <label class="toggle">
201
+ <input type="checkbox" ${state.abTestEnabled ? 'checked' : ''} onchange="api(this.checked ? '/_sc/ab-test/enable' : '/_sc/ab-test/disable')">
202
+ <span class="slider"></span>
203
+ </label>
204
+ </div>
205
+
206
+ <div class="setting">
207
+ <div class="setting-info">
208
+ <div class="setting-name">Debug Headers</div>
209
+ <div class="setting-desc">Add X-SmartContext-* headers to responses</div>
210
+ </div>
211
+ <label class="toggle">
212
+ <input type="checkbox" ${state.debugHeaders ? 'checked' : ''} onchange="api(this.checked ? '/_sc/debug-headers/enable' : '/_sc/debug-headers/disable')">
213
+ <span class="slider"></span>
214
+ </label>
215
+ </div>
216
+
217
+ <div class="setting">
218
+ <div class="setting-info">
219
+ <div class="setting-name">System Proxy</div>
220
+ <div class="setting-desc">Auto-intercept all LLM traffic system-wide</div>
221
+ </div>
222
+ <label class="toggle">
223
+ <input type="checkbox" ${state.systemProxyActive ? 'checked' : ''} onchange="api(this.checked ? '/_sc/system-proxy/enable' : '/_sc/system-proxy/disable')">
224
+ <span class="slider"></span>
225
+ </label>
226
+ </div>
227
+ </div>
228
+ </div>
229
+
230
+ <div class="refresh-note">v0.2.0 | Uptime: ${uptimeStr} | Auto-refresh: 5s</div>
148
231
  </div>
149
232
  <script>
150
- function switchTab(name) {
233
+ function switchTab(name, el) {
151
234
  document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
152
235
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
153
236
  document.getElementById('tab-' + name).classList.add('active');
154
- event.target.classList.add('active');
237
+ if (el) el.classList.add('active');
238
+ location.hash = name;
155
239
  }
156
240
  async function api(path) {
157
241
  await fetch(path, { method: 'POST' });
158
242
  location.reload();
159
243
  }
160
- setTimeout(() => location.reload(), 10000);
244
+ // Restore tab from URL hash
245
+ (function() {
246
+ var hash = location.hash.replace('#', '');
247
+ if (hash && document.getElementById('tab-' + hash)) {
248
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
249
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
250
+ document.getElementById('tab-' + hash).classList.add('active');
251
+ document.querySelectorAll('.tab').forEach(t => {
252
+ if (t.getAttribute('onclick') && t.getAttribute('onclick').indexOf("'" + hash + "'") !== -1) t.classList.add('active');
253
+ });
254
+ }
255
+ })();
256
+ setTimeout(() => { var h = location.hash; location.reload(); if (h) location.hash = h; }, 5000);
161
257
  </script>
162
258
  </body>
163
259
  </html>`;
@@ -176,9 +272,7 @@ function formatDuration(ms: number): string {
176
272
  return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
177
273
  }
178
274
 
179
- /** Rough cost estimate based on Anthropic/OpenAI pricing */
180
275
  function estimateCostSaved(tokensSaved: number): string {
181
- // Assume avg $15/1M input tokens (Opus pricing)
182
276
  const cost = (tokensSaved / 1000000) * 15;
183
277
  return cost.toFixed(2);
184
278
  }