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.
- package/CHANGELOG.md +201 -0
- package/DEPLOYMENT_HANDOFF.md +923 -0
- package/HANDOFF_BRIEF.md +619 -0
- package/README.md +64 -0
- package/SKILL.md +654 -0
- package/canvas-template/app.js +273 -11
- package/canvas-template/index.html +49 -0
- package/canvas-template/styles.css +764 -90
- package/package.json +19 -3
- package/src/analyzer/config-manager.js +92 -0
- package/src/analyzer/openrouter-client.js +112 -0
- package/src/canvas/api-server.js +104 -5
- package/src/canvas/deployer.js +1 -1
- package/src/cli/commands.js +3 -2
- package/src/generator/config-builder.js +7 -2
- package/src/generator/validator.js +7 -7
package/canvas-template/app.js
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
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
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 -->
|