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.
- package/dashboard/README.md +176 -0
- package/dashboard/api/stats.js +109 -0
- package/dashboard/api/submit.js +99 -0
- package/dashboard/lib/schema.sql +67 -0
- package/dashboard/package.json +16 -0
- package/dashboard/public/index.html +377 -0
- package/dashboard/vercel.json +18 -0
- package/index.js +52 -7
- package/package.json +1 -1
- package/src/energy-data.js +3 -1
- package/src/oracle.js +21 -25
- package/test-api.js +147 -0
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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
package/src/energy-data.js
CHANGED
|
@@ -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
|
|
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
|
+
});
|