decharge-scout 4.0.0 → 4.0.4
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/api/alpha.js +167 -0
- package/dashboard/api/submit.js +23 -0
- package/dashboard/lib/migration-alpha-contributions.sql +185 -0
- package/dashboard/public/index.html +236 -0
- package/index.js +78 -11
- package/package.json +1 -1
- package/src/weather-data.js +84 -37
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel API Endpoint - Alpha Contributions
|
|
3
|
+
*
|
|
4
|
+
* GET /api/alpha - Get verified alpha contributions
|
|
5
|
+
* POST /api/alpha - Submit new alpha contribution
|
|
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, 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
|
+
// Initialize Supabase client
|
|
29
|
+
const supabase = createClient(
|
|
30
|
+
process.env.SUPABASE_URL,
|
|
31
|
+
process.env.SUPABASE_ANON_KEY
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// GET - Retrieve alpha contributions
|
|
35
|
+
if (req.method === 'GET') {
|
|
36
|
+
try {
|
|
37
|
+
const url = new URL(req.url);
|
|
38
|
+
const location = url.searchParams.get('location');
|
|
39
|
+
const verified = url.searchParams.get('verified') === 'true';
|
|
40
|
+
const minConfidence = parseFloat(url.searchParams.get('min_confidence') || '0');
|
|
41
|
+
|
|
42
|
+
let query = supabase
|
|
43
|
+
.from('alpha_contributions')
|
|
44
|
+
.select('*')
|
|
45
|
+
.order('created_at', { ascending: false })
|
|
46
|
+
.limit(100);
|
|
47
|
+
|
|
48
|
+
// Apply filters
|
|
49
|
+
if (location) {
|
|
50
|
+
query = query.ilike('location', `%${location}%`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (verified) {
|
|
54
|
+
query = query.eq('verified', true);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (minConfidence > 0) {
|
|
58
|
+
query = query.gte('confidence', minConfidence);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { data, error } = await query;
|
|
62
|
+
|
|
63
|
+
if (error) throw error;
|
|
64
|
+
|
|
65
|
+
// Get statistics
|
|
66
|
+
const { data: stats } = await supabase
|
|
67
|
+
.from('alpha_by_location')
|
|
68
|
+
.select('*')
|
|
69
|
+
.order('verified_count', { ascending: false })
|
|
70
|
+
.limit(10);
|
|
71
|
+
|
|
72
|
+
return new Response(
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
contributions: data || [],
|
|
75
|
+
statistics: stats || [],
|
|
76
|
+
total: data?.length || 0,
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
}),
|
|
79
|
+
{ status: 200, headers }
|
|
80
|
+
);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Alpha GET error:', error);
|
|
83
|
+
return new Response(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
error: 'Failed to fetch alpha contributions',
|
|
86
|
+
message: error.message,
|
|
87
|
+
}),
|
|
88
|
+
{ status: 500, headers }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// POST - Submit alpha contribution
|
|
94
|
+
if (req.method === 'POST') {
|
|
95
|
+
try {
|
|
96
|
+
const data = await req.json();
|
|
97
|
+
|
|
98
|
+
// Validate required fields
|
|
99
|
+
if (!data.agent_name || !data.location || !data.contribution) {
|
|
100
|
+
return new Response(
|
|
101
|
+
JSON.stringify({ error: 'Missing required fields' }),
|
|
102
|
+
{ status: 400, headers }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { contribution } = data;
|
|
107
|
+
|
|
108
|
+
// Validate contribution structure
|
|
109
|
+
if (
|
|
110
|
+
typeof contribution.startHour !== 'number' ||
|
|
111
|
+
typeof contribution.endHour !== 'number' ||
|
|
112
|
+
!contribution.type ||
|
|
113
|
+
contribution.startHour < 0 ||
|
|
114
|
+
contribution.startHour > 23 ||
|
|
115
|
+
contribution.endHour < 0 ||
|
|
116
|
+
contribution.endHour > 23
|
|
117
|
+
) {
|
|
118
|
+
return new Response(
|
|
119
|
+
JSON.stringify({ error: 'Invalid contribution data' }),
|
|
120
|
+
{ status: 400, headers }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Insert alpha contribution
|
|
125
|
+
const { data: insertedData, error: insertError } = await supabase
|
|
126
|
+
.from('alpha_contributions')
|
|
127
|
+
.insert({
|
|
128
|
+
agent_name: data.agent_name,
|
|
129
|
+
location: data.location,
|
|
130
|
+
contribution_type: contribution.type,
|
|
131
|
+
start_hour: contribution.startHour,
|
|
132
|
+
end_hour: contribution.endHour,
|
|
133
|
+
verified: data.verified || false,
|
|
134
|
+
confidence: data.confidence || 0.5,
|
|
135
|
+
verification_reasons: data.verificationReasons || [],
|
|
136
|
+
timestamp: new Date(data.timestamp || Date.now()).toISOString(),
|
|
137
|
+
})
|
|
138
|
+
.select()
|
|
139
|
+
.single();
|
|
140
|
+
|
|
141
|
+
if (insertError) throw insertError;
|
|
142
|
+
|
|
143
|
+
return new Response(
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
success: true,
|
|
146
|
+
message: 'Alpha contribution submitted successfully',
|
|
147
|
+
contribution: insertedData,
|
|
148
|
+
}),
|
|
149
|
+
{ status: 200, headers }
|
|
150
|
+
);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Alpha POST error:', error);
|
|
153
|
+
return new Response(
|
|
154
|
+
JSON.stringify({
|
|
155
|
+
error: 'Failed to submit alpha contribution',
|
|
156
|
+
message: error.message,
|
|
157
|
+
}),
|
|
158
|
+
{ status: 500, headers }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return new Response(
|
|
164
|
+
JSON.stringify({ error: 'Method not allowed' }),
|
|
165
|
+
{ status: 405, headers }
|
|
166
|
+
);
|
|
167
|
+
}
|
package/dashboard/api/submit.js
CHANGED
|
@@ -65,6 +65,29 @@ export default async function handler(req) {
|
|
|
65
65
|
|
|
66
66
|
if (submissionError) throw submissionError;
|
|
67
67
|
|
|
68
|
+
// Handle alpha contribution if present
|
|
69
|
+
if (data.alpha_contribution) {
|
|
70
|
+
const alpha = data.alpha_contribution;
|
|
71
|
+
const { error: alphaError } = await supabase
|
|
72
|
+
.from('alpha_contributions')
|
|
73
|
+
.insert({
|
|
74
|
+
agent_name: data.agent_name,
|
|
75
|
+
location: data.location,
|
|
76
|
+
contribution_type: alpha.type,
|
|
77
|
+
start_hour: alpha.startHour,
|
|
78
|
+
end_hour: alpha.endHour,
|
|
79
|
+
verified: alpha.verified || false,
|
|
80
|
+
confidence: alpha.confidence || 0.5,
|
|
81
|
+
verification_reasons: alpha.verificationReasons || [],
|
|
82
|
+
timestamp: new Date(data.timestamp).toISOString(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Don't fail the entire request if alpha submission fails
|
|
86
|
+
if (alphaError) {
|
|
87
|
+
console.warn('Alpha contribution failed:', alphaError);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
// Update agent heartbeat (upsert)
|
|
69
92
|
const { error: heartbeatError } = await supabase
|
|
70
93
|
.from('agent_heartbeat')
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
-- DeCharge Scout Dashboard - Alpha Contributions Migration
|
|
2
|
+
-- Add this to your Supabase database to support verified local alpha contributions
|
|
3
|
+
|
|
4
|
+
-- ============================================================================
|
|
5
|
+
-- TABLE: alpha_contributions
|
|
6
|
+
-- Stores verified local knowledge contributions from agents
|
|
7
|
+
-- ============================================================================
|
|
8
|
+
CREATE TABLE IF NOT EXISTS alpha_contributions (
|
|
9
|
+
id SERIAL PRIMARY KEY,
|
|
10
|
+
agent_name VARCHAR(255) NOT NULL,
|
|
11
|
+
location VARCHAR(255) NOT NULL,
|
|
12
|
+
contribution_type VARCHAR(20) NOT NULL CHECK (contribution_type IN ('peak', 'cheap', 'general')),
|
|
13
|
+
start_hour INTEGER NOT NULL CHECK (start_hour >= 0 AND start_hour <= 23),
|
|
14
|
+
end_hour INTEGER NOT NULL CHECK (end_hour >= 0 AND end_hour <= 23),
|
|
15
|
+
|
|
16
|
+
-- Verification fields
|
|
17
|
+
verified BOOLEAN DEFAULT FALSE,
|
|
18
|
+
confidence DECIMAL(4, 3) CHECK (confidence >= 0 AND confidence <= 1),
|
|
19
|
+
verification_reasons TEXT[],
|
|
20
|
+
|
|
21
|
+
-- Metadata
|
|
22
|
+
timestamp TIMESTAMP NOT NULL,
|
|
23
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
24
|
+
|
|
25
|
+
-- Indexes for performance
|
|
26
|
+
CONSTRAINT valid_hour_range CHECK (start_hour <= end_hour)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- Indexes for efficient querying
|
|
30
|
+
CREATE INDEX idx_alpha_location ON alpha_contributions(location);
|
|
31
|
+
CREATE INDEX idx_alpha_verified ON alpha_contributions(verified);
|
|
32
|
+
CREATE INDEX idx_alpha_confidence ON alpha_contributions(confidence DESC);
|
|
33
|
+
CREATE INDEX idx_alpha_created_at ON alpha_contributions(created_at DESC);
|
|
34
|
+
CREATE INDEX idx_alpha_type ON alpha_contributions(contribution_type);
|
|
35
|
+
|
|
36
|
+
-- ============================================================================
|
|
37
|
+
-- VIEW: Verified Alpha Contributions (high confidence only)
|
|
38
|
+
-- ============================================================================
|
|
39
|
+
CREATE OR REPLACE VIEW verified_alpha_contributions AS
|
|
40
|
+
SELECT
|
|
41
|
+
id,
|
|
42
|
+
agent_name,
|
|
43
|
+
location,
|
|
44
|
+
contribution_type,
|
|
45
|
+
start_hour,
|
|
46
|
+
end_hour,
|
|
47
|
+
confidence,
|
|
48
|
+
verification_reasons,
|
|
49
|
+
timestamp,
|
|
50
|
+
created_at
|
|
51
|
+
FROM alpha_contributions
|
|
52
|
+
WHERE verified = TRUE AND confidence >= 0.6
|
|
53
|
+
ORDER BY confidence DESC, created_at DESC;
|
|
54
|
+
|
|
55
|
+
-- ============================================================================
|
|
56
|
+
-- VIEW: Alpha Contributions Summary by Location
|
|
57
|
+
-- ============================================================================
|
|
58
|
+
CREATE OR REPLACE VIEW alpha_by_location AS
|
|
59
|
+
SELECT
|
|
60
|
+
location,
|
|
61
|
+
COUNT(*) as total_contributions,
|
|
62
|
+
COUNT(CASE WHEN verified = TRUE THEN 1 END) as verified_count,
|
|
63
|
+
AVG(confidence) as avg_confidence,
|
|
64
|
+
MAX(created_at) as latest_contribution
|
|
65
|
+
FROM alpha_contributions
|
|
66
|
+
WHERE created_at > NOW() - INTERVAL '30 days'
|
|
67
|
+
GROUP BY location
|
|
68
|
+
ORDER BY verified_count DESC, total_contributions DESC;
|
|
69
|
+
|
|
70
|
+
-- ============================================================================
|
|
71
|
+
-- VIEW: Peak Hours Analysis (most commonly reported)
|
|
72
|
+
-- ============================================================================
|
|
73
|
+
CREATE OR REPLACE VIEW peak_hours_analysis AS
|
|
74
|
+
WITH hour_counts AS (
|
|
75
|
+
SELECT
|
|
76
|
+
location,
|
|
77
|
+
generate_series(start_hour, end_hour) as hour,
|
|
78
|
+
confidence,
|
|
79
|
+
contribution_type
|
|
80
|
+
FROM alpha_contributions
|
|
81
|
+
WHERE contribution_type = 'peak'
|
|
82
|
+
AND verified = TRUE
|
|
83
|
+
AND created_at > NOW() - INTERVAL '30 days'
|
|
84
|
+
)
|
|
85
|
+
SELECT
|
|
86
|
+
location,
|
|
87
|
+
hour,
|
|
88
|
+
COUNT(*) as report_count,
|
|
89
|
+
AVG(confidence) as avg_confidence,
|
|
90
|
+
COUNT(*) * AVG(confidence) as weighted_score
|
|
91
|
+
FROM hour_counts
|
|
92
|
+
GROUP BY location, hour
|
|
93
|
+
HAVING COUNT(*) >= 2 -- At least 2 reports
|
|
94
|
+
ORDER BY location, weighted_score DESC;
|
|
95
|
+
|
|
96
|
+
-- ============================================================================
|
|
97
|
+
-- FUNCTION: Get Alpha Insights for Location
|
|
98
|
+
-- ============================================================================
|
|
99
|
+
CREATE OR REPLACE FUNCTION get_alpha_insights(
|
|
100
|
+
p_location VARCHAR(255),
|
|
101
|
+
p_min_confidence DECIMAL DEFAULT 0.6
|
|
102
|
+
)
|
|
103
|
+
RETURNS TABLE (
|
|
104
|
+
contribution_type VARCHAR(20),
|
|
105
|
+
start_hour INTEGER,
|
|
106
|
+
end_hour INTEGER,
|
|
107
|
+
confidence DECIMAL(4, 3),
|
|
108
|
+
report_count BIGINT,
|
|
109
|
+
latest_report TIMESTAMP
|
|
110
|
+
) AS $$
|
|
111
|
+
BEGIN
|
|
112
|
+
RETURN QUERY
|
|
113
|
+
SELECT
|
|
114
|
+
ac.contribution_type,
|
|
115
|
+
ac.start_hour,
|
|
116
|
+
ac.end_hour,
|
|
117
|
+
AVG(ac.confidence)::DECIMAL(4, 3) as confidence,
|
|
118
|
+
COUNT(*) as report_count,
|
|
119
|
+
MAX(ac.created_at) as latest_report
|
|
120
|
+
FROM alpha_contributions ac
|
|
121
|
+
WHERE ac.location ILIKE '%' || p_location || '%'
|
|
122
|
+
AND ac.verified = TRUE
|
|
123
|
+
AND ac.confidence >= p_min_confidence
|
|
124
|
+
AND ac.created_at > NOW() - INTERVAL '30 days'
|
|
125
|
+
GROUP BY ac.contribution_type, ac.start_hour, ac.end_hour
|
|
126
|
+
ORDER BY report_count DESC, confidence DESC
|
|
127
|
+
LIMIT 10;
|
|
128
|
+
END;
|
|
129
|
+
$$ LANGUAGE plpgsql;
|
|
130
|
+
|
|
131
|
+
-- ============================================================================
|
|
132
|
+
-- FUNCTION: Calculate Contribution Quality Score
|
|
133
|
+
-- ============================================================================
|
|
134
|
+
CREATE OR REPLACE FUNCTION calculate_contribution_quality(
|
|
135
|
+
p_agent_name VARCHAR(255)
|
|
136
|
+
)
|
|
137
|
+
RETURNS TABLE (
|
|
138
|
+
agent_name VARCHAR(255),
|
|
139
|
+
total_contributions BIGINT,
|
|
140
|
+
verified_contributions BIGINT,
|
|
141
|
+
avg_confidence DECIMAL(4, 3),
|
|
142
|
+
quality_score DECIMAL(6, 2)
|
|
143
|
+
) AS $$
|
|
144
|
+
BEGIN
|
|
145
|
+
RETURN QUERY
|
|
146
|
+
SELECT
|
|
147
|
+
p_agent_name::VARCHAR(255),
|
|
148
|
+
COUNT(*)::BIGINT as total_contributions,
|
|
149
|
+
COUNT(CASE WHEN verified = TRUE THEN 1 END)::BIGINT as verified_contributions,
|
|
150
|
+
AVG(confidence)::DECIMAL(4, 3) as avg_confidence,
|
|
151
|
+
(
|
|
152
|
+
(COUNT(CASE WHEN verified = TRUE THEN 1 END)::DECIMAL / NULLIF(COUNT(*), 0)) * 100 +
|
|
153
|
+
(AVG(confidence) * 100)
|
|
154
|
+
)::DECIMAL(6, 2) as quality_score
|
|
155
|
+
FROM alpha_contributions
|
|
156
|
+
WHERE agent_name = p_agent_name;
|
|
157
|
+
END;
|
|
158
|
+
$$ LANGUAGE plpgsql;
|
|
159
|
+
|
|
160
|
+
-- ============================================================================
|
|
161
|
+
-- COMMENTS for documentation
|
|
162
|
+
-- ============================================================================
|
|
163
|
+
COMMENT ON TABLE alpha_contributions IS 'Stores verified local energy pricing knowledge from agents';
|
|
164
|
+
COMMENT ON COLUMN alpha_contributions.verified IS 'TRUE if contribution matches actual pricing data';
|
|
165
|
+
COMMENT ON COLUMN alpha_contributions.confidence IS 'Confidence score 0-1, higher means better match with actual data';
|
|
166
|
+
COMMENT ON COLUMN alpha_contributions.verification_reasons IS 'Array of reasons explaining verification result';
|
|
167
|
+
|
|
168
|
+
-- ============================================================================
|
|
169
|
+
-- Sample queries for testing
|
|
170
|
+
-- ============================================================================
|
|
171
|
+
|
|
172
|
+
-- Get all verified contributions for a location
|
|
173
|
+
-- SELECT * FROM verified_alpha_contributions WHERE location ILIKE '%mumbai%';
|
|
174
|
+
|
|
175
|
+
-- Get alpha insights for a specific location
|
|
176
|
+
-- SELECT * FROM get_alpha_insights('Mumbai', 0.7);
|
|
177
|
+
|
|
178
|
+
-- Get contribution quality for an agent
|
|
179
|
+
-- SELECT * FROM calculate_contribution_quality('Agent-X8DOAO');
|
|
180
|
+
|
|
181
|
+
-- Get peak hours analysis
|
|
182
|
+
-- SELECT * FROM peak_hours_analysis WHERE location ILIKE '%india%';
|
|
183
|
+
|
|
184
|
+
-- Get alpha summary by location
|
|
185
|
+
-- SELECT * FROM alpha_by_location ORDER BY verified_count DESC LIMIT 10;
|
|
@@ -213,6 +213,132 @@
|
|
|
213
213
|
font-weight: bold;
|
|
214
214
|
color: #667eea;
|
|
215
215
|
}
|
|
216
|
+
|
|
217
|
+
.alpha-grid {
|
|
218
|
+
display: grid;
|
|
219
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
220
|
+
gap: 20px;
|
|
221
|
+
margin-top: 20px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.alpha-card {
|
|
225
|
+
background: #f8f9fa;
|
|
226
|
+
padding: 20px;
|
|
227
|
+
border-radius: 8px;
|
|
228
|
+
border-left: 4px solid #4caf50;
|
|
229
|
+
transition: transform 0.2s;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.alpha-card:hover {
|
|
233
|
+
transform: translateY(-2px);
|
|
234
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.alpha-card.unverified {
|
|
238
|
+
border-left-color: #ff9800;
|
|
239
|
+
opacity: 0.8;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.alpha-header {
|
|
243
|
+
display: flex;
|
|
244
|
+
justify-content: space-between;
|
|
245
|
+
align-items: center;
|
|
246
|
+
margin-bottom: 12px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.alpha-type {
|
|
250
|
+
display: inline-block;
|
|
251
|
+
padding: 4px 10px;
|
|
252
|
+
background: #4caf50;
|
|
253
|
+
color: white;
|
|
254
|
+
border-radius: 12px;
|
|
255
|
+
font-size: 0.75em;
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
text-transform: uppercase;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.alpha-type.cheap {
|
|
261
|
+
background: #2196f3;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.confidence-badge {
|
|
265
|
+
display: inline-block;
|
|
266
|
+
padding: 4px 10px;
|
|
267
|
+
border-radius: 12px;
|
|
268
|
+
font-size: 0.75em;
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.confidence-high {
|
|
273
|
+
background: #c8e6c9;
|
|
274
|
+
color: #2e7d32;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.confidence-medium {
|
|
278
|
+
background: #fff9c4;
|
|
279
|
+
color: #f57f17;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.confidence-low {
|
|
283
|
+
background: #ffccbc;
|
|
284
|
+
color: #d84315;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.alpha-hours {
|
|
288
|
+
font-size: 1.5em;
|
|
289
|
+
font-weight: bold;
|
|
290
|
+
color: #333;
|
|
291
|
+
margin: 10px 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.alpha-location {
|
|
295
|
+
color: #666;
|
|
296
|
+
font-size: 0.9em;
|
|
297
|
+
margin-bottom: 8px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.alpha-agent {
|
|
301
|
+
color: #999;
|
|
302
|
+
font-size: 0.8em;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.verification-reason {
|
|
306
|
+
background: white;
|
|
307
|
+
padding: 8px;
|
|
308
|
+
border-radius: 4px;
|
|
309
|
+
margin-top: 10px;
|
|
310
|
+
font-size: 0.85em;
|
|
311
|
+
color: #666;
|
|
312
|
+
border-left: 3px solid #4caf50;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.verification-reason.warning {
|
|
316
|
+
border-left-color: #ff9800;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.verified-badge {
|
|
320
|
+
display: inline-flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 4px;
|
|
323
|
+
padding: 4px 8px;
|
|
324
|
+
background: #c8e6c9;
|
|
325
|
+
color: #2e7d32;
|
|
326
|
+
border-radius: 12px;
|
|
327
|
+
font-size: 0.75em;
|
|
328
|
+
font-weight: 600;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.unverified-badge {
|
|
332
|
+
display: inline-flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
gap: 4px;
|
|
335
|
+
padding: 4px 8px;
|
|
336
|
+
background: #ffccbc;
|
|
337
|
+
color: #d84315;
|
|
338
|
+
border-radius: 12px;
|
|
339
|
+
font-size: 0.75em;
|
|
340
|
+
font-weight: 600;
|
|
341
|
+
}
|
|
216
342
|
</style>
|
|
217
343
|
</head>
|
|
218
344
|
<body>
|
|
@@ -255,6 +381,11 @@
|
|
|
255
381
|
<div id="locations-map" class="map-grid"></div>
|
|
256
382
|
</div>
|
|
257
383
|
|
|
384
|
+
<div class="section">
|
|
385
|
+
<h2>💡 Verified Local Alpha (Community Knowledge)</h2>
|
|
386
|
+
<div id="alpha-container"></div>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
258
389
|
<div class="section">
|
|
259
390
|
<h2>📊 Recent Submissions</h2>
|
|
260
391
|
<button class="refresh-btn" onclick="loadData()">Refresh Data</button>
|
|
@@ -295,6 +426,9 @@
|
|
|
295
426
|
// Render submissions table
|
|
296
427
|
renderSubmissionsTable(data.recentSubmissions || []);
|
|
297
428
|
|
|
429
|
+
// Load alpha contributions
|
|
430
|
+
await loadAlphaContributions();
|
|
431
|
+
|
|
298
432
|
// Update timestamp
|
|
299
433
|
document.getElementById('last-updated').textContent =
|
|
300
434
|
`Last updated: ${new Date().toLocaleTimeString()}`;
|
|
@@ -308,6 +442,24 @@
|
|
|
308
442
|
}
|
|
309
443
|
}
|
|
310
444
|
|
|
445
|
+
async function loadAlphaContributions() {
|
|
446
|
+
try {
|
|
447
|
+
const response = await fetch(`${API_BASE}/api/alpha?verified=true&min_confidence=0.6`);
|
|
448
|
+
|
|
449
|
+
if (!response.ok) {
|
|
450
|
+
console.warn('Alpha API not available yet');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const data = await response.json();
|
|
455
|
+
renderAlphaContributions(data.contributions || [], data.statistics || []);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
console.warn('Error loading alpha contributions:', error);
|
|
458
|
+
document.getElementById('alpha-container').innerHTML =
|
|
459
|
+
'<div class="loading">Alpha contributions will appear here once the database is migrated</div>';
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
311
463
|
function renderLocationsMap(locations) {
|
|
312
464
|
const container = document.getElementById('locations-map');
|
|
313
465
|
|
|
@@ -325,6 +477,90 @@
|
|
|
325
477
|
`).join('');
|
|
326
478
|
}
|
|
327
479
|
|
|
480
|
+
function renderAlphaContributions(contributions, statistics) {
|
|
481
|
+
const container = document.getElementById('alpha-container');
|
|
482
|
+
|
|
483
|
+
if (contributions.length === 0) {
|
|
484
|
+
container.innerHTML = '<div class="loading">No verified alpha contributions yet. Share your local knowledge to be the first!</div>';
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Show top locations first
|
|
489
|
+
let statsHtml = '';
|
|
490
|
+
if (statistics.length > 0) {
|
|
491
|
+
statsHtml = `
|
|
492
|
+
<div class="map-grid" style="margin-bottom: 20px;">
|
|
493
|
+
${statistics.slice(0, 5).map(stat => `
|
|
494
|
+
<div class="location-card">
|
|
495
|
+
<h4>${escapeHtml(stat.location)}</h4>
|
|
496
|
+
<div class="count">${stat.verified_count}</div>
|
|
497
|
+
<div class="label">verified contributions</div>
|
|
498
|
+
<div style="margin-top: 8px; color: #666; font-size: 0.85em;">
|
|
499
|
+
Avg confidence: ${(stat.avg_confidence * 100).toFixed(0)}%
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
`).join('')}
|
|
503
|
+
</div>
|
|
504
|
+
`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
container.innerHTML = statsHtml + `
|
|
508
|
+
<div class="alpha-grid">
|
|
509
|
+
${contributions.slice(0, 12).map(alpha => {
|
|
510
|
+
const confidence = parseFloat(alpha.confidence);
|
|
511
|
+
const confidenceClass = confidence >= 0.8 ? 'confidence-high' :
|
|
512
|
+
confidence >= 0.6 ? 'confidence-medium' : 'confidence-low';
|
|
513
|
+
const confidenceLabel = confidence >= 0.8 ? 'High' :
|
|
514
|
+
confidence >= 0.6 ? 'Medium' : 'Low';
|
|
515
|
+
|
|
516
|
+
const formatHour = (h) => {
|
|
517
|
+
const hour = parseInt(h);
|
|
518
|
+
if (hour === 0) return '12 AM';
|
|
519
|
+
if (hour < 12) return `${hour} AM`;
|
|
520
|
+
if (hour === 12) return '12 PM';
|
|
521
|
+
return `${hour - 12} PM`;
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const typeClass = alpha.contribution_type === 'peak' ? 'alpha-type' : 'alpha-type cheap';
|
|
525
|
+
|
|
526
|
+
return `
|
|
527
|
+
<div class="alpha-card ${alpha.verified ? '' : 'unverified'}">
|
|
528
|
+
<div class="alpha-header">
|
|
529
|
+
<span class="${typeClass}">${alpha.contribution_type}</span>
|
|
530
|
+
${alpha.verified
|
|
531
|
+
? `<span class="verified-badge">✓ Verified</span>`
|
|
532
|
+
: `<span class="unverified-badge">⚠ Unverified</span>`
|
|
533
|
+
}
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<div class="alpha-hours">
|
|
537
|
+
${formatHour(alpha.start_hour)} - ${formatHour(alpha.end_hour)}
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<div class="alpha-location">📍 ${escapeHtml(alpha.location)}</div>
|
|
541
|
+
|
|
542
|
+
<div class="alpha-agent">
|
|
543
|
+
By ${escapeHtml(alpha.agent_name)} • ${new Date(alpha.created_at).toLocaleDateString()}
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
<div style="margin-top: 12px;">
|
|
547
|
+
<span class="confidence-badge ${confidenceClass}">
|
|
548
|
+
${confidenceLabel} Confidence (${(confidence * 100).toFixed(0)}%)
|
|
549
|
+
</span>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
${alpha.verification_reasons && alpha.verification_reasons.length > 0 ? `
|
|
553
|
+
<div class="verification-reason ${confidence < 0.6 ? 'warning' : ''}">
|
|
554
|
+
${escapeHtml(alpha.verification_reasons[0])}
|
|
555
|
+
</div>
|
|
556
|
+
` : ''}
|
|
557
|
+
</div>
|
|
558
|
+
`;
|
|
559
|
+
}).join('')}
|
|
560
|
+
</div>
|
|
561
|
+
`;
|
|
562
|
+
}
|
|
563
|
+
|
|
328
564
|
function renderSubmissionsTable(submissions) {
|
|
329
565
|
const container = document.getElementById('submissions-container');
|
|
330
566
|
|
package/index.js
CHANGED
|
@@ -33,7 +33,7 @@ dotenv.config({ path: path.join(process.cwd(), '.env') });
|
|
|
33
33
|
// Import modules
|
|
34
34
|
import { startWalletServer, openWalletConnection, waitForWalletConnection, getConnectedWallet } from './src/wallet-server.js';
|
|
35
35
|
import { setConnectedWallet, getBalance, checkBalance, mockStake, refundStake } from './src/browser-wallet.js';
|
|
36
|
-
import { getWeatherForLocation } from './src/weather-data.js';
|
|
36
|
+
import { getWeatherForLocation, getFallbackCoordinates } from './src/weather-data.js';
|
|
37
37
|
import { generateSmartPricing, getPricingInsights, getRegionalPricing } from './src/smart-pricing.js';
|
|
38
38
|
import { findCheapestWindow, calculateSavings } from './src/optimizer.js';
|
|
39
39
|
import { submitToOracle } from './src/oracle.js';
|
|
@@ -53,6 +53,7 @@ const CYCLE_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
|
|
53
53
|
let isRunning = true;
|
|
54
54
|
let totalRuns = 0;
|
|
55
55
|
let stakeTransactionSignature = null;
|
|
56
|
+
let pendingAlphaContribution = null; // Store alpha contribution for next submission
|
|
56
57
|
|
|
57
58
|
// Readline interface for prompts
|
|
58
59
|
const rl = createInterface({
|
|
@@ -172,16 +173,26 @@ async function main(options) {
|
|
|
172
173
|
const detectedLocation = await getLocation();
|
|
173
174
|
locationSpinner.succeed(chalk.green(`📍 Detected Location: ${detectedLocation}`));
|
|
174
175
|
|
|
175
|
-
// Ask user to confirm or override
|
|
176
|
+
// Ask user to confirm or override with 10 second timeout
|
|
176
177
|
console.log(chalk.blue('\nYou can use this location or enter a custom one.'));
|
|
177
178
|
console.log(chalk.gray('(Press Enter to use detected location, or type a custom location like "Dallas,TX,USA")'));
|
|
179
|
+
console.log(chalk.gray('(Will auto-proceed in 10 seconds if no input)\n'));
|
|
178
180
|
|
|
179
181
|
try {
|
|
180
|
-
|
|
182
|
+
// Create a promise race between user input and timeout
|
|
183
|
+
const customLocation = await Promise.race([
|
|
184
|
+
question('Enter location (or press Enter for auto-detected): '),
|
|
185
|
+
new Promise((resolve) => {
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
console.log(chalk.yellow('\n⏱️ Timeout - using detected location'));
|
|
188
|
+
resolve('');
|
|
189
|
+
}, 10000); // 10 second timeout
|
|
190
|
+
})
|
|
191
|
+
]);
|
|
181
192
|
|
|
182
193
|
// Use custom location if provided, otherwise use detected
|
|
183
|
-
location = customLocation.trim() || detectedLocation;
|
|
184
|
-
if (customLocation.trim()) {
|
|
194
|
+
location = (customLocation || '').trim() || detectedLocation;
|
|
195
|
+
if ((customLocation || '').trim()) {
|
|
185
196
|
console.log(chalk.green(`✓ Using custom location: ${location}`));
|
|
186
197
|
} else {
|
|
187
198
|
console.log(chalk.green(`✓ Using detected location: ${location}`));
|
|
@@ -277,7 +288,38 @@ async function runQueryCycle(wallet, agentName, location, options) {
|
|
|
277
288
|
|
|
278
289
|
} catch (error) {
|
|
279
290
|
weatherSpinner.fail(chalk.red('Failed to fetch weather data'));
|
|
280
|
-
|
|
291
|
+
|
|
292
|
+
// Check if we have fallback coordinates available
|
|
293
|
+
const fallbackCoords = getFallbackCoordinates(location);
|
|
294
|
+
|
|
295
|
+
console.log(chalk.yellow(`\n⚠️ Could not geocode location: "${location}"`));
|
|
296
|
+
console.log(chalk.yellow(` Error: ${error.message}`));
|
|
297
|
+
|
|
298
|
+
if (fallbackCoords) {
|
|
299
|
+
console.log(chalk.blue(`\n💡 We have fallback coordinates for: ${fallbackCoords.name}, ${fallbackCoords.country}`));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log(chalk.blue(`\nOptions:`));
|
|
303
|
+
console.log(chalk.gray(` 1. Try a different location (e.g., just "Hyderabad" or "Hyderabad, India")`));
|
|
304
|
+
if (fallbackCoords) {
|
|
305
|
+
console.log(chalk.gray(` 2. Use fallback coordinates for ${fallbackCoords.name}`));
|
|
306
|
+
}
|
|
307
|
+
console.log(chalk.gray(` 3. Exit and restart\n`));
|
|
308
|
+
|
|
309
|
+
const userChoice = await question(chalk.blue('Enter new location (or press Enter to exit): '));
|
|
310
|
+
|
|
311
|
+
if (!userChoice.trim()) {
|
|
312
|
+
console.log(chalk.yellow('Exiting...'));
|
|
313
|
+
throw new Error('User chose to exit');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Update location and retry this cycle
|
|
317
|
+
location = userChoice.trim();
|
|
318
|
+
console.log(chalk.green(`✓ Updated location to: ${location}`));
|
|
319
|
+
console.log(chalk.gray('Retrying...\n'));
|
|
320
|
+
|
|
321
|
+
// Retry with new location - this will loop back and try again
|
|
322
|
+
continue;
|
|
281
323
|
}
|
|
282
324
|
|
|
283
325
|
// Run optimization
|
|
@@ -341,14 +383,22 @@ async function runQueryCycle(wallet, agentName, location, options) {
|
|
|
341
383
|
console.log(chalk.blue(`\n🌐 Dashboard API URL: ${apiUrl}`));
|
|
342
384
|
console.log(chalk.gray(`📤 Submitting data...`));
|
|
343
385
|
|
|
386
|
+
const dashboardPayload = {
|
|
387
|
+
...submissionData,
|
|
388
|
+
wallet: wallet, // wallet parameter from runQueryCycle
|
|
389
|
+
run_number: totalRuns
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Include alpha contribution if available
|
|
393
|
+
if (pendingAlphaContribution) {
|
|
394
|
+
dashboardPayload.alpha_contribution = pendingAlphaContribution;
|
|
395
|
+
console.log(chalk.blue(`📊 Including verified alpha contribution (${(pendingAlphaContribution.confidence * 100).toFixed(0)}% confidence)`));
|
|
396
|
+
}
|
|
397
|
+
|
|
344
398
|
const response = await fetch(apiUrl, {
|
|
345
399
|
method: 'POST',
|
|
346
400
|
headers: { 'Content-Type': 'application/json' },
|
|
347
|
-
body: JSON.stringify(
|
|
348
|
-
...submissionData,
|
|
349
|
-
wallet: wallet, // wallet parameter from runQueryCycle
|
|
350
|
-
run_number: totalRuns
|
|
351
|
-
})
|
|
401
|
+
body: JSON.stringify(dashboardPayload)
|
|
352
402
|
});
|
|
353
403
|
|
|
354
404
|
const responseText = await response.text();
|
|
@@ -358,6 +408,12 @@ async function runQueryCycle(wallet, agentName, location, options) {
|
|
|
358
408
|
dashboardSpinner.succeed(chalk.green('✅ Dashboard submission successful!'));
|
|
359
409
|
console.log(chalk.green(`🎉 Data should now appear at: https://decharge-scout.vercel.app/agentone`));
|
|
360
410
|
console.log(chalk.gray(`Response: ${responseText}`));
|
|
411
|
+
|
|
412
|
+
// Clear pending alpha contribution after successful submission
|
|
413
|
+
if (pendingAlphaContribution) {
|
|
414
|
+
console.log(chalk.green(` ✓ Alpha contribution synced to dashboard`));
|
|
415
|
+
pendingAlphaContribution = null;
|
|
416
|
+
}
|
|
361
417
|
} else {
|
|
362
418
|
dashboardSpinner.warn(chalk.yellow(`⚠️ Dashboard API returned: ${response.status}`));
|
|
363
419
|
console.log(chalk.yellow(`Response: ${responseText}`));
|
|
@@ -420,6 +476,17 @@ async function runQueryCycle(wallet, agentName, location, options) {
|
|
|
420
476
|
// Save contribution with verification status
|
|
421
477
|
const contribution = saveAlphaContribution(parsed, agentName, location, verification);
|
|
422
478
|
|
|
479
|
+
// Store for dashboard submission
|
|
480
|
+
pendingAlphaContribution = {
|
|
481
|
+
type: parsed.type,
|
|
482
|
+
startHour: parsed.startHour,
|
|
483
|
+
endHour: parsed.endHour,
|
|
484
|
+
location: parsed.location,
|
|
485
|
+
verified: verification.verified,
|
|
486
|
+
confidence: verification.confidence,
|
|
487
|
+
verificationReasons: verification.reasons
|
|
488
|
+
};
|
|
489
|
+
|
|
423
490
|
// Calculate bonus (higher for verified contributions)
|
|
424
491
|
const alphaBonus = calculateAlphaBonus(parsed, verification);
|
|
425
492
|
|
package/package.json
CHANGED
package/src/weather-data.js
CHANGED
|
@@ -12,34 +12,57 @@ import fetch from 'node-fetch';
|
|
|
12
12
|
/**
|
|
13
13
|
* Get coordinates from location string
|
|
14
14
|
* Simple geocoding using Open-Meteo's geocoding API (also free!)
|
|
15
|
+
* Tries multiple search strategies for better success rate
|
|
15
16
|
*/
|
|
16
17
|
export async function getCoordinatesFromLocation(location) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
// Try multiple search strategies
|
|
19
|
+
const searchTerms = [
|
|
20
|
+
location, // Original: "Hyderabad, TS, IN"
|
|
21
|
+
location.split(',')[0].trim(), // Just city: "Hyderabad"
|
|
22
|
+
location.replace(/,\s*[A-Z]{2}\s*,/, ','), // Remove state: "Hyderabad, IN"
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Remove duplicates
|
|
26
|
+
const uniqueSearchTerms = [...new Set(searchTerms)];
|
|
27
|
+
|
|
28
|
+
let lastError = null;
|
|
29
|
+
|
|
30
|
+
for (const searchTerm of uniqueSearchTerms) {
|
|
31
|
+
try {
|
|
32
|
+
const locationEncoded = encodeURIComponent(searchTerm);
|
|
33
|
+
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${locationEncoded}&count=5&language=en&format=json`;
|
|
34
|
+
|
|
35
|
+
const response = await fetch(url);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
lastError = new Error(`Geocoding failed: ${response.status}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
|
|
43
|
+
if (!data.results || data.results.length === 0) {
|
|
44
|
+
lastError = new Error(`Location "${searchTerm}" not found`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Use first result
|
|
49
|
+
const result = data.results[0];
|
|
50
|
+
console.log(`✓ Geocoded "${searchTerm}" → ${result.name}, ${result.country} (${result.latitude}, ${result.longitude})`);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
latitude: result.latitude,
|
|
54
|
+
longitude: result.longitude,
|
|
55
|
+
name: result.name,
|
|
56
|
+
country: result.country,
|
|
57
|
+
timezone: result.timezone || 'auto'
|
|
58
|
+
};
|
|
59
|
+
} catch (error) {
|
|
60
|
+
lastError = error;
|
|
61
|
+
continue;
|
|
24
62
|
}
|
|
25
|
-
|
|
26
|
-
const data = await response.json();
|
|
27
|
-
|
|
28
|
-
if (!data.results || data.results.length === 0) {
|
|
29
|
-
throw new Error('Location not found');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const result = data.results[0];
|
|
33
|
-
return {
|
|
34
|
-
latitude: result.latitude,
|
|
35
|
-
longitude: result.longitude,
|
|
36
|
-
name: result.name,
|
|
37
|
-
country: result.country,
|
|
38
|
-
timezone: result.timezone || 'UTC'
|
|
39
|
-
};
|
|
40
|
-
} catch (error) {
|
|
41
|
-
throw new Error(`Failed to geocode location: ${error.message}`);
|
|
42
63
|
}
|
|
64
|
+
|
|
65
|
+
throw new Error(`Failed to geocode location after trying: ${uniqueSearchTerms.join(', ')}. Last error: ${lastError?.message}`);
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
/**
|
|
@@ -95,23 +118,47 @@ export async function fetchWeatherForecast(latitude, longitude, timezone = 'auto
|
|
|
95
118
|
}
|
|
96
119
|
}
|
|
97
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Known coordinates for common cities (fallback)
|
|
123
|
+
*/
|
|
124
|
+
const KNOWN_CITIES = {
|
|
125
|
+
'hyderabad': { latitude: 17.385, longitude: 78.487, name: 'Hyderabad', country: 'India', timezone: 'Asia/Kolkata' },
|
|
126
|
+
'mumbai': { latitude: 19.076, longitude: 72.877, name: 'Mumbai', country: 'India', timezone: 'Asia/Kolkata' },
|
|
127
|
+
'delhi': { latitude: 28.704, longitude: 77.102, name: 'Delhi', country: 'India', timezone: 'Asia/Kolkata' },
|
|
128
|
+
'bangalore': { latitude: 12.972, longitude: 77.594, name: 'Bangalore', country: 'India', timezone: 'Asia/Kolkata' },
|
|
129
|
+
'chennai': { latitude: 13.083, longitude: 80.270, name: 'Chennai', country: 'India', timezone: 'Asia/Kolkata' },
|
|
130
|
+
'lagos': { latitude: 6.524, longitude: 3.379, name: 'Lagos', country: 'Nigeria', timezone: 'Africa/Lagos' },
|
|
131
|
+
'london': { latitude: 51.509, longitude: -0.118, name: 'London', country: 'United Kingdom', timezone: 'Europe/London' },
|
|
132
|
+
'new york': { latitude: 40.713, longitude: -74.006, name: 'New York', country: 'United States', timezone: 'America/New_York' },
|
|
133
|
+
'los angeles': { latitude: 34.052, longitude: -118.244, name: 'Los Angeles', country: 'United States', timezone: 'America/Los_Angeles' },
|
|
134
|
+
'chicago': { latitude: 41.878, longitude: -87.630, name: 'Chicago', country: 'United States', timezone: 'America/Chicago' },
|
|
135
|
+
'houston': { latitude: 29.760, longitude: -95.369, name: 'Houston', country: 'United States', timezone: 'America/Chicago' },
|
|
136
|
+
'dallas': { latitude: 32.776, longitude: -96.797, name: 'Dallas', country: 'United States', timezone: 'America/Chicago' },
|
|
137
|
+
};
|
|
138
|
+
|
|
98
139
|
/**
|
|
99
140
|
* Get weather data for a location
|
|
141
|
+
* Throws error if geocoding fails - caller should handle user interaction
|
|
100
142
|
*/
|
|
101
143
|
export async function getWeatherForLocation(location) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
console.log(`📍 Location: ${coords.name}, ${coords.country} (${coords.latitude}, ${coords.longitude})`);
|
|
144
|
+
// Try geocoding - throw error if fails
|
|
145
|
+
const coords = await getCoordinatesFromLocation(location);
|
|
146
|
+
console.log(`📍 Location: ${coords.name}, ${coords.country} (${coords.latitude}, ${coords.longitude})`);
|
|
106
147
|
|
|
107
|
-
|
|
108
|
-
|
|
148
|
+
// Get weather forecast
|
|
149
|
+
const forecast = await fetchWeatherForecast(coords.latitude, coords.longitude, coords.timezone);
|
|
109
150
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
151
|
+
return {
|
|
152
|
+
location: coords,
|
|
153
|
+
forecast
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get fallback coordinates for known cities
|
|
159
|
+
* Returns null if city not in database
|
|
160
|
+
*/
|
|
161
|
+
export function getFallbackCoordinates(location) {
|
|
162
|
+
const cityKey = location.split(',')[0].trim().toLowerCase();
|
|
163
|
+
return KNOWN_CITIES[cityKey] || null;
|
|
117
164
|
}
|