openclaw-smartmeter 0.5.1 → 0.5.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.
@@ -123,6 +123,7 @@ function renderAll() {
123
123
  safe(updateModelDetails);
124
124
  safe(updateLastUpdated);
125
125
  safe(checkCostDataNotice);
126
+ safe(() => populateStepPanel(activeStep));
126
127
  }
127
128
 
128
129
  /* ─── KPIs ─── */
@@ -991,6 +992,228 @@ async function initGetStartedCard() {
991
992
  }
992
993
  }
993
994
 
995
+ /* ─── Step Panels (Analyze / Evaluate / Guide) ─── */
996
+ let activeStep = 1;
997
+
998
+ function activateStep(n) {
999
+ activeStep = n;
1000
+ // Toggle active class on step elements
1001
+ for (let i = 1; i <= 3; i++) {
1002
+ const step = document.getElementById('gsStep' + i);
1003
+ if (step) step.classList.toggle('gs-step-active', i === n);
1004
+ }
1005
+ // Show/hide panels
1006
+ for (let i = 1; i <= 3; i++) {
1007
+ const panel = document.getElementById('gsPanel' + i);
1008
+ if (panel) panel.style.display = (i === n) ? '' : 'none';
1009
+ }
1010
+ // Populate panel content
1011
+ populateStepPanel(n);
1012
+ // Update contextual info banner
1013
+ updateContextualBanner(n);
1014
+ }
1015
+
1016
+ function populateStepPanel(n) {
1017
+ const d = analysisData;
1018
+ if (n === 1) {
1019
+ const panel = document.getElementById('gsPanel1');
1020
+ if (!panel) return;
1021
+ if (!d) {
1022
+ panel.innerHTML = '<p class="panel-empty">Run an analysis to see your usage summary here.</p>';
1023
+ return;
1024
+ }
1025
+ const days = d.days_analyzed || 0;
1026
+ const tasks = d.total_tasks || 0;
1027
+ const current = d.monthly_projected_current || 0;
1028
+ const optimized = d.monthly_projected_optimized || 0;
1029
+ const models = (d.model_breakdown || []).length;
1030
+ panel.innerHTML = `
1031
+ <h4>Analysis Summary</h4>
1032
+ <p>${days} day${days !== 1 ? 's' : ''} analyzed &middot; ${tasks} task${tasks !== 1 ? 's' : ''} &middot; ${models} model${models !== 1 ? 's' : ''} detected</p>
1033
+ <table>
1034
+ <tr><td>Projected Monthly Cost</td><td style="text-align:right;font-weight:600">$${current.toFixed(2)}</td></tr>
1035
+ <tr><td>Optimized Projection</td><td style="text-align:right;font-weight:600;color:var(--green)">$${optimized.toFixed(2)}</td></tr>
1036
+ <tr><td>Potential Savings</td><td style="text-align:right;font-weight:600;color:var(--accent)">$${(current - optimized).toFixed(2)}</td></tr>
1037
+ </table>`;
1038
+ } else if (n === 2) {
1039
+ const panel = document.getElementById('gsPanel2');
1040
+ if (!panel) return;
1041
+ const models = d ? (d.model_breakdown || []) : [];
1042
+ if (models.length === 0) {
1043
+ panel.innerHTML = '<p class="panel-empty">No model data available yet. Run an analysis first.</p>';
1044
+ return;
1045
+ }
1046
+ const totalCost = models.reduce((s, m) => s + m.cost, 0);
1047
+ panel.innerHTML = `
1048
+ <h4>Model Cost Breakdown</h4>
1049
+ <table>
1050
+ <thead><tr>
1051
+ <th>Model</th><th>Tasks</th><th>Cost</th><th>Avg/Task</th><th>Share</th>
1052
+ </tr></thead>
1053
+ <tbody>
1054
+ ${models.map(m => {
1055
+ const share = totalCost > 0 ? ((m.cost / totalCost) * 100).toFixed(1) : '0.0';
1056
+ return `<tr>
1057
+ <td>${escHtml(m.model)}</td>
1058
+ <td>${m.tasks}</td>
1059
+ <td>$${m.cost.toFixed(2)}</td>
1060
+ <td>$${m.avg_cost_per_task.toFixed(3)}</td>
1061
+ <td>${share}%</td>
1062
+ </tr>`;
1063
+ }).join('')}
1064
+ </tbody>
1065
+ </table>`;
1066
+ } else if (n === 3) {
1067
+ const panel = document.getElementById('gsPanel3');
1068
+ if (!panel) return;
1069
+ const recs = d ? (d.recommendations || []) : [];
1070
+ if (recs.length === 0) {
1071
+ panel.innerHTML = '<p class="panel-empty">No recommendations available yet. Run an analysis first.</p>';
1072
+ return;
1073
+ }
1074
+ panel.innerHTML = `
1075
+ <h4>Recommendations</h4>
1076
+ <p>Toggle on the optimizations you want to activate, then hit Apply.</p>
1077
+ <div class="gs-rec-list">
1078
+ ${recs.map((r, i) => {
1079
+ const impactClass = (r.impact || '').toLowerCase().includes('high') ? 'high'
1080
+ : (r.impact || '').toLowerCase().includes('medium') ? 'medium' : 'low';
1081
+ const checked = editedRecommendations[i] && editedRecommendations[i].selected ? 'checked' : '';
1082
+ return `<label class="gs-rec-row" data-index="${i}">
1083
+ <input type="checkbox" class="gs-rec-toggle" ${checked} onchange="toggleGuideRec(${i}, this.checked)">
1084
+ <div class="gs-rec-body">
1085
+ <div class="gs-rec-header">
1086
+ <span class="rec-title">${escHtml(r.title)}</span>
1087
+ ${r.impact ? `<span class="rec-impact ${impactClass}">${escHtml(r.impact)}</span>` : ''}
1088
+ </div>
1089
+ <div class="rec-desc">${escHtml(r.description)}</div>
1090
+ </div>
1091
+ </label>`;
1092
+ }).join('')}
1093
+ </div>
1094
+ <div class="gs-rec-actions">
1095
+ <button class="btn btn-primary" onclick="applyGuideRecommendations()">Apply Selected</button>
1096
+ <span class="gs-rec-count" id="gsRecCount">${Object.values(editedRecommendations).filter(v => v.selected).length} selected</span>
1097
+ </div>`;
1098
+ }
1099
+ }
1100
+
1101
+ function updateContextualBanner(n) {
1102
+ const notice = document.getElementById('costDataNotice');
1103
+ const content = document.getElementById('costDataNoticeContent');
1104
+ if (!notice || !content) return;
1105
+
1106
+ const messages = {
1107
+ 1: {
1108
+ title: 'About Cost Tracking',
1109
+ text: 'Your OpenRouter API responses may not include cost data. SmartMeter still optimizes based on token usage.'
1110
+ },
1111
+ 2: {
1112
+ title: 'Understanding Model Costs',
1113
+ text: 'Costs are based on token usage reported by OpenRouter. Compare models to find cheaper alternatives for similar tasks.'
1114
+ },
1115
+ 3: {
1116
+ title: 'Applying Recommendations',
1117
+ text: 'Select recommendations below to apply them. Changes update your OpenClaw configuration for optimized model routing.'
1118
+ }
1119
+ };
1120
+
1121
+ const msg = messages[n] || messages[1];
1122
+ const d = analysisData;
1123
+ const hasCostData = d && (d.monthly_projected_current || 0) > 0;
1124
+
1125
+ // Show banner contextually: always for step 2/3 if data exists, or step 1 if no cost data
1126
+ if (n === 1 && hasCostData) {
1127
+ notice.style.display = 'none';
1128
+ } else {
1129
+ content.innerHTML = `<strong>${msg.title}</strong><p>${msg.text}</p>`;
1130
+ notice.style.display = 'flex';
1131
+ }
1132
+ }
1133
+
1134
+ /* ─── Guide Panel Actions ─── */
1135
+ function toggleGuideRec(i, on) {
1136
+ if (!editedRecommendations[i]) editedRecommendations[i] = {};
1137
+ editedRecommendations[i].selected = on;
1138
+ // Update count
1139
+ const count = Object.values(editedRecommendations).filter(v => v.selected).length;
1140
+ const el = document.getElementById('gsRecCount');
1141
+ if (el) el.textContent = count + ' selected';
1142
+ // Sync main recommendation cards if they exist
1143
+ const card = document.querySelector(`.rec-card[data-index="${i}"]`);
1144
+ if (card) card.classList.toggle('selected', on);
1145
+ }
1146
+
1147
+ function applyGuideRecommendations() {
1148
+ const selected = Object.entries(editedRecommendations)
1149
+ .filter(([_, v]) => v.selected)
1150
+ .map(([i]) => parseInt(i));
1151
+
1152
+ if (selected.length === 0) {
1153
+ showToast('Toggle at least one recommendation to apply');
1154
+ return;
1155
+ }
1156
+
1157
+ // Build summary list for the modal
1158
+ const recs = analysisData ? (analysisData.recommendations || []) : [];
1159
+ const listEl = document.getElementById('applyModalList');
1160
+ if (listEl) {
1161
+ listEl.innerHTML = selected.map(i => {
1162
+ const r = recs[i];
1163
+ if (!r) return '';
1164
+ const impactClass = (r.impact || '').toLowerCase().includes('high') ? 'high'
1165
+ : (r.impact || '').toLowerCase().includes('medium') ? 'medium' : 'low';
1166
+ return `<div class="apply-modal-item">
1167
+ <span class="apply-modal-dot ${impactClass}"></span>
1168
+ <span>${escHtml(r.title)}</span>
1169
+ ${r.impact ? `<span class="rec-impact ${impactClass}">${escHtml(r.impact)}</span>` : ''}
1170
+ </div>`;
1171
+ }).join('');
1172
+ }
1173
+
1174
+ // Show modal
1175
+ const modal = document.getElementById('applyModal');
1176
+ if (modal) modal.style.display = 'flex';
1177
+ }
1178
+
1179
+ function closeApplyModal() {
1180
+ const modal = document.getElementById('applyModal');
1181
+ if (modal) modal.style.display = 'none';
1182
+ }
1183
+
1184
+ async function confirmApplyRecommendations() {
1185
+ const selected = Object.entries(editedRecommendations)
1186
+ .filter(([_, v]) => v.selected)
1187
+ .map(([i]) => parseInt(i));
1188
+
1189
+ const btn = document.getElementById('applyModalConfirmBtn');
1190
+ if (btn) { btn.disabled = true; btn.textContent = 'Applying…'; }
1191
+
1192
+ try {
1193
+ const res = await fetch(`${API_BASE_URL}/api/apply`, {
1194
+ method: 'POST',
1195
+ headers: { 'Content-Type': 'application/json' },
1196
+ body: JSON.stringify({ confirm: true })
1197
+ });
1198
+ const json = await res.json();
1199
+ if (json.success) {
1200
+ closeApplyModal();
1201
+ showToast(`✅ ${selected.length} optimization(s) applied!`);
1202
+ selected.forEach(i => {
1203
+ if (editedRecommendations[i]) editedRecommendations[i].selected = false;
1204
+ });
1205
+ populateStepPanel(3);
1206
+ updateRecommendations();
1207
+ } else {
1208
+ showToast(`Error: ${json.error || 'Apply failed'}`);
1209
+ }
1210
+ } catch (err) {
1211
+ showToast(`Network error: ${err.message}`);
1212
+ } finally {
1213
+ if (btn) { btn.disabled = false; btn.textContent = 'Apply Now'; }
1214
+ }
1215
+ }
1216
+
994
1217
  /* ─── OpenRouter Integration ─── */
995
1218
  async function checkOpenRouterConfig() {
996
1219
  try {
@@ -1270,16 +1493,8 @@ async function refreshDashboard() {
1270
1493
 
1271
1494
  /* ─── Cost Data Notice ─── */
1272
1495
  function checkCostDataNotice() {
1273
- const d = analysisData;
1274
- if (!d) return;
1275
- const hasCostData = (d.monthly_projected_current || 0) > 0;
1276
- const notice = document.getElementById('costDataNotice');
1277
- if (notice) {
1278
- // Only show when cost data is truly missing — keep hidden otherwise
1279
- if (!hasCostData) {
1280
- notice.style.display = 'flex';
1281
- }
1282
- }
1496
+ // Delegate to contextual banner system
1497
+ updateContextualBanner(activeStep);
1283
1498
  }
1284
1499
 
1285
1500
  /* ─── Helpers ─── */
@@ -119,21 +119,21 @@
119
119
 
120
120
  <!-- 3-Step Flow -->
121
121
  <div class="gs-steps">
122
- <div class="gs-step" id="gsStep1">
122
+ <div class="gs-step gs-step-active" id="gsStep1" onclick="activateStep(1)">
123
123
  <div class="gs-step-num">1</div>
124
124
  <div>
125
125
  <div class="gs-step-title">Analyze</div>
126
126
  <div class="gs-step-desc">Parse session logs to classify tasks, track costs, and break down model usage.</div>
127
127
  </div>
128
128
  </div>
129
- <div class="gs-step" id="gsStep2">
129
+ <div class="gs-step" id="gsStep2" onclick="activateStep(2)">
130
130
  <div class="gs-step-num">2</div>
131
131
  <div>
132
132
  <div class="gs-step-title">Evaluate</div>
133
133
  <div class="gs-step-desc">Compare models side-by-side — see which cost the most and where cheaper alternatives save money.</div>
134
134
  </div>
135
135
  </div>
136
- <div class="gs-step" id="gsStep3">
136
+ <div class="gs-step" id="gsStep3" onclick="activateStep(3)">
137
137
  <div class="gs-step-num">3</div>
138
138
  <div>
139
139
  <div class="gs-step-title">Guide</div>
@@ -141,12 +141,23 @@
141
141
  </div>
142
142
  </div>
143
143
  </div>
144
+
145
+ <!-- Step Detail Panels -->
146
+ <div class="gs-step-panel" id="gsPanel1">
147
+ <!-- Analyze: shown by default with analysis summary -->
148
+ </div>
149
+ <div class="gs-step-panel" id="gsPanel2" style="display:none">
150
+ <!-- Evaluate: model cost breakdown table -->
151
+ </div>
152
+ <div class="gs-step-panel" id="gsPanel3" style="display:none">
153
+ <!-- Guide: top recommendations -->
154
+ </div>
144
155
  </div>
145
156
 
146
- <!-- Info Banner -->
157
+ <!-- Contextual Info Banner -->
147
158
  <div class="alert alert-info" id="costDataNotice" style="display:none;">
148
159
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
149
- <div>
160
+ <div id="costDataNoticeContent">
150
161
  <strong>About Cost Tracking</strong>
151
162
  <p>Your OpenRouter API responses may not include cost data. SmartMeter still optimizes based on token usage.</p>
152
163
  </div>
@@ -160,7 +171,7 @@
160
171
  <div class="kpi-sub" id="savingsPercentage">--</div>
161
172
  </div>
162
173
  <div class="kpi-card">
163
- <div class="kpi-label">Current Cost</div>
174
+ <div class="kpi-label">Projected Monthly Cost</div>
164
175
  <div class="kpi-value" id="currentCost">$0.00<small>/mo</small></div>
165
176
  </div>
166
177
  <div class="kpi-card kpi-success">
@@ -402,6 +413,31 @@
402
413
  </div>
403
414
  </div>
404
415
 
416
+ <!-- Apply Recommendations Modal -->
417
+ <div id="applyModal" class="modal-overlay" style="display:none">
418
+ <div class="modal">
419
+ <div class="modal-header">
420
+ <h3>Apply Optimizations</h3>
421
+ <button class="btn-icon modal-close" onclick="closeApplyModal()">✕</button>
422
+ </div>
423
+ <div class="modal-body">
424
+ <p class="modal-desc">The following recommendations will be applied to your OpenClaw configuration at <code>~/.openclaw/openclaw.json</code>.</p>
425
+ <div id="applyModalList" class="apply-modal-list"></div>
426
+ <div class="apply-revert-info">
427
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
428
+ <div>
429
+ <strong>Easy to undo</strong>
430
+ <p>To revert, run <code>smartmeter reset</code> in your terminal, or edit <code>~/.openclaw/openclaw.json</code> directly. You can also toggle individual recommendations off here and re-apply.</p>
431
+ </div>
432
+ </div>
433
+ </div>
434
+ <div class="modal-footer">
435
+ <button class="btn btn-ghost" onclick="closeApplyModal()">Cancel</button>
436
+ <button class="btn btn-primary" id="applyModalConfirmBtn" onclick="confirmApplyRecommendations()">Apply Now</button>
437
+ </div>
438
+ </div>
439
+ </div>
440
+
405
441
  <!-- Preview Modal -->
406
442
  <div id="previewModal" class="modal-overlay" style="display:none">
407
443
  <div class="modal modal-lg">
@@ -399,7 +399,12 @@ a:hover { color: var(--accent-hover); }
399
399
  background: var(--bg-surface);
400
400
  border-radius: var(--radius);
401
401
  border: 1px solid var(--border);
402
- transition: border-color .2s;
402
+ transition: border-color .2s, background .2s;
403
+ cursor: pointer;
404
+ }
405
+ .gs-step:hover {
406
+ border-color: var(--border-light);
407
+ background: var(--bg-card-hover);
403
408
  }
404
409
  .gs-step.gs-step-active {
405
410
  border-color: var(--accent);
@@ -437,6 +442,167 @@ a:hover { color: var(--accent-hover); }
437
442
  line-height: 1.5;
438
443
  }
439
444
 
445
+ /* Step detail panels */
446
+ .gs-step-panel {
447
+ margin-top: 16px;
448
+ background: var(--bg-surface);
449
+ border: 1px solid var(--border);
450
+ border-radius: var(--radius);
451
+ padding: 18px;
452
+ animation: fadeIn .25s ease;
453
+ }
454
+ .gs-step-panel h4 {
455
+ font-size: 14px;
456
+ font-weight: 700;
457
+ color: var(--text-primary);
458
+ margin: 0 0 12px 0;
459
+ }
460
+ .gs-step-panel p {
461
+ font-size: 13px;
462
+ color: var(--text-secondary);
463
+ margin: 0 0 12px 0;
464
+ line-height: 1.5;
465
+ }
466
+ .gs-step-panel table {
467
+ width: 100%;
468
+ border-collapse: collapse;
469
+ font-size: 13px;
470
+ }
471
+ .gs-step-panel th {
472
+ text-align: left;
473
+ font-weight: 600;
474
+ color: var(--text-muted);
475
+ padding: 6px 10px;
476
+ border-bottom: 1px solid var(--border);
477
+ font-size: 11px;
478
+ text-transform: uppercase;
479
+ letter-spacing: .5px;
480
+ }
481
+ .gs-step-panel td {
482
+ padding: 8px 10px;
483
+ color: var(--text-primary);
484
+ border-bottom: 1px solid var(--border-subtle, rgba(255,255,255,0.04));
485
+ }
486
+ .gs-step-panel tr:last-child td {
487
+ border-bottom: none;
488
+ }
489
+ .gs-step-panel .panel-empty {
490
+ color: var(--text-muted);
491
+ font-style: italic;
492
+ font-size: 13px;
493
+ }
494
+ .gs-step-panel .rec-item {
495
+ padding: 10px 0;
496
+ border-bottom: 1px solid var(--border-subtle, rgba(255,255,255,0.04));
497
+ }
498
+ .gs-step-panel .rec-item:last-child {
499
+ border-bottom: none;
500
+ }
501
+ .gs-step-panel .rec-title {
502
+ font-weight: 600;
503
+ color: var(--text-primary);
504
+ font-size: 13px;
505
+ margin-bottom: 4px;
506
+ }
507
+ .gs-step-panel .rec-desc {
508
+ font-size: 12px;
509
+ color: var(--text-muted);
510
+ line-height: 1.5;
511
+ }
512
+ .gs-step-panel .rec-impact {
513
+ display: inline-block;
514
+ font-size: 11px;
515
+ font-weight: 600;
516
+ padding: 2px 8px;
517
+ border-radius: 10px;
518
+ margin-top: 4px;
519
+ }
520
+ .gs-step-panel .rec-impact.high {
521
+ background: var(--green-subtle);
522
+ color: var(--green);
523
+ }
524
+ .gs-step-panel .rec-impact.medium {
525
+ background: var(--accent-subtle);
526
+ color: var(--accent);
527
+ }
528
+ .gs-step-panel .rec-impact.low {
529
+ background: rgba(255,255,255,0.06);
530
+ color: var(--text-muted);
531
+ }
532
+
533
+ /* Guide panel – recommendation list */
534
+ .gs-rec-list {
535
+ display: flex;
536
+ flex-direction: column;
537
+ gap: 2px;
538
+ }
539
+ .gs-rec-row {
540
+ display: flex;
541
+ align-items: flex-start;
542
+ gap: 12px;
543
+ padding: 12px 14px;
544
+ border-radius: 8px;
545
+ cursor: pointer;
546
+ transition: background .15s;
547
+ }
548
+ .gs-rec-row:hover {
549
+ background: rgba(255,255,255,0.03);
550
+ }
551
+ .gs-rec-toggle {
552
+ appearance: none;
553
+ -webkit-appearance: none;
554
+ width: 38px;
555
+ min-width: 38px;
556
+ height: 22px;
557
+ border-radius: 11px;
558
+ background: var(--border);
559
+ position: relative;
560
+ cursor: pointer;
561
+ transition: background .2s;
562
+ margin-top: 2px;
563
+ }
564
+ .gs-rec-toggle::after {
565
+ content: '';
566
+ position: absolute;
567
+ top: 3px;
568
+ left: 3px;
569
+ width: 16px;
570
+ height: 16px;
571
+ border-radius: 50%;
572
+ background: var(--text-muted);
573
+ transition: transform .2s, background .2s;
574
+ }
575
+ .gs-rec-toggle:checked {
576
+ background: var(--accent);
577
+ }
578
+ .gs-rec-toggle:checked::after {
579
+ transform: translateX(16px);
580
+ background: #fff;
581
+ }
582
+ .gs-rec-body {
583
+ flex: 1;
584
+ min-width: 0;
585
+ }
586
+ .gs-rec-header {
587
+ display: flex;
588
+ align-items: center;
589
+ gap: 8px;
590
+ flex-wrap: wrap;
591
+ margin-bottom: 4px;
592
+ }
593
+ .gs-rec-actions {
594
+ display: flex;
595
+ align-items: center;
596
+ gap: 14px;
597
+ margin-top: 16px;
598
+ padding-top: 14px;
599
+ border-top: 1px solid var(--border);
600
+ }
601
+ .gs-rec-count {
602
+ font-size: 12px;
603
+ color: var(--text-muted);
604
+ }
605
+
440
606
  @media (max-width: 768px) {
441
607
  .gs-steps {
442
608
  grid-template-columns: 1fr;
@@ -924,6 +1090,67 @@ a:hover { color: var(--accent-hover); }
924
1090
  padding: 12px 24px 20px;
925
1091
  }
926
1092
 
1093
+ /* Apply Modal */
1094
+ .apply-modal-list {
1095
+ display: flex;
1096
+ flex-direction: column;
1097
+ gap: 8px;
1098
+ margin-bottom: 16px;
1099
+ max-height: 200px;
1100
+ overflow-y: auto;
1101
+ }
1102
+ .apply-modal-item {
1103
+ display: flex;
1104
+ align-items: center;
1105
+ gap: 10px;
1106
+ padding: 10px 12px;
1107
+ background: var(--bg-card);
1108
+ border: 1px solid var(--border);
1109
+ border-radius: var(--radius-sm);
1110
+ font-size: 13px;
1111
+ color: var(--text-primary);
1112
+ }
1113
+ .apply-modal-dot {
1114
+ flex-shrink: 0;
1115
+ width: 8px;
1116
+ height: 8px;
1117
+ border-radius: 50%;
1118
+ }
1119
+ .apply-modal-dot.high { background: var(--green); }
1120
+ .apply-modal-dot.medium { background: var(--accent); }
1121
+ .apply-modal-dot.low { background: var(--text-muted); }
1122
+ .apply-revert-info {
1123
+ display: flex;
1124
+ gap: 10px;
1125
+ padding: 12px 14px;
1126
+ background: var(--accent-subtle);
1127
+ border: 1px solid rgba(99,102,241,.2);
1128
+ border-radius: var(--radius-sm);
1129
+ font-size: 12px;
1130
+ color: var(--text-secondary);
1131
+ line-height: 1.5;
1132
+ }
1133
+ .apply-revert-info svg {
1134
+ flex-shrink: 0;
1135
+ margin-top: 2px;
1136
+ }
1137
+ .apply-revert-info strong {
1138
+ display: block;
1139
+ color: var(--text-primary);
1140
+ font-size: 13px;
1141
+ margin-bottom: 4px;
1142
+ }
1143
+ .apply-revert-info p {
1144
+ margin: 0;
1145
+ }
1146
+ .apply-revert-info code {
1147
+ background: rgba(255,255,255,.06);
1148
+ padding: 1px 5px;
1149
+ border-radius: 3px;
1150
+ font-size: 11px;
1151
+ font-family: var(--font-mono);
1152
+ }
1153
+
927
1154
  /* Form */
928
1155
  .form-label {
929
1156
  display: block;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-smartmeter",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
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": {