openclaw-smartmeter 0.2.2 → 0.3.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.
@@ -6,12 +6,15 @@ let modelChart = null;
6
6
  let taskChart = null;
7
7
  let activeTab = 'usage';
8
8
  let refreshInterval = null;
9
+ let openRouterConfigured = false;
10
+ let openRouterUsage = null;
9
11
 
10
12
  // Initialize dashboard on page load
11
13
  document.addEventListener('DOMContentLoaded', () => {
12
14
  console.log('SmartMeter Dashboard loading...');
13
15
  initializeDashboard();
14
16
  startAutoRefresh();
17
+ checkOpenRouterConfig();
15
18
  });
16
19
 
17
20
  // Initialize the dashboard
@@ -76,18 +79,45 @@ function updateHeroCard(data) {
76
79
  const savings = data.monthly_projected_current - data.monthly_projected_optimized;
77
80
  const savingsPercent = ((savings / data.monthly_projected_current) * 100).toFixed(1);
78
81
 
79
- document.getElementById('savingsAmount').textContent = `$${savings.toFixed(2)}/mo`;
80
- document.getElementById('savingsPercentage').textContent = `${savingsPercent}% savings`;
81
- document.getElementById('currentCost').textContent = `$${data.monthly_projected_current.toFixed(2)}/mo`;
82
- document.getElementById('optimizedCost').textContent = `$${data.monthly_projected_optimized.toFixed(2)}/mo`;
82
+ // Check if we have insufficient data for meaningful analysis
83
+ const hasInsufficientData = isInsufficientData(data);
83
84
 
84
- // Update confidence badge
85
- const badge = document.getElementById('confidenceBadge');
86
- const confidenceText = getConfidenceText(data.confidence_level, data.days_analyzed);
87
- badge.innerHTML = `
88
- <span class="confidence-icon">${getConfidenceIcon(data.confidence_level)}</span>
89
- <span class="confidence-text">${confidenceText}</span>
90
- `;
85
+ // Show cost data notice if costs are zero but we have usage
86
+ const costDataNotice = document.getElementById('costDataNotice');
87
+ if (data.monthly_projected_current === 0 && data.total_tasks > 0) {
88
+ costDataNotice.style.display = 'flex';
89
+ } else {
90
+ costDataNotice.style.display = 'none';
91
+ }
92
+
93
+ if (hasInsufficientData) {
94
+ // Show professional message about needing more data
95
+ document.getElementById('savingsAmount').textContent = '📊';
96
+ document.getElementById('savingsPercentage').textContent = 'Analyzing...';
97
+ document.getElementById('currentCost').innerHTML = '<span class="insufficient-data">Insufficient data</span>';
98
+ document.getElementById('optimizedCost').innerHTML = '<span class="insufficient-data">Gathering usage...</span>';
99
+
100
+ // Show helpful message in confidence badge
101
+ const badge = document.getElementById('confidenceBadge');
102
+ badge.innerHTML = `
103
+ <span class="confidence-icon">💡</span>
104
+ <span class="confidence-text">Need more usage data for accurate cost analysis (${data.total_tasks} tasks, ${data.days_analyzed} days so far)</span>
105
+ `;
106
+ } else {
107
+ // Normal display with actual costs
108
+ document.getElementById('savingsAmount').textContent = `$${savings.toFixed(2)}/mo`;
109
+ document.getElementById('savingsPercentage').textContent = `${savingsPercent}% savings`;
110
+ document.getElementById('currentCost').textContent = `$${data.monthly_projected_current.toFixed(2)}/mo`;
111
+ document.getElementById('optimizedCost').textContent = `$${data.monthly_projected_optimized.toFixed(2)}/mo`;
112
+
113
+ // Update confidence badge
114
+ const badge = document.getElementById('confidenceBadge');
115
+ const confidenceText = getConfidenceText(data.confidence_level, data.days_analyzed);
116
+ badge.innerHTML = `
117
+ <span class="confidence-icon">${getConfidenceIcon(data.confidence_level)}</span>
118
+ <span class="confidence-text">${confidenceText}</span>
119
+ `;
120
+ }
91
121
  }
92
122
 
93
123
  // Update stats cards
@@ -339,6 +369,18 @@ function generateTimelineDetails(data) {
339
369
  }
340
370
 
341
371
  // Helper functions
372
+ function isInsufficientData(data) {
373
+ // Insufficient data if:
374
+ // 1. Total costs are zero or near-zero (< $0.01)
375
+ // 2. Less than 5 tasks analyzed
376
+ // 3. Less than 1 day of data
377
+ const hasNoCosts = data.monthly_projected_current < 0.01;
378
+ const hasMinimalTasks = data.total_tasks < 5;
379
+ const hasMinimalDays = data.days_analyzed < 1;
380
+
381
+ return hasNoCosts || hasMinimalTasks || hasMinimalDays;
382
+ }
383
+
342
384
  function getConfidenceIcon(level) {
343
385
  const icons = {
344
386
  'high': '✅',
@@ -504,6 +546,219 @@ async function applyOptimizations() {
504
546
  }
505
547
  }
506
548
 
549
+ // ============================================
550
+ // OpenRouter Integration Functions
551
+ // ============================================
552
+
553
+ /**
554
+ * Check if OpenRouter API key is configured
555
+ */
556
+ async function checkOpenRouterConfig() {
557
+ // Always show OpenRouter section
558
+ document.getElementById('openRouterSection').style.display = 'block';
559
+
560
+ try {
561
+ const response = await fetch(`${API_BASE_URL}/config/openrouter-key`);
562
+ if (response.ok) {
563
+ const data = await response.json();
564
+ openRouterConfigured = data.configured;
565
+
566
+ if (openRouterConfigured) {
567
+ await fetchOpenRouterUsage();
568
+ } else {
569
+ // Show configure prompt
570
+ updateOpenRouterDisplay({ configured: false });
571
+ }
572
+ }
573
+ } catch (error) {
574
+ console.log('OpenRouter config check failed (API server may not be running):', error.message);
575
+ // Still show section with configuration prompt
576
+ updateOpenRouterDisplay({ configured: false });
577
+ }
578
+ }
579
+
580
+ /**
581
+ * Fetch actual OpenRouter usage from API
582
+ */
583
+ async function fetchOpenRouterUsage() {
584
+ try {
585
+ const response = await fetch(`${API_BASE_URL}/openrouter-usage`);
586
+ if (!response.ok) {
587
+ throw new Error('Failed to fetch OpenRouter usage');
588
+ }
589
+
590
+ const data = await response.json();
591
+ openRouterUsage = data;
592
+
593
+ updateOpenRouterDisplay(data);
594
+ } catch (error) {
595
+ console.error('Failed to fetch OpenRouter usage:', error);
596
+ updateOpenRouterDisplay({ success: false, error: error.message });
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Update OpenRouter usage display in dashboard
602
+ */
603
+ function updateOpenRouterDisplay(data) {
604
+ const content = document.getElementById('openRouterContent');
605
+
606
+ if (!data.configured) {
607
+ content.innerHTML = `
608
+ <div class="openrouter-notice">
609
+ <p>âš™ī¸ Configure your OpenRouter API key to view actual usage and compare with analyzed data.</p>
610
+ <button class="btn btn-primary" onclick="openConfigModal()">Configure API Key</button>
611
+ </div>
612
+ `;
613
+ return;
614
+ }
615
+
616
+ if (!data.success) {
617
+ content.innerHTML = `
618
+ <div class="openrouter-error">
619
+ <p>❌ Error fetching OpenRouter usage: ${data.error || 'Unknown error'}</p>
620
+ <button class="btn btn-secondary" onclick="openConfigModal()">Update API Key</button>
621
+ </div>
622
+ `;
623
+ return;
624
+ }
625
+
626
+ // Display actual usage
627
+ const totalSpent = data.totalSpent !== null ? `$${data.totalSpent.toFixed(4)}` : 'N/A';
628
+ const accountInfo = data.account || {};
629
+
630
+ content.innerHTML = `
631
+ <div class="openrouter-data">
632
+ <div class="usage-grid">
633
+ <div class="usage-item">
634
+ <div class="usage-label">Account</div>
635
+ <div class="usage-value">${accountInfo.label || 'Unknown'}</div>
636
+ </div>
637
+ <div class="usage-item">
638
+ <div class="usage-label">Total Spent</div>
639
+ <div class="usage-value highlight">${totalSpent}</div>
640
+ </div>
641
+ <div class="usage-item">
642
+ <div class="usage-label">Usage Balance</div>
643
+ <div class="usage-value">${accountInfo.usageBalance !== null ? `$${(accountInfo.usageBalance / 100).toFixed(2)}` : 'N/A'}</div>
644
+ </div>
645
+ <div class="usage-item">
646
+ <div class="usage-label">Account Type</div>
647
+ <div class="usage-value">${accountInfo.isFreeTier ? 'Free Tier' : 'Paid'}</div>
648
+ </div>
649
+ </div>
650
+ ${getComparisonHtml(data)}
651
+ <div class="openrouter-footer">
652
+ <small>Last updated: ${new Date(data.timestamp).toLocaleString()}</small>
653
+ <button class="btn-link" onclick="fetchOpenRouterUsage()">🔄 Refresh</button>
654
+ </div>
655
+ </div>
656
+ `;
657
+ }
658
+
659
+ /**
660
+ * Generate comparison HTML between analyzed and actual usage
661
+ */
662
+ function getComparisonHtml(openRouterData) {
663
+ if (!currentData || !openRouterData.totalSpent) {
664
+ return '';
665
+ }
666
+
667
+ const analyzed = currentData.monthly_projected_current;
668
+ const actual = openRouterData.totalSpent;
669
+ const difference = Math.abs(analyzed - actual);
670
+ const percentDiff = analyzed > 0 ? ((difference / analyzed) * 100).toFixed(1) : 0;
671
+
672
+ return `
673
+ <div class="comparison-section">
674
+ <h4>📊 Comparison</h4>
675
+ <div class="comparison-grid">
676
+ <div class="comparison-item">
677
+ <span class="comparison-label">SmartMeter Analyzed:</span>
678
+ <span class="comparison-value">$${analyzed.toFixed(4)}</span>
679
+ </div>
680
+ <div class="comparison-item">
681
+ <span class="comparison-label">OpenRouter Actual:</span>
682
+ <span class="comparison-value">$${actual.toFixed(4)}</span>
683
+ </div>
684
+ <div class="comparison-item">
685
+ <span class="comparison-label">Difference:</span>
686
+ <span class="comparison-value ${analyzed > actual ? 'positive' : 'negative'}">
687
+ ${analyzed > actual ? '-' : '+'}$${difference.toFixed(4)} (${percentDiff}%)
688
+ </span>
689
+ </div>
690
+ </div>
691
+ ${analyzed === 0 && actual > 0 ? `
692
+ <div class="comparison-note">
693
+ â„šī¸ SmartMeter shows $0 because OpenRouter isn't including cost data in API responses.
694
+ Your actual usage is ${totalSpent} as shown above.
695
+ </div>
696
+ ` : ''}
697
+ </div>
698
+ `;
699
+ }
700
+
701
+ /**
702
+ * Open API key configuration modal
703
+ */
704
+ function openConfigModal() {
705
+ document.getElementById('configModal').style.display = 'flex';
706
+ document.getElementById('configStatus').innerHTML = '';
707
+ }
708
+
709
+ /**
710
+ * Close API key configuration modal
711
+ */
712
+ function closeConfigModal() {
713
+ document.getElementById('configModal').style.display = 'none';
714
+ document.getElementById('apiKeyInput').value = '';
715
+ document.getElementById('configStatus').innerHTML = '';
716
+ }
717
+
718
+ /**
719
+ * Save and validate API key
720
+ */
721
+ async function saveApiKey() {
722
+ const apiKey = document.getElementById('apiKeyInput').value.trim();
723
+ const statusDiv = document.getElementById('configStatus');
724
+
725
+ if (!apiKey) {
726
+ statusDiv.innerHTML = '<div class="status-error">âš ī¸ Please enter an API key</div>';
727
+ return;
728
+ }
729
+
730
+ if (!apiKey.startsWith('sk-or-')) {
731
+ statusDiv.innerHTML = '<div class="status-error">âš ī¸ Invalid API key format (should start with "sk-or-")</div>';
732
+ return;
733
+ }
734
+
735
+ statusDiv.innerHTML = '<div class="status-loading">âŗ Validating API key...</div>';
736
+
737
+ try {
738
+ const response = await fetch(`${API_BASE_URL}/config/openrouter-key`, {
739
+ method: 'POST',
740
+ headers: {
741
+ 'Content-Type': 'application/json'
742
+ },
743
+ body: JSON.stringify({ apiKey })
744
+ });
745
+
746
+ const data = await response.json();
747
+
748
+ if (data.success) {
749
+ statusDiv.innerHTML = '<div class="status-success">✅ API key saved and validated!</div>';
750
+ setTimeout(() => {
751
+ closeConfigModal();
752
+ window.location.reload(); // Reload to show OpenRouter section
753
+ }, 1500);
754
+ } else {
755
+ statusDiv.innerHTML = `<div class="status-error">❌ ${data.error || 'Validation failed'}</div>`;
756
+ }
757
+ } catch (error) {
758
+ statusDiv.innerHTML = `<div class="status-error">❌ Failed to save: ${error.message}</div>`;
759
+ }
760
+ }
761
+
507
762
  // Export functions for inline onclick handlers
508
763
  window.refreshDashboard = refreshDashboard;
509
764
  window.switchTab = switchTab;
@@ -511,3 +766,10 @@ window.viewRecommendationDetails = viewRecommendationDetails;
511
766
  window.exportReport = exportReport;
512
767
  window.viewConfig = viewConfig;
513
768
  window.applyOptimizations = applyOptimizations;
769
+ window.openConfigModal = openConfigModal;
770
+ window.closeConfigModal = closeConfigModal;
771
+ window.saveApiKey = saveApiKey;
772
+ window.fetchOpenRouterUsage = fetchOpenRouterUsage;
773
+
774
+ window.viewConfig = viewConfig;
775
+ window.applyOptimizations = applyOptimizations;
@@ -36,6 +36,32 @@
36
36
 
37
37
  <!-- Main Dashboard -->
38
38
  <main class="dashboard">
39
+ <!-- Info Banner - Cost Data Notice -->
40
+ <div class="info-banner" id="costDataNotice" style="display: none;">
41
+ <div class="info-banner-icon">â„šī¸</div>
42
+ <div class="info-banner-content">
43
+ <div class="info-banner-title">About Cost Tracking</div>
44
+ <div class="info-banner-text">
45
+ SmartMeter analyzes cost data from your OpenClaw session files. If costs show as $0.00,
46
+ your OpenRouter API responses may not include cost information. SmartMeter will still
47
+ provide optimization recommendations based on token usage patterns.
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- OpenRouter Live Usage Section -->
53
+ <section class="openrouter-section" id="openRouterSection">
54
+ <div class="openrouter-card">
55
+ <div class="openrouter-header">
56
+ <h3>🔗 OpenRouter Actual Usage</h3>
57
+ <button class="btn-config" onclick="openConfigModal()">âš™ī¸ Configure API Key</button>
58
+ </div>
59
+ <div id="openRouterContent">
60
+ <div class="openrouter-loading">Loading actual usage...</div>
61
+ </div>
62
+ </div>
63
+ </section>
64
+
39
65
  <!-- Hero Section - Savings Display -->
40
66
  <section class="hero-card">
41
67
  <div class="savings-display">
@@ -146,6 +172,29 @@
146
172
  </div>
147
173
  </footer>
148
174
  </main>
175
+
176
+ <!-- OpenRouter API Key Configuration Modal -->
177
+ <div id="configModal" class="modal" style="display: none;">
178
+ <div class="modal-content">
179
+ <div class="modal-header">
180
+ <h2>Configure OpenRouter API Key</h2>
181
+ <button class="modal-close" onclick="closeConfigModal()">×</button>
182
+ </div>
183
+ <div class="modal-body">
184
+ <p>Enter your OpenRouter API key to view actual usage and compare with analyzed data.</p>
185
+ <div class="form-group">
186
+ <label for="apiKeyInput">OpenRouter API Key:</label>
187
+ <input type="password" id="apiKeyInput" placeholder="sk-or-v1-..." class="api-key-input">
188
+ <small class="form-hint">Your API key is stored locally and never shared.</small>
189
+ </div>
190
+ <div id="configStatus" class="config-status"></div>
191
+ </div>
192
+ <div class="modal-footer">
193
+ <button class="btn btn-secondary" onclick="closeConfigModal()">Cancel</button>
194
+ <button class="btn btn-primary" onclick="saveApiKey()">Save & Validate</button>
195
+ </div>
196
+ </div>
197
+ </div>
149
198
  </div>
150
199
 
151
200
  <!-- Toast Notifications -->