decharge-scout 1.0.1 → 1.2.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.
@@ -0,0 +1,176 @@
1
+ # DeCharge Scout Dashboard
2
+
3
+ Real-time analytics dashboard for tracking DeCharge Scout agents worldwide.
4
+
5
+ ## Features
6
+
7
+ - **Real-time Agent Tracking** - See how many agents are currently running
8
+ - **Location Analytics** - View agent distribution across different locations
9
+ - **Submission History** - Track all oracle submissions with prices and savings
10
+ - **Live Updates** - Dashboard auto-refreshes every 30 seconds
11
+
12
+ ## Deployment to Vercel
13
+
14
+ ### Prerequisites
15
+
16
+ 1. [Vercel Account](https://vercel.com/signup)
17
+ 2. [Vercel CLI](https://vercel.com/cli) installed: `npm i -g vercel`
18
+
19
+ ### Step 1: Set Up Vercel Postgres Database
20
+
21
+ 1. Go to [Vercel Dashboard](https://vercel.com/dashboard)
22
+ 2. Create a new project or select existing one
23
+ 3. Go to "Storage" tab
24
+ 4. Click "Create Database" → Select "Postgres"
25
+ 5. Name it (e.g., `decharge-scout-db`)
26
+ 6. Wait for provisioning to complete
27
+
28
+ ### Step 2: Initialize Database Schema
29
+
30
+ 1. In your Vercel Postgres dashboard, click "Query" tab
31
+ 2. Copy and paste the contents of `lib/schema.sql`
32
+ 3. Click "Run Query" to create tables and views
33
+
34
+ ### Step 3: Deploy Dashboard
35
+
36
+ ```bash
37
+ cd dashboard
38
+ npm install
39
+ vercel
40
+ ```
41
+
42
+ Follow the prompts:
43
+ - **Set up and deploy?** → Yes
44
+ - **Which scope?** → Your account
45
+ - **Link to existing project?** → No (unless you already created one)
46
+ - **What's your project's name?** → `decharge-scout-dashboard`
47
+ - **In which directory is your code located?** → `./`
48
+
49
+ ### Step 4: Link Postgres to Project
50
+
51
+ ```bash
52
+ vercel env pull
53
+ ```
54
+
55
+ This will create a `.env.local` file with your `POSTGRES_URL` automatically.
56
+
57
+ ### Step 5: Deploy to Production
58
+
59
+ ```bash
60
+ vercel --prod
61
+ ```
62
+
63
+ Your dashboard will be live at: `https://decharge-scout-dashboard.vercel.app`
64
+
65
+ ## Update CLI to Use Dashboard
66
+
67
+ After deploying, update your `.env` file in the CLI project:
68
+
69
+ ```bash
70
+ DASHBOARD_API_URL=https://your-dashboard-url.vercel.app/api/submit
71
+ ```
72
+
73
+ ## API Endpoints
74
+
75
+ ### POST `/api/submit`
76
+
77
+ Submit agent data to the dashboard.
78
+
79
+ **Request Body:**
80
+ ```json
81
+ {
82
+ "agent_name": "Agent-ABC123",
83
+ "location": "Austin, TX, US",
84
+ "timestamp": 1234567890000,
85
+ "results": {
86
+ "cheapest_window": "5AM-6AM",
87
+ "price": 0.0338,
88
+ "savings": 71.7,
89
+ "data_points": 24
90
+ }
91
+ }
92
+ ```
93
+
94
+ **Response:**
95
+ ```json
96
+ {
97
+ "success": true,
98
+ "message": "Data submitted successfully"
99
+ }
100
+ ```
101
+
102
+ ### GET `/api/stats`
103
+
104
+ Get real-time statistics about agents.
105
+
106
+ **Response:**
107
+ ```json
108
+ {
109
+ "activeAgents": 5,
110
+ "totalSubmissions": 127,
111
+ "locations": [
112
+ {
113
+ "location": "Austin, TX, US",
114
+ "submissions": 45
115
+ }
116
+ ],
117
+ "recentSubmissions": [...],
118
+ "timestamp": "2024-01-15T10:30:00Z"
119
+ }
120
+ ```
121
+
122
+ ## Local Development
123
+
124
+ ```bash
125
+ npm install
126
+ npm run dev
127
+ ```
128
+
129
+ Visit http://localhost:3000 to see the dashboard.
130
+
131
+ ## Database Schema
132
+
133
+ ### Tables
134
+
135
+ - **agent_heartbeat** - Tracks active agents (updated every submission)
136
+ - **agent_submissions** - Stores all oracle submissions with full data
137
+
138
+ ### Views
139
+
140
+ - **active_agents_hourly** - Agents active in the last hour
141
+ - **submissions_by_location** - Aggregated stats by location
142
+ - **hourly_submission_counts** - Time series of submissions
143
+
144
+ ## Troubleshooting
145
+
146
+ **Error: "Internal server error"**
147
+ - Check that `POSTGRES_URL` is configured in Vercel environment variables
148
+ - Verify database tables are created using `lib/schema.sql`
149
+
150
+ **No data showing**
151
+ - Ensure CLI is configured with `DASHBOARD_API_URL` in `.env`
152
+ - Check API endpoint is accessible: `curl https://your-url.vercel.app/api/stats`
153
+ - Verify agents are running and submitting data
154
+
155
+ **CORS errors**
156
+ - API endpoints include `Access-Control-Allow-Origin: *` headers
157
+ - If issues persist, check Vercel logs: `vercel logs`
158
+
159
+ ## Architecture
160
+
161
+ ```
162
+ ┌─────────────┐
163
+ │ CLI Agents │ → POST /api/submit → ┌──────────────┐
164
+ │ (Multiple) │ │ Vercel Edge │
165
+ └─────────────┘ │ Functions │
166
+ └───────┬──────┘
167
+
168
+ ┌─────────────┐ ┌──────────────┐
169
+ │ Dashboard │ ← GET /api/stats ← │ Postgres │
170
+ │ (Admin) │ │ Database │
171
+ └─────────────┘ └──────────────┘
172
+ ```
173
+
174
+ ## License
175
+
176
+ MIT
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Vercel API Endpoint - Agent Statistics
3
+ *
4
+ * GET /api/stats
5
+ * Returns real-time statistics about active agents from Supabase
6
+ */
7
+
8
+ import { createClient } from '@supabase/supabase-js';
9
+
10
+ export const config = {
11
+ runtime: 'edge',
12
+ };
13
+
14
+ export default async function handler(req) {
15
+ // CORS headers
16
+ const headers = {
17
+ 'Access-Control-Allow-Origin': '*',
18
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
19
+ 'Access-Control-Allow-Headers': 'Content-Type',
20
+ 'Content-Type': 'application/json',
21
+ };
22
+
23
+ // Handle OPTIONS request for CORS
24
+ if (req.method === 'OPTIONS') {
25
+ return new Response(null, { status: 200, headers });
26
+ }
27
+
28
+ // Only allow GET
29
+ if (req.method !== 'GET') {
30
+ return new Response(
31
+ JSON.stringify({ error: 'Method not allowed' }),
32
+ { status: 405, headers }
33
+ );
34
+ }
35
+
36
+ try {
37
+ // Initialize Supabase client
38
+ const supabase = createClient(
39
+ process.env.SUPABASE_URL,
40
+ process.env.SUPABASE_ANON_KEY
41
+ );
42
+
43
+ // Calculate time thresholds
44
+ const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000).toISOString();
45
+ const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
46
+
47
+ // Get active agents (last seen within 30 minutes)
48
+ const { data: activeAgents, error: activeError } = await supabase
49
+ .from('agent_heartbeat')
50
+ .select('agent_name', { count: 'exact', head: false })
51
+ .gt('last_seen', thirtyMinutesAgo);
52
+
53
+ if (activeError) throw activeError;
54
+
55
+ // Get total submissions in last 24 hours
56
+ const { count: submissionCount, error: submissionError } = await supabase
57
+ .from('agent_submissions')
58
+ .select('*', { count: 'exact', head: true })
59
+ .gt('created_at', twentyFourHoursAgo);
60
+
61
+ if (submissionError) throw submissionError;
62
+
63
+ // Get unique locations with submission counts
64
+ const { data: locationsData, error: locationsError } = await supabase
65
+ .from('agent_submissions')
66
+ .select('location')
67
+ .gt('created_at', twentyFourHoursAgo);
68
+
69
+ if (locationsError) throw locationsError;
70
+
71
+ // Aggregate locations manually
72
+ const locationMap = {};
73
+ locationsData.forEach(row => {
74
+ locationMap[row.location] = (locationMap[row.location] || 0) + 1;
75
+ });
76
+ const locations = Object.entries(locationMap)
77
+ .map(([location, submissions]) => ({ location, submissions }))
78
+ .sort((a, b) => b.submissions - a.submissions);
79
+
80
+ // Get recent submissions
81
+ const { data: recentSubmissions, error: recentError } = await supabase
82
+ .from('agent_submissions')
83
+ .select('agent_name, location, timestamp, cheapest_window, price, savings, created_at')
84
+ .order('created_at', { ascending: false })
85
+ .limit(20);
86
+
87
+ if (recentError) throw recentError;
88
+
89
+ return new Response(
90
+ JSON.stringify({
91
+ activeAgents: new Set(activeAgents.map(a => a.agent_name)).size,
92
+ totalSubmissions: submissionCount || 0,
93
+ locations: locations,
94
+ recentSubmissions: recentSubmissions,
95
+ timestamp: new Date().toISOString(),
96
+ }),
97
+ { status: 200, headers }
98
+ );
99
+ } catch (error) {
100
+ console.error('Stats error:', error);
101
+ return new Response(
102
+ JSON.stringify({
103
+ error: 'Internal server error',
104
+ message: error.message,
105
+ }),
106
+ { status: 500, headers }
107
+ );
108
+ }
109
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Vercel API Endpoint - Agent Data Submission
3
+ *
4
+ * POST /api/submit
5
+ * Receives agent data submissions and stores them in Supabase
6
+ */
7
+
8
+ import { createClient } from '@supabase/supabase-js';
9
+
10
+ export const config = {
11
+ runtime: 'edge',
12
+ };
13
+
14
+ export default async function handler(req) {
15
+ // CORS headers
16
+ const headers = {
17
+ 'Access-Control-Allow-Origin': '*',
18
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
19
+ 'Access-Control-Allow-Headers': 'Content-Type',
20
+ 'Content-Type': 'application/json',
21
+ };
22
+
23
+ // Handle OPTIONS request for CORS
24
+ if (req.method === 'OPTIONS') {
25
+ return new Response(null, { status: 200, headers });
26
+ }
27
+
28
+ // Only allow POST
29
+ if (req.method !== 'POST') {
30
+ return new Response(
31
+ JSON.stringify({ error: 'Method not allowed' }),
32
+ { status: 405, headers }
33
+ );
34
+ }
35
+
36
+ try {
37
+ const data = await req.json();
38
+
39
+ // Validate required fields
40
+ if (!data.agent_name || !data.location || !data.timestamp || !data.results) {
41
+ return new Response(
42
+ JSON.stringify({ error: 'Missing required fields' }),
43
+ { status: 400, headers }
44
+ );
45
+ }
46
+
47
+ // Initialize Supabase client
48
+ const supabase = createClient(
49
+ process.env.SUPABASE_URL,
50
+ process.env.SUPABASE_ANON_KEY
51
+ );
52
+
53
+ // Insert submission
54
+ const { error: submissionError } = await supabase
55
+ .from('agent_submissions')
56
+ .insert({
57
+ agent_name: data.agent_name,
58
+ location: data.location,
59
+ timestamp: new Date(data.timestamp).toISOString(),
60
+ cheapest_window: data.results.cheapest_window,
61
+ price: data.results.price,
62
+ savings: data.results.savings,
63
+ data_points: data.results.data_points,
64
+ });
65
+
66
+ if (submissionError) throw submissionError;
67
+
68
+ // Update agent heartbeat (upsert)
69
+ const { error: heartbeatError } = await supabase
70
+ .from('agent_heartbeat')
71
+ .upsert(
72
+ {
73
+ agent_name: data.agent_name,
74
+ location: data.location,
75
+ last_seen: new Date().toISOString(),
76
+ },
77
+ { onConflict: 'agent_name' }
78
+ );
79
+
80
+ if (heartbeatError) throw heartbeatError;
81
+
82
+ return new Response(
83
+ JSON.stringify({
84
+ success: true,
85
+ message: 'Data submitted successfully',
86
+ }),
87
+ { status: 200, headers }
88
+ );
89
+ } catch (error) {
90
+ console.error('Submission error:', error);
91
+ return new Response(
92
+ JSON.stringify({
93
+ error: 'Internal server error',
94
+ message: error.message,
95
+ }),
96
+ { status: 500, headers }
97
+ );
98
+ }
99
+ }
@@ -0,0 +1,67 @@
1
+ -- DeCharge Scout Dashboard Database Schema
2
+ -- Use this to initialize your Supabase Postgres database
3
+
4
+ -- Table: agent_heartbeat
5
+ -- Tracks which agents are currently active
6
+ CREATE TABLE IF NOT EXISTS agent_heartbeat (
7
+ agent_name VARCHAR(255) PRIMARY KEY,
8
+ location VARCHAR(255) NOT NULL,
9
+ last_seen TIMESTAMP NOT NULL,
10
+ created_at TIMESTAMP DEFAULT NOW()
11
+ );
12
+
13
+ -- Index for querying active agents
14
+ CREATE INDEX idx_heartbeat_last_seen ON agent_heartbeat(last_seen DESC);
15
+
16
+ -- Table: agent_submissions
17
+ -- Stores all data submissions from agents
18
+ CREATE TABLE IF NOT EXISTS agent_submissions (
19
+ id SERIAL PRIMARY KEY,
20
+ agent_name VARCHAR(255) NOT NULL,
21
+ location VARCHAR(255) NOT NULL,
22
+ timestamp TIMESTAMP NOT NULL,
23
+ cheapest_window VARCHAR(50) NOT NULL,
24
+ price DECIMAL(10, 6) NOT NULL,
25
+ savings DECIMAL(5, 2) NOT NULL,
26
+ data_points INTEGER NOT NULL,
27
+ created_at TIMESTAMP DEFAULT NOW()
28
+ );
29
+
30
+ -- Indexes for analytics queries
31
+ CREATE INDEX idx_submissions_created_at ON agent_submissions(created_at DESC);
32
+ CREATE INDEX idx_submissions_agent ON agent_submissions(agent_name);
33
+ CREATE INDEX idx_submissions_location ON agent_submissions(location);
34
+
35
+ -- View: Active agents in the last hour
36
+ CREATE OR REPLACE VIEW active_agents_hourly AS
37
+ SELECT
38
+ agent_name,
39
+ location,
40
+ last_seen,
41
+ EXTRACT(EPOCH FROM (NOW() - last_seen)) / 60 as minutes_ago
42
+ FROM agent_heartbeat
43
+ WHERE last_seen > NOW() - INTERVAL '1 hour'
44
+ ORDER BY last_seen DESC;
45
+
46
+ -- View: Submissions summary by location (last 24h)
47
+ CREATE OR REPLACE VIEW submissions_by_location AS
48
+ SELECT
49
+ location,
50
+ COUNT(*) as total_submissions,
51
+ AVG(price) as avg_price,
52
+ AVG(savings) as avg_savings,
53
+ MAX(created_at) as latest_submission
54
+ FROM agent_submissions
55
+ WHERE created_at > NOW() - INTERVAL '24 hours'
56
+ GROUP BY location
57
+ ORDER BY total_submissions DESC;
58
+
59
+ -- View: Hourly submission counts (last 24h)
60
+ CREATE OR REPLACE VIEW hourly_submission_counts AS
61
+ SELECT
62
+ DATE_TRUNC('hour', created_at) as hour,
63
+ COUNT(*) as submissions
64
+ FROM agent_submissions
65
+ WHERE created_at > NOW() - INTERVAL '24 hours'
66
+ GROUP BY DATE_TRUNC('hour', created_at)
67
+ ORDER BY hour DESC;
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "dechargescout",
3
+ "version": "1.0.0",
4
+ "description": "Real-time analytics dashboard for DeCharge Scout agents",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vercel dev",
8
+ "deploy": "vercel --prod"
9
+ },
10
+ "dependencies": {
11
+ "@supabase/supabase-js": "^2.39.0"
12
+ },
13
+ "devDependencies": {
14
+ "vercel": "^33.0.0"
15
+ }
16
+ }
@@ -0,0 +1,377 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DeCharge Scout - Admin Dashboard</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1400px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ .header {
27
+ text-align: center;
28
+ color: white;
29
+ margin-bottom: 40px;
30
+ }
31
+
32
+ .header h1 {
33
+ font-size: 3em;
34
+ margin-bottom: 10px;
35
+ }
36
+
37
+ .header p {
38
+ font-size: 1.2em;
39
+ opacity: 0.9;
40
+ }
41
+
42
+ .stats-grid {
43
+ display: grid;
44
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
45
+ gap: 20px;
46
+ margin-bottom: 40px;
47
+ }
48
+
49
+ .stat-card {
50
+ background: white;
51
+ border-radius: 12px;
52
+ padding: 30px;
53
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
54
+ transition: transform 0.3s ease;
55
+ }
56
+
57
+ .stat-card:hover {
58
+ transform: translateY(-5px);
59
+ }
60
+
61
+ .stat-card h3 {
62
+ color: #666;
63
+ font-size: 0.9em;
64
+ text-transform: uppercase;
65
+ letter-spacing: 1px;
66
+ margin-bottom: 10px;
67
+ }
68
+
69
+ .stat-card .value {
70
+ font-size: 3em;
71
+ font-weight: bold;
72
+ color: #667eea;
73
+ }
74
+
75
+ .stat-card .label {
76
+ color: #999;
77
+ margin-top: 5px;
78
+ }
79
+
80
+ .pulse {
81
+ animation: pulse 2s infinite;
82
+ }
83
+
84
+ @keyframes pulse {
85
+ 0%, 100% { opacity: 1; }
86
+ 50% { opacity: 0.7; }
87
+ }
88
+
89
+ .section {
90
+ background: white;
91
+ border-radius: 12px;
92
+ padding: 30px;
93
+ margin-bottom: 30px;
94
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
95
+ }
96
+
97
+ .section h2 {
98
+ color: #333;
99
+ margin-bottom: 20px;
100
+ font-size: 1.8em;
101
+ }
102
+
103
+ table {
104
+ width: 100%;
105
+ border-collapse: collapse;
106
+ }
107
+
108
+ th, td {
109
+ text-align: left;
110
+ padding: 12px;
111
+ border-bottom: 1px solid #eee;
112
+ }
113
+
114
+ th {
115
+ background: #f8f9fa;
116
+ color: #666;
117
+ font-weight: 600;
118
+ text-transform: uppercase;
119
+ font-size: 0.85em;
120
+ letter-spacing: 0.5px;
121
+ }
122
+
123
+ tr:hover {
124
+ background: #f8f9fa;
125
+ }
126
+
127
+ .location-badge {
128
+ display: inline-block;
129
+ padding: 4px 12px;
130
+ background: #e3f2fd;
131
+ color: #1976d2;
132
+ border-radius: 12px;
133
+ font-size: 0.85em;
134
+ font-weight: 500;
135
+ }
136
+
137
+ .price {
138
+ color: #4caf50;
139
+ font-weight: 600;
140
+ }
141
+
142
+ .savings {
143
+ color: #ff9800;
144
+ font-weight: 600;
145
+ }
146
+
147
+ .loading {
148
+ text-align: center;
149
+ padding: 40px;
150
+ color: #999;
151
+ }
152
+
153
+ .error {
154
+ background: #ffebee;
155
+ color: #c62828;
156
+ padding: 20px;
157
+ border-radius: 8px;
158
+ margin-bottom: 20px;
159
+ }
160
+
161
+ .refresh-btn {
162
+ background: #667eea;
163
+ color: white;
164
+ border: none;
165
+ padding: 12px 24px;
166
+ border-radius: 8px;
167
+ font-size: 1em;
168
+ cursor: pointer;
169
+ transition: background 0.3s;
170
+ }
171
+
172
+ .refresh-btn:hover {
173
+ background: #764ba2;
174
+ }
175
+
176
+ .last-updated {
177
+ color: #999;
178
+ font-size: 0.9em;
179
+ margin-top: 10px;
180
+ }
181
+
182
+ .live-indicator {
183
+ display: inline-block;
184
+ width: 12px;
185
+ height: 12px;
186
+ background: #4caf50;
187
+ border-radius: 50%;
188
+ margin-right: 8px;
189
+ animation: pulse 2s infinite;
190
+ }
191
+
192
+ .map-grid {
193
+ display: grid;
194
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
195
+ gap: 15px;
196
+ margin-top: 20px;
197
+ }
198
+
199
+ .location-card {
200
+ background: #f8f9fa;
201
+ padding: 15px;
202
+ border-radius: 8px;
203
+ border-left: 4px solid #667eea;
204
+ }
205
+
206
+ .location-card h4 {
207
+ color: #333;
208
+ margin-bottom: 8px;
209
+ }
210
+
211
+ .location-card .count {
212
+ font-size: 2em;
213
+ font-weight: bold;
214
+ color: #667eea;
215
+ }
216
+ </style>
217
+ </head>
218
+ <body>
219
+ <div class="container">
220
+ <div class="header">
221
+ <h1>🔋 DeCharge Scout</h1>
222
+ <p>Real-Time Agent Analytics Dashboard</p>
223
+ </div>
224
+
225
+ <div id="error-container"></div>
226
+
227
+ <div class="stats-grid">
228
+ <div class="stat-card">
229
+ <h3>Active Agents</h3>
230
+ <div class="value pulse" id="active-agents">-</div>
231
+ <div class="label"><span class="live-indicator"></span>Live Now</div>
232
+ </div>
233
+
234
+ <div class="stat-card">
235
+ <h3>Total Submissions</h3>
236
+ <div class="value" id="total-submissions">-</div>
237
+ <div class="label">Last 24 Hours</div>
238
+ </div>
239
+
240
+ <div class="stat-card">
241
+ <h3>Unique Locations</h3>
242
+ <div class="value" id="unique-locations">-</div>
243
+ <div class="label">Worldwide Coverage</div>
244
+ </div>
245
+
246
+ <div class="stat-card">
247
+ <h3>Avg Savings</h3>
248
+ <div class="value" id="avg-savings">-</div>
249
+ <div class="label">Cost Reduction</div>
250
+ </div>
251
+ </div>
252
+
253
+ <div class="section">
254
+ <h2>📍 Agents by Location</h2>
255
+ <div id="locations-map" class="map-grid"></div>
256
+ </div>
257
+
258
+ <div class="section">
259
+ <h2>📊 Recent Submissions</h2>
260
+ <button class="refresh-btn" onclick="loadData()">Refresh Data</button>
261
+ <div class="last-updated" id="last-updated"></div>
262
+ <div id="submissions-container"></div>
263
+ </div>
264
+ </div>
265
+
266
+ <script>
267
+ const API_BASE = window.location.hostname === 'localhost'
268
+ ? 'http://localhost:3000'
269
+ : '';
270
+
271
+ async function loadData() {
272
+ try {
273
+ const response = await fetch(`${API_BASE}/api/stats`);
274
+
275
+ if (!response.ok) {
276
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
277
+ }
278
+
279
+ const data = await response.json();
280
+
281
+ // Update stats
282
+ document.getElementById('active-agents').textContent = data.activeAgents || 0;
283
+ document.getElementById('total-submissions').textContent = data.totalSubmissions || 0;
284
+ document.getElementById('unique-locations').textContent = data.locations?.length || 0;
285
+
286
+ // Calculate average savings
287
+ const avgSavings = data.recentSubmissions?.length
288
+ ? (data.recentSubmissions.reduce((sum, s) => sum + parseFloat(s.savings), 0) / data.recentSubmissions.length).toFixed(1)
289
+ : 0;
290
+ document.getElementById('avg-savings').textContent = `${avgSavings}%`;
291
+
292
+ // Render locations map
293
+ renderLocationsMap(data.locations || []);
294
+
295
+ // Render submissions table
296
+ renderSubmissionsTable(data.recentSubmissions || []);
297
+
298
+ // Update timestamp
299
+ document.getElementById('last-updated').textContent =
300
+ `Last updated: ${new Date().toLocaleTimeString()}`;
301
+
302
+ // Clear errors
303
+ document.getElementById('error-container').innerHTML = '';
304
+ } catch (error) {
305
+ console.error('Error loading data:', error);
306
+ document.getElementById('error-container').innerHTML =
307
+ `<div class="error">⚠️ Error loading data: ${error.message}. Make sure the API is deployed and POSTGRES_URL is configured.</div>`;
308
+ }
309
+ }
310
+
311
+ function renderLocationsMap(locations) {
312
+ const container = document.getElementById('locations-map');
313
+
314
+ if (locations.length === 0) {
315
+ container.innerHTML = '<div class="loading">No location data available</div>';
316
+ return;
317
+ }
318
+
319
+ container.innerHTML = locations.map(loc => `
320
+ <div class="location-card">
321
+ <h4>${escapeHtml(loc.location)}</h4>
322
+ <div class="count">${loc.submissions}</div>
323
+ <div class="label">submissions</div>
324
+ </div>
325
+ `).join('');
326
+ }
327
+
328
+ function renderSubmissionsTable(submissions) {
329
+ const container = document.getElementById('submissions-container');
330
+
331
+ if (submissions.length === 0) {
332
+ container.innerHTML = '<div class="loading">No submissions yet</div>';
333
+ return;
334
+ }
335
+
336
+ container.innerHTML = `
337
+ <table>
338
+ <thead>
339
+ <tr>
340
+ <th>Agent</th>
341
+ <th>Location</th>
342
+ <th>Cheapest Window</th>
343
+ <th>Price</th>
344
+ <th>Savings</th>
345
+ <th>Time</th>
346
+ </tr>
347
+ </thead>
348
+ <tbody>
349
+ ${submissions.map(s => `
350
+ <tr>
351
+ <td><strong>${escapeHtml(s.agent_name)}</strong></td>
352
+ <td><span class="location-badge">${escapeHtml(s.location)}</span></td>
353
+ <td>${escapeHtml(s.cheapest_window)}</td>
354
+ <td class="price">$${parseFloat(s.price).toFixed(4)}/kWh</td>
355
+ <td class="savings">${parseFloat(s.savings).toFixed(1)}%</td>
356
+ <td>${new Date(s.created_at).toLocaleString()}</td>
357
+ </tr>
358
+ `).join('')}
359
+ </tbody>
360
+ </table>
361
+ `;
362
+ }
363
+
364
+ function escapeHtml(text) {
365
+ const div = document.createElement('div');
366
+ div.textContent = text;
367
+ return div.innerHTML;
368
+ }
369
+
370
+ // Initial load
371
+ loadData();
372
+
373
+ // Auto-refresh every 30 seconds
374
+ setInterval(loadData, 30000);
375
+ </script>
376
+ </body>
377
+ </html>
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": 2,
3
+ "buildCommand": "echo 'No build needed'",
4
+ "routes": [
5
+ {
6
+ "src": "/api/submit",
7
+ "dest": "/api/submit.js"
8
+ },
9
+ {
10
+ "src": "/api/stats",
11
+ "dest": "/api/stats.js"
12
+ },
13
+ {
14
+ "src": "/(.*)",
15
+ "dest": "/public/index.html"
16
+ }
17
+ ]
18
+ }
package/index.js CHANGED
@@ -168,12 +168,42 @@ async function main(options) {
168
168
  console.log(chalk.blue(`🤖 Agent Name: ${agentName}`));
169
169
 
170
170
  // Get location
171
- const locationSpinner = ora('Detecting location...').start();
172
171
  let location = options.location;
173
172
  if (!location) {
174
- location = await getLocation();
173
+ const locationSpinner = ora('Detecting location via IP...').start();
174
+ const detectedLocation = await getLocation();
175
+ locationSpinner.succeed(chalk.green(`📍 Detected Location: ${detectedLocation}`));
176
+
177
+ // Ask user to confirm or override (with timeout)
178
+ console.log(chalk.blue('\nYou can use this location or enter a custom one.'));
179
+ console.log(chalk.gray('(Press Enter to use detected location, or type custom location)'));
180
+
181
+ try {
182
+ // Create a promise that auto-resolves after 10 seconds
183
+ const timeoutPromise = new Promise((resolve) => {
184
+ setTimeout(() => {
185
+ console.log(chalk.yellow('\n⏱️ No input received, using detected location...'));
186
+ resolve('');
187
+ }, 10000);
188
+ });
189
+
190
+ const questionPromise = question('Enter custom location (or press Enter): ');
191
+
192
+ const customLocation = await Promise.race([questionPromise, timeoutPromise]);
193
+ location = customLocation.trim() || detectedLocation;
194
+
195
+ if (customLocation.trim()) {
196
+ console.log(chalk.green(`✓ Using custom location: ${location}`));
197
+ } else {
198
+ console.log(chalk.green(`✓ Using detected location: ${location}`));
199
+ }
200
+ } catch (error) {
201
+ console.log(chalk.yellow(`\n⚠️ Prompt error, using detected location: ${detectedLocation}`));
202
+ location = detectedLocation;
203
+ }
204
+ } else {
205
+ console.log(chalk.green(`📍 Location: ${location}`));
175
206
  }
176
- locationSpinner.succeed(chalk.green(`📍 Location: ${location}`));
177
207
 
178
208
  // Initialize points system
179
209
  initializePoints(wallet.publicKey.toBase58());
@@ -291,21 +321,36 @@ async function runQueryCycle(wallet, agentName, location, options) {
291
321
  if (process.env.DASHBOARD_API_URL) {
292
322
  try {
293
323
  const dashboardSpinner = ora('Submitting to dashboard API...').start();
294
- const response = await fetch(process.env.DASHBOARD_API_URL, {
324
+ const apiUrl = process.env.DASHBOARD_API_URL;
325
+
326
+ console.log(chalk.blue(`\n🌐 Dashboard API URL: ${apiUrl}`));
327
+ console.log(chalk.gray(`📤 Submitting data: ${JSON.stringify(submissionData, null, 2)}`));
328
+
329
+ const response = await fetch(apiUrl, {
295
330
  method: 'POST',
296
331
  headers: { 'Content-Type': 'application/json' },
297
332
  body: JSON.stringify(submissionData)
298
333
  });
299
334
 
335
+ const responseText = await response.text();
336
+ console.log(chalk.blue(`📥 API Response Status: ${response.status}`));
337
+ console.log(chalk.gray(`📥 API Response Body: ${responseText}`));
338
+
300
339
  if (response.ok) {
301
- dashboardSpinner.succeed(chalk.green('Dashboard submission successful'));
340
+ dashboardSpinner.succeed(chalk.green('Dashboard submission successful!'));
341
+ console.log(chalk.green(`🎉 Data should now appear at: https://decharge-scout.vercel.app/agentone`));
302
342
  } else {
303
- dashboardSpinner.warn(chalk.yellow(`Dashboard API returned: ${response.status}`));
343
+ dashboardSpinner.warn(chalk.yellow(`⚠️ Dashboard API returned: ${response.status}`));
344
+ console.log(chalk.yellow(`Response: ${responseText}`));
304
345
  }
305
346
  } catch (error) {
306
347
  // Silent fail for dashboard - it's optional
307
- console.log(chalk.gray(`ℹ️ Dashboard API not available (${error.message})`));
348
+ console.log(chalk.red(`❌ Dashboard API error: ${error.message}`));
349
+ console.log(chalk.gray(`Stack: ${error.stack}`));
308
350
  }
351
+ } else {
352
+ console.log(chalk.yellow(`\n⚠️ DASHBOARD_API_URL not set - skipping dashboard submission`));
353
+ console.log(chalk.gray(` To enable: Set DASHBOARD_API_URL in .env file`));
309
354
  }
310
355
 
311
356
  // Award points
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decharge-scout",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "AI-powered energy grid data scout with Solana integration",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -16,7 +16,9 @@ const EIA_BASE_URL = 'https://api.eia.gov/v2';
16
16
  * Fetch energy data from EIA API (ERCOT)
17
17
  */
18
18
  export async function fetchEnergyData() {
19
- if (!EIA_API_KEY || EIA_API_KEY === 'your_eia_api_key_here') {
19
+ if (!EIA_API_KEY || EIA_API_KEY === 'your_eia_api_key_here' || EIA_API_KEY.length < 20) {
20
+ console.warn('⚠️ EIA_API_KEY not configured or invalid');
21
+ console.warn(' Get a free key: https://www.eia.gov/opendata/register.php');
20
22
  throw new Error('EIA_API_KEY not configured');
21
23
  }
22
24
 
package/src/oracle.js CHANGED
@@ -21,6 +21,25 @@ export async function submitToOracle(wallet, submissionData) {
21
21
  try {
22
22
  const connection = getConnection();
23
23
 
24
+ // Check wallet balance first
25
+ const balance = await connection.getBalance(wallet.publicKey);
26
+ const balanceSOL = balance / 1000000000; // Convert lamports to SOL
27
+
28
+ // Minimum balance needed: 0.01 SOL for transaction + fees
29
+ const minRequired = 0.01;
30
+
31
+ if (balanceSOL < minRequired) {
32
+ console.warn(`\n⚠️ Insufficient funds for oracle submission`);
33
+ console.warn(` Current balance: ${balanceSOL.toFixed(4)} SOL`);
34
+ console.warn(` Required: ${minRequired} SOL`);
35
+ console.warn(` Wallet: ${wallet.publicKey.toBase58()}`);
36
+ console.warn(`\n💡 To get devnet SOL:`);
37
+ console.warn(` 1. solana airdrop 1 ${wallet.publicKey.toBase58()} --url devnet`);
38
+ console.warn(` 2. Or visit: https://faucet.solana.com/`);
39
+ console.warn(`\n📝 Using mock signature for demo purposes...\n`);
40
+ return 'MOCK_ORACLE_TX_' + Date.now() + '_' + crypto.randomBytes(16).toString('hex');
41
+ }
42
+
24
43
  // Anonymize data
25
44
  const anonymizedData = anonymizeData(submissionData);
26
45
 
@@ -28,35 +47,12 @@ export async function submitToOracle(wallet, submissionData) {
28
47
  const dataJSON = JSON.stringify(anonymizedData);
29
48
  const dataBuffer = Buffer.from(dataJSON);
30
49
 
31
- // For demo, we'll send a memo transaction with the data
50
+ // For demo, we'll send just a memo transaction (no transfer needed)
32
51
  // In production, this would call a custom Solana program
33
52
 
34
- // Create oracle program ID (mock - using a valid pubkey format)
35
- const oracleProgramId = new PublicKey('DeCh4rG3orac1e111111111111111111111111111111');
36
-
37
- // Create PDA for storing oracle data (simplified for demo)
38
- const [oraclePDA] = await PublicKey.findProgramAddress(
39
- [
40
- Buffer.from('oracle'),
41
- wallet.publicKey.toBuffer(),
42
- Buffer.from(Date.now().toString())
43
- ],
44
- oracleProgramId
45
- );
46
-
47
53
  // Create transaction with memo containing our data
48
54
  const transaction = new Transaction();
49
55
 
50
- // Add a small transfer to make it a valid transaction
51
- // (In production, this would be a custom program instruction)
52
- transaction.add(
53
- SystemProgram.transfer({
54
- fromPubkey: wallet.publicKey,
55
- toPubkey: oraclePDA,
56
- lamports: 1000 // Minimal amount for demo
57
- })
58
- );
59
-
60
56
  // Add memo instruction with our data (truncate if needed for tx size limits)
61
57
  const MAX_MEMO_SIZE = 566; // Solana memo size limit
62
58
  const memoData = dataBuffer.length > MAX_MEMO_SIZE
@@ -89,7 +85,7 @@ export async function submitToOracle(wallet, submissionData) {
89
85
  return signature;
90
86
  } catch (error) {
91
87
  // If transaction fails (e.g., insufficient funds), create mock signature
92
- if (error.message.includes('insufficient')) {
88
+ if (error.message.includes('insufficient') || error.message.includes('balance')) {
93
89
  console.warn('Insufficient funds for oracle submission, creating mock signature');
94
90
  return 'MOCK_ORACLE_TX_' + Date.now() + '_' + crypto.randomBytes(16).toString('hex');
95
91
  }
package/test-api.js ADDED
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test Script - Direct API Submission
5
+ *
6
+ * This script tests the dashboard API by submitting sample data
7
+ * Use this to verify your Supabase database connection
8
+ */
9
+
10
+ import fetch from 'node-fetch';
11
+ import chalk from 'chalk';
12
+
13
+ const DASHBOARD_API_URL = 'https://decharge-scout.vercel.app/agentone/api/submit';
14
+ const STATS_API_URL = 'https://decharge-scout.vercel.app/agentone/api/stats';
15
+
16
+ // Sample test data
17
+ const testData = {
18
+ agent_name: `TestAgent-${Math.random().toString(36).substring(7).toUpperCase()}`,
19
+ location: 'Test City, TX, US',
20
+ timestamp: Date.now(),
21
+ results: {
22
+ cheapest_window: '2AM-3AM',
23
+ price: 0.0299,
24
+ savings: 75.5,
25
+ data_points: 24
26
+ }
27
+ };
28
+
29
+ console.log(chalk.cyan.bold('\n🧪 Testing DeCharge Dashboard API\n'));
30
+ console.log(chalk.gray('=' .repeat(50)));
31
+
32
+ async function testSubmission() {
33
+ console.log(chalk.blue('\n1️⃣ Testing Submission Endpoint'));
34
+ console.log(chalk.gray(` URL: ${DASHBOARD_API_URL}`));
35
+ console.log(chalk.gray(` Data: ${JSON.stringify(testData, null, 2)}`));
36
+
37
+ try {
38
+ const response = await fetch(DASHBOARD_API_URL, {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ },
43
+ body: JSON.stringify(testData)
44
+ });
45
+
46
+ const responseText = await response.text();
47
+
48
+ console.log(chalk.blue(`\n📥 Response Status: ${response.status}`));
49
+ console.log(chalk.blue(`📥 Response Body:`));
50
+ console.log(chalk.gray(responseText));
51
+
52
+ if (response.ok) {
53
+ console.log(chalk.green('\n✅ Submission successful!'));
54
+ return true;
55
+ } else {
56
+ console.log(chalk.red('\n❌ Submission failed!'));
57
+ return false;
58
+ }
59
+ } catch (error) {
60
+ console.log(chalk.red(`\n❌ Error: ${error.message}`));
61
+ console.log(chalk.gray(error.stack));
62
+ return false;
63
+ }
64
+ }
65
+
66
+ async function testStats() {
67
+ console.log(chalk.blue('\n2️⃣ Testing Stats Endpoint'));
68
+ console.log(chalk.gray(` URL: ${STATS_API_URL}`));
69
+
70
+ try {
71
+ const response = await fetch(STATS_API_URL);
72
+ const data = await response.json();
73
+
74
+ console.log(chalk.blue(`\n📥 Response Status: ${response.status}`));
75
+ console.log(chalk.blue(`📊 Stats Data:`));
76
+ console.log(chalk.gray(JSON.stringify(data, null, 2)));
77
+
78
+ if (response.ok) {
79
+ console.log(chalk.green('\n✅ Stats endpoint working!'));
80
+ console.log(chalk.magenta(`📊 Active Agents: ${data.activeAgents}`));
81
+ console.log(chalk.magenta(`📊 Total Submissions: ${data.totalSubmissions}`));
82
+ return true;
83
+ } else {
84
+ console.log(chalk.red('\n❌ Stats endpoint failed!'));
85
+ return false;
86
+ }
87
+ } catch (error) {
88
+ console.log(chalk.red(`\n❌ Error: ${error.message}`));
89
+ console.log(chalk.gray(error.stack));
90
+ return false;
91
+ }
92
+ }
93
+
94
+ async function checkDatabase() {
95
+ console.log(chalk.blue('\n3️⃣ Checking Database Connection'));
96
+
97
+ console.log(chalk.yellow('\nPlease verify in Supabase:'));
98
+ console.log(chalk.gray(' 1. Go to https://supabase.com'));
99
+ console.log(chalk.gray(' 2. Open your project'));
100
+ console.log(chalk.gray(' 3. Go to Table Editor'));
101
+ console.log(chalk.gray(' 4. Check tables: agent_submissions, agent_heartbeat'));
102
+ console.log(chalk.gray(` 5. Look for agent: ${testData.agent_name}`));
103
+ }
104
+
105
+ async function main() {
106
+ console.log(chalk.yellow('🚀 Starting API tests...\n'));
107
+
108
+ const submissionOk = await testSubmission();
109
+
110
+ console.log(chalk.gray('\n' + '=' .repeat(50)));
111
+
112
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds
113
+
114
+ const statsOk = await testStats();
115
+
116
+ console.log(chalk.gray('\n' + '=' .repeat(50)));
117
+
118
+ await checkDatabase();
119
+
120
+ console.log(chalk.gray('\n' + '=' .repeat(50)));
121
+ console.log(chalk.cyan.bold('\n📋 Test Summary:'));
122
+ console.log(submissionOk
123
+ ? chalk.green('✅ Submission endpoint: PASS')
124
+ : chalk.red('❌ Submission endpoint: FAIL'));
125
+ console.log(statsOk
126
+ ? chalk.green('✅ Stats endpoint: PASS')
127
+ : chalk.red('❌ Stats endpoint: FAIL'));
128
+
129
+ if (submissionOk && statsOk) {
130
+ console.log(chalk.green.bold('\n🎉 All tests passed! Your dashboard is working!'));
131
+ console.log(chalk.blue(`\n🌐 View dashboard at: https://decharge-scout.vercel.app/agentone\n`));
132
+ } else {
133
+ console.log(chalk.red.bold('\n⚠️ Some tests failed. Check the errors above.'));
134
+ console.log(chalk.yellow('\nTroubleshooting:'));
135
+ console.log(chalk.gray('1. Make sure Supabase tables are created (run schema.sql)'));
136
+ console.log(chalk.gray('2. Check Vercel environment variables:'));
137
+ console.log(chalk.gray(' - SUPABASE_URL'));
138
+ console.log(chalk.gray(' - SUPABASE_ANON_KEY'));
139
+ console.log(chalk.gray('3. Redeploy Vercel after adding env vars'));
140
+ console.log(chalk.gray('4. Check Vercel function logs: vercel logs\n'));
141
+ }
142
+ }
143
+
144
+ main().catch(error => {
145
+ console.error(chalk.red('Fatal error:'), error);
146
+ process.exit(1);
147
+ });