openclaw-smartmeter 0.4.0 → 0.5.0

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) {
@@ -111,16 +112,17 @@ function normalizeApiData(api) {
111
112
  /* ─── Render All ─── */
112
113
  function renderAll() {
113
114
  if (!analysisData) return;
114
- updateKPIs();
115
- updateMetrics();
116
- updateCharts();
117
- updateRecommendations();
118
- updateModelRecommendations();
119
- updateOtherRecommendations();
120
- updateBudgetControls();
121
- updateModelDetails();
122
- updateLastUpdated();
123
- checkCostDataNotice();
115
+ const safe = fn => { try { fn(); } catch (e) { console.error(`[SmartMeter] ${fn.name} error:`, e); } };
116
+ safe(updateKPIs);
117
+ safe(updateMetrics);
118
+ safe(updateCharts);
119
+ safe(updateRecommendations);
120
+ safe(updateModelRecommendations);
121
+ safe(updateOtherRecommendations);
122
+ safe(updateBudgetControls);
123
+ safe(updateModelDetails);
124
+ safe(updateLastUpdated);
125
+ safe(checkCostDataNotice);
124
126
  }
125
127
 
126
128
  /* ─── KPIs ─── */
@@ -440,7 +442,7 @@ function updateModelRecommendations() {
440
442
  ${alt.isRecommended ? '<span class="model-tag best-tag">RECOMMENDED</span>' : ''}
441
443
  </div>
442
444
  <div class="model-alt-meta">
443
- Quality: ${renderQualityDots(alt.quality_score)} · ${escHtml(alt.speed)} · Best for: ${alt.best_for.join(', ')}
445
+ Quality: ${renderQualityDots(alt.quality_score)} · ${escHtml(alt.speed || '')} · Best for: ${(alt.best_for || []).join(', ')}
444
446
  </div>
445
447
  </div>
446
448
  <div class="model-alt-cost">
@@ -827,6 +829,161 @@ function updateModelDetails() {
827
829
  </table>`;
828
830
  }
829
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
+ const usage = usageData.usage || 0;
929
+ const limit = usageData.limit || usageData.credit || 0;
930
+ const remaining = limit - usage;
931
+ const rate = usageData.rate_limit?.requests || usageData.rateLimit?.requests || '--';
932
+
933
+ setText('gsBalanceCredits', `$${limit.toFixed(2)}`);
934
+ setText('gsBalanceUsage', `$${usage.toFixed(2)}`);
935
+ setText('gsBalanceRemaining', `$${remaining.toFixed(2)}`);
936
+ setText('gsBalanceRate', typeof rate === 'number' ? `${rate}/s` : rate);
937
+ }
938
+
939
+ // Highlight step 1 as done
940
+ const step1 = document.getElementById('gsStep1');
941
+ if (step1) { step1.classList.add('gs-step-done'); }
942
+ }
943
+
944
+ function showApiKeyInput() {
945
+ const keySection = document.getElementById('gsKeySection');
946
+ const balanceSection = document.getElementById('gsBalance');
947
+ keySection.style.display = 'block';
948
+ balanceSection.style.display = 'none';
949
+ document.getElementById('gsApiKeyInput').focus();
950
+ }
951
+
952
+ /** On init, check if key is already stored and auto-show balance */
953
+ async function initGetStartedCard() {
954
+ const stored = localStorage.getItem('smartmeter_openrouter_key');
955
+ if (!stored) return;
956
+
957
+ // Pre-fill the input
958
+ const input = document.getElementById('gsApiKeyInput');
959
+ if (input) input.value = stored;
960
+
961
+ // Try to fetch balance
962
+ let usageData = null;
963
+ try {
964
+ const res = await fetch(`${API_BASE_URL}/api/openrouter-usage`);
965
+ const json = await res.json();
966
+ if (json.success && json.configured) {
967
+ usageData = json.data || json;
968
+ }
969
+ } catch {
970
+ // Try direct OpenRouter
971
+ try {
972
+ const res = await fetch('https://openrouter.ai/api/v1/auth/key', {
973
+ headers: { 'Authorization': `Bearer ${stored}` }
974
+ });
975
+ if (res.ok) {
976
+ const data = await res.json();
977
+ if (data && data.data) usageData = data.data;
978
+ }
979
+ } catch {}
980
+ }
981
+
982
+ if (usageData) {
983
+ showBalanceDisplay(usageData);
984
+ }
985
+ }
986
+
830
987
  /* ─── OpenRouter Integration ─── */
831
988
  async function checkOpenRouterConfig() {
832
989
  try {
@@ -877,10 +1034,13 @@ async function fetchOpenRouterUsage() {
877
1034
  /* ─── Config Modal ─── */
878
1035
  function openConfigModal() {
879
1036
  document.getElementById('configModal').style.display = 'flex';
880
- document.getElementById('apiKeyInput').focus();
1037
+ const input = document.getElementById('apiKeyInput');
1038
+ const stored = localStorage.getItem('smartmeter_openrouter_key');
1039
+ if (stored && !input.value) input.value = stored;
1040
+ input.focus();
881
1041
  const status = document.getElementById('configStatus');
882
1042
  status.className = 'config-status';
883
- status.style.display = 'none';
1043
+ status.style.removeProperty('display');
884
1044
  }
885
1045
  function closeConfigModal() {
886
1046
  document.getElementById('configModal').style.display = 'none';
@@ -890,22 +1050,28 @@ async function saveApiKey() {
890
1050
  const key = document.getElementById('apiKeyInput').value.trim();
891
1051
  const status = document.getElementById('configStatus');
892
1052
 
1053
+ function showStatus(msg, type) {
1054
+ status.textContent = msg;
1055
+ status.className = 'config-status ' + type;
1056
+ status.style.removeProperty('display');
1057
+ }
1058
+
893
1059
  if (!key) {
894
- status.textContent = 'Please enter an API key.';
895
- status.className = 'config-status error';
1060
+ showStatus('Please enter an API key.', 'error');
896
1061
  return;
897
1062
  }
898
1063
 
899
1064
  if (!key.startsWith('sk-or-')) {
900
- status.textContent = 'Invalid format — key should start with sk-or-';
901
- status.className = 'config-status error';
1065
+ showStatus('Invalid format — key should start with sk-or-', 'error');
902
1066
  return;
903
1067
  }
904
1068
 
905
- status.textContent = 'Validating…';
906
- status.className = 'config-status';
907
- status.style.display = 'block';
1069
+ showStatus('Validating…', 'validating');
908
1070
 
1071
+ let validated = false;
1072
+ let errorMsg = '';
1073
+
1074
+ // Try API server first
909
1075
  try {
910
1076
  const res = await fetch(`${API_BASE_URL}/api/config/openrouter-key`, {
911
1077
  method: 'POST',
@@ -914,20 +1080,41 @@ async function saveApiKey() {
914
1080
  });
915
1081
  const json = await res.json();
916
1082
  if (json.success) {
917
- status.textContent = '✅ API key saved and validated!';
918
- status.className = 'config-status success';
919
- setTimeout(() => {
920
- closeConfigModal();
921
- fetchOpenRouterUsage();
922
- navigateTo('openrouter');
923
- }, 1200);
1083
+ validated = true;
924
1084
  } else {
925
- status.textContent = `❌ ${json.error || 'Validation failed'}`;
926
- status.className = 'config-status error';
1085
+ errorMsg = json.error || 'Validation failed';
927
1086
  }
928
- } catch (err) {
929
- status.textContent = `❌ Network error: ${err.message}`;
930
- status.className = 'config-status error';
1087
+ } catch (_) {
1088
+ // API server not available — validate directly against OpenRouter
1089
+ try {
1090
+ const res = await fetch('https://openrouter.ai/api/v1/auth/key', {
1091
+ headers: { 'Authorization': `Bearer ${key}` }
1092
+ });
1093
+ if (res.ok) {
1094
+ const data = await res.json();
1095
+ validated = !!(data && data.data);
1096
+ if (!validated) errorMsg = 'Key not recognized by OpenRouter';
1097
+ } else if (res.status === 401 || res.status === 403) {
1098
+ errorMsg = 'Invalid API key — authentication failed';
1099
+ } else {
1100
+ errorMsg = `OpenRouter returned status ${res.status}`;
1101
+ }
1102
+ } catch (e2) {
1103
+ errorMsg = 'Could not reach OpenRouter to validate — check your connection';
1104
+ }
1105
+ }
1106
+
1107
+ if (validated) {
1108
+ localStorage.setItem('smartmeter_openrouter_key', key);
1109
+ showStatus('✅ API key saved and validated!', 'success');
1110
+ setTimeout(() => {
1111
+ closeConfigModal();
1112
+ fetchOpenRouterUsage();
1113
+ // Also sync the Get Started card
1114
+ initGetStartedCard();
1115
+ }, 1200);
1116
+ } else {
1117
+ showStatus(`❌ ${errorMsg}`, 'error');
931
1118
  }
932
1119
  }
933
1120
 
@@ -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;
275
344
  }
276
- .hiw-desc strong {
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;
351
+ }
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);
368
+ }
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;
278
377
  }
279
- .hiw-steps {
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 ===== */
@@ -860,6 +970,11 @@ a:hover { color: var(--accent-hover); }
860
970
  background: var(--green-subtle);
861
971
  color: var(--green);
862
972
  }
973
+ .config-status.validating {
974
+ display: block;
975
+ background: var(--indigo-subtle, rgba(99,102,241,.08));
976
+ color: var(--text-secondary);
977
+ }
863
978
 
864
979
  /* Code block */
865
980
  .code-block {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-smartmeter",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
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
+ }