openclaw-smartmeter 0.4.1 → 0.5.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.
@@ -23,6 +23,7 @@ async function initializeDashboard() {
23
23
  // Try to load data from local file first (works when served statically)
24
24
  await loadAnalysisData();
25
25
  hideLoading();
26
+ initGetStartedCard();
26
27
  checkOpenRouterConfig();
27
28
  startAutoRefresh();
28
29
  } catch (err) {
@@ -828,6 +829,168 @@ function updateModelDetails() {
828
829
  </table>`;
829
830
  }
830
831
 
832
+ /* ─── Inline API Key (Get Started Card) ─── */
833
+ async function validateInlineApiKey() {
834
+ const input = document.getElementById('gsApiKeyInput');
835
+ const key = input.value.trim();
836
+ const status = document.getElementById('gsKeyStatus');
837
+ const btn = document.getElementById('gsValidateBtn');
838
+
839
+ function showStatus(msg, type) {
840
+ status.textContent = msg;
841
+ status.className = 'config-status ' + type;
842
+ status.style.removeProperty('display');
843
+ }
844
+
845
+ if (!key) { showStatus('Please enter an API key.', 'error'); return; }
846
+ if (!key.startsWith('sk-or-')) { showStatus('Invalid format — key should start with sk-or-', 'error'); return; }
847
+
848
+ showStatus('Validating…', 'validating');
849
+ btn.disabled = true;
850
+ btn.textContent = 'Validating…';
851
+
852
+ let validated = false;
853
+ let errorMsg = '';
854
+ let usageData = null;
855
+
856
+ // Try API server first
857
+ try {
858
+ const res = await fetch(`${API_BASE_URL}/api/config/openrouter-key`, {
859
+ method: 'POST',
860
+ headers: { 'Content-Type': 'application/json' },
861
+ body: JSON.stringify({ apiKey: key })
862
+ });
863
+ const json = await res.json();
864
+ if (json.success) {
865
+ validated = true;
866
+ // Fetch usage via API server
867
+ try {
868
+ const ur = await fetch(`${API_BASE_URL}/api/openrouter-usage`);
869
+ const uj = await ur.json();
870
+ if (uj.success && uj.configured) usageData = uj.data || uj;
871
+ } catch {}
872
+ } else {
873
+ errorMsg = json.error || 'Validation failed';
874
+ }
875
+ } catch (_) {
876
+ // API server not available — validate directly against OpenRouter
877
+ try {
878
+ const res = await fetch('https://openrouter.ai/api/v1/auth/key', {
879
+ headers: { 'Authorization': `Bearer ${key}` }
880
+ });
881
+ if (res.ok) {
882
+ const data = await res.json();
883
+ if (data && data.data) {
884
+ validated = true;
885
+ usageData = data.data;
886
+ } else {
887
+ errorMsg = 'Key not recognized by OpenRouter';
888
+ }
889
+ } else if (res.status === 401 || res.status === 403) {
890
+ errorMsg = 'Invalid API key — authentication failed';
891
+ } else {
892
+ errorMsg = `OpenRouter returned status ${res.status}`;
893
+ }
894
+ } catch (e2) {
895
+ errorMsg = 'Could not reach OpenRouter to validate — check your connection';
896
+ }
897
+ }
898
+
899
+ btn.disabled = false;
900
+ btn.textContent = 'Save & Validate';
901
+
902
+ if (validated) {
903
+ localStorage.setItem('smartmeter_openrouter_key', key);
904
+ showStatus('✅ API key saved and validated!', 'success');
905
+
906
+ // Show balance section, hide key input
907
+ setTimeout(() => {
908
+ showBalanceDisplay(usageData);
909
+ // Also sync the modal key input
910
+ const modalInput = document.getElementById('apiKeyInput');
911
+ if (modalInput) modalInput.value = key;
912
+ // Also update the OpenRouter sidebar section
913
+ fetchOpenRouterUsage();
914
+ }, 600);
915
+ } else {
916
+ showStatus(`❌ ${errorMsg}`, 'error');
917
+ }
918
+ }
919
+
920
+ function showBalanceDisplay(usageData) {
921
+ const keySection = document.getElementById('gsKeySection');
922
+ const balanceSection = document.getElementById('gsBalance');
923
+
924
+ keySection.style.display = 'none';
925
+ balanceSection.style.display = 'block';
926
+
927
+ if (usageData) {
928
+ // Handle both API server shape (credits.total/used/remaining, account.limit)
929
+ // and direct OpenRouter shape (usage, limit, rate_limit)
930
+ const credits = usageData.credits || {};
931
+ const account = usageData.account || {};
932
+ const usage = credits.used ?? usageData.usage ?? account.usageBalance ?? 0;
933
+ const limit = credits.total ?? usageData.limit ?? account.limit ?? 0;
934
+ const remaining = credits.remaining ?? (limit - usage);
935
+ const rate = usageData.rate_limit?.requests
936
+ || usageData.rateLimit?.requests
937
+ || usageData.rate?.requests
938
+ || '--';
939
+
940
+ setText('gsBalanceCredits', `$${Number(limit).toFixed(2)}`);
941
+ setText('gsBalanceUsage', `$${Number(usage).toFixed(2)}`);
942
+ setText('gsBalanceRemaining', `$${Number(remaining).toFixed(2)}`);
943
+ setText('gsBalanceRate', typeof rate === 'number' ? `${rate}/s` : rate);
944
+ }
945
+
946
+ // Highlight step 1 as done
947
+ const step1 = document.getElementById('gsStep1');
948
+ if (step1) { step1.classList.add('gs-step-done'); }
949
+ }
950
+
951
+ function showApiKeyInput() {
952
+ const keySection = document.getElementById('gsKeySection');
953
+ const balanceSection = document.getElementById('gsBalance');
954
+ keySection.style.display = 'block';
955
+ balanceSection.style.display = 'none';
956
+ document.getElementById('gsApiKeyInput').focus();
957
+ }
958
+
959
+ /** On init, check if key is already stored and auto-show balance */
960
+ async function initGetStartedCard() {
961
+ const stored = localStorage.getItem('smartmeter_openrouter_key');
962
+ if (!stored) return;
963
+
964
+ // Pre-fill the input
965
+ const input = document.getElementById('gsApiKeyInput');
966
+ if (input) input.value = stored;
967
+
968
+ // Try to fetch balance
969
+ let usageData = null;
970
+ try {
971
+ const res = await fetch(`${API_BASE_URL}/api/openrouter-usage`);
972
+ const json = await res.json();
973
+ if (json.success && json.configured) {
974
+ usageData = json.data || json;
975
+ }
976
+ } catch {
977
+ // Try direct OpenRouter
978
+ try {
979
+ const res = await fetch('https://openrouter.ai/api/v1/auth/key', {
980
+ headers: { 'Authorization': `Bearer ${stored}` }
981
+ });
982
+ if (res.ok) {
983
+ const data = await res.json();
984
+ if (data && data.data) usageData = data.data;
985
+ }
986
+ } catch {}
987
+ }
988
+
989
+ if (usageData) {
990
+ showBalanceDisplay(usageData);
991
+ }
992
+ }
993
+
831
994
  /* ─── OpenRouter Integration ─── */
832
995
  async function checkOpenRouterConfig() {
833
996
  try {
@@ -849,26 +1012,35 @@ async function fetchOpenRouterUsage() {
849
1012
  if (!res.ok) return;
850
1013
  const json = await res.json();
851
1014
  if (json.success && json.configured) {
852
- const usage = json.data || json;
1015
+ const raw = json.data || json;
1016
+ const credits = raw.credits || {};
1017
+ const account = raw.account || {};
1018
+ const used = credits.used ?? raw.usage ?? account.usageBalance ?? 0;
1019
+ const limit = credits.total ?? raw.limit ?? account.limit ?? 0;
1020
+ const remaining = credits.remaining ?? (limit - used);
1021
+ const rate = raw.rate_limit?.requests || raw.rate?.requests || '--';
853
1022
  container.innerHTML = `
854
1023
  <div class="or-stats-grid">
855
1024
  <div class="or-stat-card">
856
1025
  <div class="or-stat-label">Usage (USD)</div>
857
- <div class="or-stat-value">$${(usage.usage || 0).toFixed(2)}</div>
1026
+ <div class="or-stat-value">$${Number(used).toFixed(2)}</div>
858
1027
  </div>
859
1028
  <div class="or-stat-card">
860
1029
  <div class="or-stat-label">Limit</div>
861
- <div class="or-stat-value">$${(usage.limit || 0).toFixed(2)}</div>
1030
+ <div class="or-stat-value">$${Number(limit).toFixed(2)}</div>
862
1031
  </div>
863
1032
  <div class="or-stat-card">
864
1033
  <div class="or-stat-label">Remaining</div>
865
- <div class="or-stat-value">$${((usage.limit || 0) - (usage.usage || 0)).toFixed(2)}</div>
1034
+ <div class="or-stat-value">$${Number(remaining).toFixed(2)}</div>
866
1035
  </div>
867
1036
  <div class="or-stat-card">
868
1037
  <div class="or-stat-label">Rate Limit</div>
869
- <div class="or-stat-value">${usage.rate_limit?.requests || '--'}/s</div>
1038
+ <div class="or-stat-value">${typeof rate === 'number' ? rate + '/s' : rate}</div>
870
1039
  </div>
871
1040
  </div>`;
1041
+
1042
+ // Also update Get Started balance card if visible
1043
+ showBalanceDisplay(raw);
872
1044
  }
873
1045
  } catch {
874
1046
  // noop
@@ -954,7 +1126,8 @@ async function saveApiKey() {
954
1126
  setTimeout(() => {
955
1127
  closeConfigModal();
956
1128
  fetchOpenRouterUsage();
957
- navigateTo('openrouter');
1129
+ // Also sync the Get Started card
1130
+ initGetStartedCard();
958
1131
  }, 1200);
959
1132
  } else {
960
1133
  showStatus(`❌ ${errorMsg}`, 'error');
@@ -63,40 +63,84 @@
63
63
 
64
64
  <!-- Section: Overview -->
65
65
  <section class="page-section active" id="section-overview">
66
- <!-- How It Works Banner -->
67
- <div class="how-it-works-banner">
68
- <div class="hiw-header">
69
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
70
- <strong>How SmartMeter Works</strong>
66
+ <!-- Get Started / Analyze Card -->
67
+ <div class="get-started-card" id="getStartedCard">
68
+ <div class="gs-header">
69
+ <div class="gs-header-left">
70
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
71
+ <div>
72
+ <strong class="gs-title">Get Started — Connect OpenRouter</strong>
73
+ <p class="gs-subtitle">Enter your API key to analyze usage, view your balance, and get cost-saving recommendations.</p>
74
+ </div>
75
+ </div>
71
76
  </div>
72
- <p class="hiw-desc">SmartMeter currently analyzes AI usage routed through <strong>OpenRouter</strong>. Connect your OpenRouter API key to unlock the full workflow:</p>
73
- <div class="hiw-steps">
74
- <div class="hiw-step">
75
- <div class="hiw-step-num">1</div>
77
+
78
+ <!-- API Key Input Row -->
79
+ <div class="gs-key-section" id="gsKeySection">
80
+ <div class="gs-key-row">
81
+ <div class="gs-key-input-wrap">
82
+ <label class="gs-key-label" for="gsApiKeyInput">OpenRouter API Key</label>
83
+ <div class="gs-key-field">
84
+ <input type="password" id="gsApiKeyInput" placeholder="sk-or-v1-..." class="form-input gs-input">
85
+ <button class="btn btn-primary gs-validate-btn" id="gsValidateBtn" onclick="validateInlineApiKey()">Save &amp; Validate</button>
86
+ </div>
87
+ <small class="form-hint">Your key is stored locally and never shared. <a href="https://openrouter.ai/keys" target="_blank">Get a key →</a></small>
88
+ </div>
89
+ </div>
90
+ <div id="gsKeyStatus" class="config-status"></div>
91
+ </div>
92
+
93
+ <!-- Balance Display (shown after validation) -->
94
+ <div class="gs-balance" id="gsBalance" style="display:none">
95
+ <div class="gs-balance-grid">
96
+ <div class="gs-balance-item">
97
+ <span class="gs-balance-label">Balance</span>
98
+ <span class="gs-balance-value" id="gsBalanceCredits">--</span>
99
+ </div>
100
+ <div class="gs-balance-item">
101
+ <span class="gs-balance-label">Usage</span>
102
+ <span class="gs-balance-value" id="gsBalanceUsage">--</span>
103
+ </div>
104
+ <div class="gs-balance-item">
105
+ <span class="gs-balance-label">Remaining</span>
106
+ <span class="gs-balance-value gs-balance-green" id="gsBalanceRemaining">--</span>
107
+ </div>
108
+ <div class="gs-balance-item">
109
+ <span class="gs-balance-label">Rate Limit</span>
110
+ <span class="gs-balance-value" id="gsBalanceRate">--</span>
111
+ </div>
112
+ </div>
113
+ <div class="gs-connected-badge">
114
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--green)" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
115
+ <span>Connected</span>
116
+ <button class="btn-link gs-change-key" onclick="showApiKeyInput()">Change key</button>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- 3-Step Flow -->
121
+ <div class="gs-steps">
122
+ <div class="gs-step" id="gsStep1">
123
+ <div class="gs-step-num">1</div>
76
124
  <div>
77
- <div class="hiw-step-title">Analyze</div>
78
- <div class="hiw-step-desc">Parse your session logs to classify tasks, track costs, and break down model usage across all your OpenClaw agents.</div>
125
+ <div class="gs-step-title">Analyze</div>
126
+ <div class="gs-step-desc">Parse session logs to classify tasks, track costs, and break down model usage.</div>
79
127
  </div>
80
128
  </div>
81
- <div class="hiw-step">
82
- <div class="hiw-step-num">2</div>
129
+ <div class="gs-step" id="gsStep2">
130
+ <div class="gs-step-num">2</div>
83
131
  <div>
84
- <div class="hiw-step-title">Evaluate</div>
85
- <div class="hiw-step-desc">Compare models side-by-side. See which models cost the most, which handle the most tasks, and where cheaper alternatives can save money without losing quality.</div>
132
+ <div class="gs-step-title">Evaluate</div>
133
+ <div class="gs-step-desc">Compare models side-by-side see which cost the most and where cheaper alternatives save money.</div>
86
134
  </div>
87
135
  </div>
88
- <div class="hiw-step">
89
- <div class="hiw-step-num">3</div>
136
+ <div class="gs-step" id="gsStep3">
137
+ <div class="gs-step-num">3</div>
90
138
  <div>
91
- <div class="hiw-step-title">Guide</div>
92
- <div class="hiw-step-desc">Get actionable recommendations — model switches, caching strategies, budget controls, and specialized agent configs — then apply them directly to your OpenClaw setup.</div>
139
+ <div class="gs-step-title">Guide</div>
140
+ <div class="gs-step-desc">Get actionable recommendations — model switches, caching strategies, and budget controls.</div>
93
141
  </div>
94
142
  </div>
95
143
  </div>
96
- <div class="hiw-note">
97
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
98
- <span>Requires an <strong>OpenRouter API key</strong>. Go to <a href="#" onclick="navigateTo('openrouter'); return false;">OpenRouter settings</a> to connect your account.</span>
99
- </div>
100
144
  </div>
101
145
 
102
146
  <!-- Info Banner -->
@@ -252,45 +252,163 @@ a:hover { color: var(--accent-hover); }
252
252
  .alert-info p { color: var(--text-secondary); margin-top: 2px; }
253
253
 
254
254
  /* ===== How It Works Banner ===== */
255
- .how-it-works-banner {
255
+ /* ===== Get Started Card ===== */
256
+ .get-started-card {
256
257
  background: var(--bg-card);
257
258
  border: 1px solid var(--border);
258
259
  border-radius: var(--radius-lg);
259
- padding: 22px 24px;
260
+ padding: 24px;
260
261
  margin-bottom: 20px;
261
262
  }
262
- .hiw-header {
263
+ .gs-header {
263
264
  display: flex;
264
- align-items: center;
265
- gap: 8px;
266
- font-size: 15px;
265
+ align-items: flex-start;
266
+ justify-content: space-between;
267
+ margin-bottom: 18px;
268
+ }
269
+ .gs-header-left {
270
+ display: flex;
271
+ gap: 12px;
272
+ align-items: flex-start;
273
+ }
274
+ .gs-header-left svg {
275
+ flex-shrink: 0;
276
+ margin-top: 2px;
277
+ }
278
+ .gs-title {
279
+ font-size: 16px;
267
280
  color: var(--text-primary);
268
- margin-bottom: 10px;
281
+ display: block;
282
+ margin-bottom: 4px;
269
283
  }
270
- .hiw-desc {
284
+ .gs-subtitle {
271
285
  font-size: 13px;
272
286
  color: var(--text-secondary);
273
- line-height: 1.6;
274
- margin-bottom: 16px;
287
+ line-height: 1.5;
288
+ }
289
+
290
+ /* API Key Section */
291
+ .gs-key-section {
292
+ background: var(--bg-surface);
293
+ border: 1px solid var(--border);
294
+ border-radius: var(--radius);
295
+ padding: 18px 20px;
296
+ margin-bottom: 18px;
297
+ }
298
+ .gs-key-row {
299
+ display: flex;
300
+ gap: 12px;
301
+ align-items: flex-end;
302
+ }
303
+ .gs-key-input-wrap {
304
+ flex: 1;
305
+ }
306
+ .gs-key-label {
307
+ display: block;
308
+ font-size: 12px;
309
+ font-weight: 600;
310
+ color: var(--text-secondary);
311
+ margin-bottom: 6px;
312
+ }
313
+ .gs-key-field {
314
+ display: flex;
315
+ gap: 10px;
316
+ }
317
+ .gs-input {
318
+ flex: 1;
319
+ min-width: 0;
320
+ }
321
+ .gs-validate-btn {
322
+ white-space: nowrap;
323
+ flex-shrink: 0;
324
+ }
325
+
326
+ /* Balance Display */
327
+ .gs-balance {
328
+ background: var(--bg-surface);
329
+ border: 1px solid var(--border);
330
+ border-radius: var(--radius);
331
+ padding: 18px 20px;
332
+ margin-bottom: 18px;
333
+ }
334
+ .gs-balance-grid {
335
+ display: grid;
336
+ grid-template-columns: repeat(4, 1fr);
337
+ gap: 16px;
338
+ margin-bottom: 12px;
339
+ }
340
+ .gs-balance-item {
341
+ display: flex;
342
+ flex-direction: column;
343
+ gap: 4px;
344
+ }
345
+ .gs-balance-label {
346
+ font-size: 11px;
347
+ font-weight: 600;
348
+ color: var(--text-muted);
349
+ text-transform: uppercase;
350
+ letter-spacing: .5px;
275
351
  }
276
- .hiw-desc strong {
352
+ .gs-balance-value {
353
+ font-size: 18px;
354
+ font-weight: 700;
277
355
  color: var(--text-primary);
356
+ font-family: var(--font-mono);
357
+ }
358
+ .gs-balance-green {
359
+ color: var(--green);
360
+ }
361
+ .gs-connected-badge {
362
+ display: flex;
363
+ align-items: center;
364
+ gap: 6px;
365
+ font-size: 12px;
366
+ font-weight: 600;
367
+ color: var(--green);
278
368
  }
279
- .hiw-steps {
369
+ .gs-change-key {
370
+ margin-left: 8px;
371
+ color: var(--text-muted);
372
+ font-size: 11px;
373
+ font-weight: 400;
374
+ cursor: pointer;
375
+ text-decoration: underline;
376
+ text-underline-offset: 2px;
377
+ }
378
+ .gs-change-key:hover {
379
+ color: var(--accent);
380
+ }
381
+ .btn-link {
382
+ background: none;
383
+ border: none;
384
+ padding: 0;
385
+ font-family: inherit;
386
+ cursor: pointer;
387
+ }
388
+
389
+ /* 3-Step Flow */
390
+ .gs-steps {
280
391
  display: grid;
281
392
  grid-template-columns: repeat(3, 1fr);
282
393
  gap: 14px;
283
- margin-bottom: 14px;
284
394
  }
285
- .hiw-step {
395
+ .gs-step {
286
396
  display: flex;
287
397
  gap: 12px;
288
398
  padding: 14px;
289
399
  background: var(--bg-surface);
290
400
  border-radius: var(--radius);
291
401
  border: 1px solid var(--border);
402
+ transition: border-color .2s;
403
+ }
404
+ .gs-step.gs-step-active {
405
+ border-color: var(--accent);
406
+ background: var(--accent-subtle);
292
407
  }
293
- .hiw-step-num {
408
+ .gs-step.gs-step-done {
409
+ border-color: var(--green);
410
+ }
411
+ .gs-step-num {
294
412
  flex-shrink: 0;
295
413
  width: 28px;
296
414
  height: 28px;
@@ -303,40 +421,32 @@ a:hover { color: var(--accent-hover); }
303
421
  font-size: 13px;
304
422
  font-weight: 700;
305
423
  }
306
- .hiw-step-title {
424
+ .gs-step-done .gs-step-num {
425
+ background: var(--green-subtle);
426
+ color: var(--green);
427
+ }
428
+ .gs-step-title {
307
429
  font-size: 13px;
308
430
  font-weight: 700;
309
431
  color: var(--text-primary);
310
432
  margin-bottom: 4px;
311
433
  }
312
- .hiw-step-desc {
434
+ .gs-step-desc {
313
435
  font-size: 12px;
314
436
  color: var(--text-muted);
315
437
  line-height: 1.5;
316
438
  }
317
- .hiw-note {
318
- display: flex;
319
- align-items: center;
320
- gap: 8px;
321
- font-size: 12px;
322
- color: var(--text-muted);
323
- padding: 10px 14px;
324
- background: var(--amber-subtle);
325
- border-radius: var(--radius-sm);
326
- border: 1px solid rgba(245,158,11,.15);
327
- }
328
- .hiw-note strong {
329
- color: var(--amber);
330
- }
331
- .hiw-note a {
332
- color: var(--amber);
333
- font-weight: 600;
334
- text-decoration: underline;
335
- text-underline-offset: 2px;
336
- }
337
- .hiw-note svg {
338
- flex-shrink: 0;
339
- color: var(--amber);
439
+
440
+ @media (max-width: 768px) {
441
+ .gs-steps {
442
+ grid-template-columns: 1fr;
443
+ }
444
+ .gs-balance-grid {
445
+ grid-template-columns: repeat(2, 1fr);
446
+ }
447
+ .gs-key-field {
448
+ flex-direction: column;
449
+ }
340
450
  }
341
451
 
342
452
  /* ===== Model Rec Explainer ===== */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-smartmeter",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "AI cost optimization for OpenClaw - analyze usage and reduce costs by 48%",
5
5
  "main": "src/cli/index.js",
6
6
  "bin": {
@@ -54,4 +54,4 @@
54
54
  "fs-extra": "^11.2.0",
55
55
  "open": "^9.1.0"
56
56
  }
57
- }
57
+ }