diff --git a/package-lock.json b/package-lock.json index 124fa5d..3f7ffb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@react-pdf/renderer": "^4.3.0", "framer-motion": "^12.12.1", + "lucide-react": "^0.515.0", "mongodb": "^6.3.0", "mongoose": "^8.15.0", "next": "14.1.0", @@ -1754,6 +1755,15 @@ "dev": true, "license": "ISC" }, + "node_modules/lucide-react": { + "version": "0.515.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.515.0.tgz", + "integrity": "sha512-Sy7bY0MeicRm2pzrnoHm2h6C1iVoeHyBU2fjdQDsXGP51fhkhau1/ZV/dzrcxEmAKsxYb6bGaIsMnGHuQ5s0dw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/media-engine": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", diff --git a/package.json b/package.json index 8bb0d1a..a9e0ba2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@react-pdf/renderer": "^4.3.0", "framer-motion": "^12.12.1", + "lucide-react": "^0.515.0", "mongodb": "^6.3.0", "mongoose": "^8.15.0", "next": "14.1.0", diff --git a/src/app/api/export-report/route.js b/src/app/api/export-report/route.js index 2afa1d2..3246e4c 100644 --- a/src/app/api/export-report/route.js +++ b/src/app/api/export-report/route.js @@ -63,7 +63,7 @@ function calculateStatistics(data) { } } else { if (isCharging && chargingStart) { - stats.battery.chargingTime += (timestamp - chargingStart) / (1000 * 60) // minutes + stats.battery.chargingTime += (timestamp - chargingStart) / (1000 * 60) isCharging = false chargingStart = null } @@ -360,7 +360,6 @@ async function generatePDF(stats, timeRange, data) { height: 300 }) - // Draw power statistics const powerStats = [ `Average PV Input Power: ${stats.power.avgPvInput.toFixed(1)}W`, `Maximum PV Input Power: ${stats.power.maxPvInput.toFixed(1)}W`, @@ -390,10 +389,8 @@ async function generatePDF(stats, timeRange, data) { drawFooter(page2) - // Page 3: Temperature and System Health drawHeader(page3, 'System Health') - // Draw temperature chart const temperaturePdfImage = await pdfDoc.embedPng(charts.temperatureImage) page3.drawImage(temperaturePdfImage, { x: margin, @@ -402,7 +399,6 @@ async function generatePDF(stats, timeRange, data) { height: 300 }) - // Draw temperature statistics const tempStats = [ `Average Inverter Temperature: ${stats.temperature.avgTemp.toFixed(1)}°C`, `Maximum Inverter Temperature: ${stats.temperature.maxTemp.toFixed(1)}°C`, @@ -427,7 +423,6 @@ async function generatePDF(stats, timeRange, data) { }) }) - // Draw system health summary const healthSummary = [ 'System Health Summary:', `• Battery Health: ${stats.battery.avgCapacity > 80 ? 'Excellent' : stats.battery.avgCapacity > 60 ? 'Good' : 'Needs Attention'}`, @@ -455,7 +450,6 @@ async function generatePDF(stats, timeRange, data) { drawFooter(page3) - // Save the PDF return await pdfDoc.save() } diff --git a/src/app/api/predictions/route.js b/src/app/api/predictions/route.js index 0e7649d..ee2ade3 100644 --- a/src/app/api/predictions/route.js +++ b/src/app/api/predictions/route.js @@ -1,183 +1,256 @@ import { NextResponse } from 'next/server'; import { connectToDatabase } from '@/lib/mongodb'; -const CACHE_DURATION = 2 * 60 * 60 * 1000; // 2 hours in milliseconds - -// Solar panel efficiency factors based on real-world data const WEATHER_IMPACT = { - // Cloud coverage impact on solar efficiency (based on NREL data) CLOUD_IMPACT: { - CLEAR: 1.0, // 0-10% clouds - PARTLY_CLOUDY: 0.7, // 11-50% clouds - MOSTLY_CLOUDY: 0.4, // 51-90% clouds - OVERCAST: 0.2, // 91-100% clouds + CLEAR: 1.0, + PARTLY_CLOUDY: 0.75, + MOSTLY_CLOUDY: 0.5, + OVERCAST: 0.25, }, - // Temperature impact (based on solar panel temperature coefficient) TEMP_IMPACT: { - OPTIMAL: 1.0, // 25°C (optimal temperature) - COLD: 0.95, // < 10°C - HOT: 0.85, // > 35°C + OPTIMAL: 1.0, + COLD: 0.9, + HOT: 0.8, }, - // Rain impact RAIN_IMPACT: { - NONE: 1.0, // 0% chance - LIGHT: 0.8, // 1-30% chance - MODERATE: 0.6, // 31-60% chance - HEAVY: 0.4, // 61-100% chance + NONE: 1.0, + LIGHT: 0.7, + HEAVY: 0.3, } }; -async function fetchWeatherData() { - const response = await fetch( - `http://api.weatherapi.com/v1/forecast.json?key=${process.env.WEATHER_API_KEY}&q=47.06149235737582,28.86683069635403&days=7&aqi=no` - ); - if (!response.ok) throw new Error('Failed to fetch weather data'); - return response.json(); -} +function getCloudImpact(description) { + if (!description) return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY; -function getCloudImpact(cloudCover) { - if (cloudCover <= 10) return WEATHER_IMPACT.CLOUD_IMPACT.CLEAR; - if (cloudCover <= 50) return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY; - if (cloudCover <= 90) return WEATHER_IMPACT.CLOUD_IMPACT.MOSTLY_CLOUDY; - return WEATHER_IMPACT.CLOUD_IMPACT.OVERCAST; + const desc = description.toLowerCase(); + if (desc.includes('clear') || desc.includes('sunny')) return WEATHER_IMPACT.CLOUD_IMPACT.CLEAR; + if (desc.includes('few clouds') || desc.includes('partly')) return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY; + if (desc.includes('scattered') || desc.includes('mostly')) return WEATHER_IMPACT.CLOUD_IMPACT.MOSTLY_CLOUDY; + if (desc.includes('overcast') || desc.includes('cloudy')) return WEATHER_IMPACT.CLOUD_IMPACT.OVERCAST; + return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY; } function getTempImpact(temp) { - if (temp >= 10 && temp <= 35) { - // Linear interpolation for temperatures between optimal ranges - if (temp <= 25) { - return 1.0 - ((25 - temp) * 0.003); // 0.3% loss per degree below optimal - } else { - return 1.0 - ((temp - 25) * 0.01); // 1% loss per degree above optimal - } + if (typeof temp !== 'number' || isNaN(temp)) return WEATHER_IMPACT.TEMP_IMPACT.OPTIMAL; + + if (temp >= 20 && temp <= 25) return WEATHER_IMPACT.TEMP_IMPACT.OPTIMAL; + if (temp < 10) return WEATHER_IMPACT.TEMP_IMPACT.COLD; + if (temp > 35) return WEATHER_IMPACT.TEMP_IMPACT.HOT; + + if (temp < 20) { + return 0.9 + ((temp - 10) / 10) * 0.1; + } else { + return 1.0 - ((temp - 25) / 10) * 0.2; } - return temp < 10 ? WEATHER_IMPACT.TEMP_IMPACT.COLD : WEATHER_IMPACT.TEMP_IMPACT.HOT; } -function getRainImpact(rainChance) { - if (rainChance <= 0) return WEATHER_IMPACT.RAIN_IMPACT.NONE; - if (rainChance <= 30) return WEATHER_IMPACT.RAIN_IMPACT.LIGHT; - if (rainChance <= 60) return WEATHER_IMPACT.RAIN_IMPACT.MODERATE; - return WEATHER_IMPACT.RAIN_IMPACT.HEAVY; +function getRainImpact(description) { + if (!description) return WEATHER_IMPACT.RAIN_IMPACT.NONE; + + const desc = description.toLowerCase(); + if (desc.includes('rain') || desc.includes('shower')) { + if (desc.includes('heavy') || desc.includes('thunderstorm')) { + return WEATHER_IMPACT.RAIN_IMPACT.HEAVY; + } + return WEATHER_IMPACT.RAIN_IMPACT.LIGHT; + } + if (desc.includes('snow') || desc.includes('thunderstorm')) { + return WEATHER_IMPACT.RAIN_IMPACT.HEAVY; + } + return WEATHER_IMPACT.RAIN_IMPACT.NONE; } -async function calculatePrediction(weatherData, historicalData) { - // Validate historical data - if (!historicalData || historicalData.length === 0) { - throw new Error('No historical data available for predictions'); +async function ensureWeatherData(db) { + console.log('Checking for weather data...'); + + let weatherData = await db.collection('weather_data') + .findOne({}, { sort: { timestamp: -1 } }); + + console.log('Weather data found:', !!weatherData); + + if (!weatherData || !weatherData.forecasts) { + console.log('No weather data found, attempting to fetch...'); + + try { + const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; + const response = await fetch(`${baseUrl}/api/weather?refresh=true`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + console.log('Weather API call successful, checking database again...'); + weatherData = await db.collection('weather_data') + .findOne({}, { sort: { timestamp: -1 } }); + } + } catch (error) { + console.error('Failed to fetch weather data internally:', error); + } } - // Filter out invalid data points and calculate average - const validHistoricalData = historicalData.filter(data => - data && - data.data && - typeof data.data.pv_input_power === 'number' && - !isNaN(data.data.pv_input_power) - ); + return weatherData; +} - if (validHistoricalData.length === 0) { - throw new Error('No valid historical power data available'); +async function calculateSimplePrediction(weatherData, historicalData) { + console.log('Calculating predictions with weather data:', weatherData?.length, 'historical data:', historicalData?.length); + + let basePower = 800; + + if (historicalData && historicalData.length > 0) { + const validData = historicalData.filter(d => + d && d.data && typeof d.data.pv_input_power === 'number' && d.data.pv_input_power > 0 + ); + + if (validData.length > 0) { + const avgPower = validData.reduce((sum, d) => sum + d.data.pv_input_power, 0) / validData.length; + basePower = Math.max(200, avgPower); + console.log('Calculated base power from historical data:', basePower); + } } - // Calculate base power from historical data - const historicalAvg = validHistoricalData.reduce((sum, data) => sum + data.data.pv_input_power, 0) / validHistoricalData.length; - const basePower = Math.max(200, historicalAvg); // Minimum 200W baseline + if (!weatherData || !Array.isArray(weatherData)) { + console.error('Invalid weather data:', weatherData); + throw new Error('Invalid weather data provided'); + } - // Calculate predictions for each day - const predictions = weatherData.forecast.forecastday.map(day => { - // Ensure we have the required data - if (!day || !day.day) { - throw new Error('Invalid weather data structure'); + const predictions = weatherData.slice(0, 7).map((dayWeather, index) => { + if (!dayWeather) { + throw new Error(`Weather data missing for day ${index}`); } - const cloudCover = day.day.cloud || 0; - const rainChance = day.day.daily_chance_of_rain || 0; - const avgTemp = ((day.day.maxtemp_c || 0) + (day.day.mintemp_c || 0)) / 2; - - // Validate weather data - if (typeof cloudCover !== 'number' || typeof rainChance !== 'number' || typeof avgTemp !== 'number') { - console.error('Weather data validation failed:', { cloudCover, rainChance, avgTemp }); - throw new Error('Invalid weather data received'); - } - - // Calculate weather impact factors - const cloudImpact = getCloudImpact(cloudCover); + const avgTemp = dayWeather.temp_max && dayWeather.temp_min + ? (dayWeather.temp_max + dayWeather.temp_min) / 2 + : 20; // Default temp + + const cloudImpact = getCloudImpact(dayWeather.description); const tempImpact = getTempImpact(avgTemp); - const rainImpact = getRainImpact(rainChance); - - // Calculate final prediction with all factors + const rainImpact = getRainImpact(dayWeather.description); + const weatherFactor = cloudImpact * tempImpact * rainImpact; const predictedPower = Math.round(basePower * weatherFactor); - - // Calculate confidence based on weather stability - const weatherStability = 1 - (Math.abs(cloudImpact - 1) + Math.abs(tempImpact - 1) + Math.abs(rainImpact - 1)) / 3; - const confidence = 0.7 + (weatherStability * 0.3); - + + const daysFuture = index + 1; + const baseConfidence = 0.9 - (daysFuture * 0.1); + const weatherConfidence = (cloudImpact + tempImpact + rainImpact) / 3; + const confidence = Math.max(0.3, baseConfidence * weatherConfidence); + return { - date: new Date(day.date), + date: dayWeather.date ? new Date(dayWeather.date) : new Date(Date.now() + index * 24 * 60 * 60 * 1000), predictedPower, - weatherData: day, + weatherData: dayWeather, confidence: Math.round(confidence * 100) / 100, factors: { cloudImpact, tempImpact, - rainImpact + rainImpact, + weatherFactor } }; }); - + + console.log('Generated predictions:', predictions.length); return predictions; } -export async function GET() { +export async function GET(request) { + console.log('GET /api/predictions called'); + try { const { db } = await connectToDatabase(); - const now = new Date(); - - // Check for cached predictions - const cachedPredictions = await db.collection('predictions') - .find({ - date: { $gte: now }, - lastUpdated: { $gte: new Date(now.getTime() - CACHE_DURATION) } - }) - .sort({ date: 1 }) - .toArray(); - - if (cachedPredictions.length === 7) { - return NextResponse.json(cachedPredictions); - } - - // Fetch fresh weather data - const weatherData = await fetchWeatherData(); - - // Get historical power data for the last 30 days - const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - const historicalData = await db.collection('solar_data') - .find({ timestamp: { $gte: thirtyDaysAgo } }) - .toArray(); - - // Calculate new predictions - const predictions = await calculatePrediction(weatherData, historicalData); - - // Store predictions in database - const bulkOps = predictions.map(prediction => ({ - updateOne: { - filter: { date: prediction.date }, - update: { $set: prediction }, - upsert: true + console.log('Database connected successfully'); + + const { searchParams } = new URL(request.url); + const forceRefresh = searchParams.get('refresh') === 'true'; + console.log('Force refresh:', forceRefresh); + + if (!forceRefresh) { + console.log('Checking for cached predictions...'); + const existingPredictions = await db.collection('predictions') + .find({}) + .sort({ date: 1 }) + .toArray(); + + console.log('Found cached predictions:', existingPredictions.length); + + if (existingPredictions.length > 0) { + const sixHoursAgo = new Date(Date.now() - 6 * 60 * 60 * 1000); + const latestPrediction = existingPredictions[0]; + + if (latestPrediction.lastUpdated && new Date(latestPrediction.lastUpdated) > sixHoursAgo) { + return NextResponse.json({ + predictions: existingPredictions, + fromCache: true, + lastUpdated: latestPrediction.lastUpdated + }); + } else { + console.log('Cached predictions are stale, will refresh'); + } } - })); - - if (bulkOps.length > 0) { - await db.collection('predictions').bulkWrite(bulkOps); } - - return NextResponse.json(predictions); + + const weatherData = await ensureWeatherData(db); + + if (!weatherData || !weatherData.forecasts) { + console.error('No weather data available after attempting to fetch'); + return NextResponse.json( + { + error: 'No weather data available. Please check your weather API configuration.', + suggestion: 'Ensure WEATHER_API_KEY is set and the weather API is accessible.' + }, + { status: 400 } + ); + } + + console.log('Weather data available with forecasts:', weatherData.forecasts.length); + + console.log('Fetching historical solar data...'); + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + const historicalData = await db.collection('solar_data') + .find({ timestamp: { $gte: thirtyDaysAgo } }) + .toArray(); + + console.log('Historical data found:', historicalData.length, 'records'); + + console.log('Calculating new predictions...'); + const predictions = await calculateSimplePrediction(weatherData.forecasts, historicalData); + + const now = new Date(); + const predictionsWithTimestamp = predictions.map(pred => ({ + ...pred, + lastUpdated: now + })); + + console.log('Storing predictions in database...'); + await db.collection('predictions').deleteMany({}); + if (predictionsWithTimestamp.length > 0) { + await db.collection('predictions').insertMany(predictionsWithTimestamp); + } + + console.log('Predictions stored successfully'); + + return NextResponse.json({ + predictions: predictionsWithTimestamp, + fromCache: false, + lastUpdated: now + }); + } catch (error) { - console.error('Prediction error:', error); + console.error('Prediction API error:', error); return NextResponse.json( - { error: error.message || 'Failed to generate predictions' }, - { status: 500 } + { + error: error.message || 'Failed to generate predictions', + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, + timestamp: new Date().toISOString() + }, + { status: 500 } ); } -} \ No newline at end of file +} + +export async function POST(request) { + console.log('POST /api/predictions called - forcing refresh'); + return GET(new Request(request.url + '?refresh=true')); +} \ No newline at end of file diff --git a/src/app/api/solar-data/route.js b/src/app/api/solar-data/route.js index 44636d1..4e34c2e 100644 --- a/src/app/api/solar-data/route.js +++ b/src/app/api/solar-data/route.js @@ -14,7 +14,6 @@ export async function GET(request) { let startDate = new Date() let endDate = new Date() - // If since parameter is provided, use it as the start date if (since) { startDate = new Date(since) } else { diff --git a/src/app/api/weather/route.js b/src/app/api/weather/route.js index 50d4ce7..b2375dc 100644 --- a/src/app/api/weather/route.js +++ b/src/app/api/weather/route.js @@ -8,67 +8,145 @@ const COORDS = { } async function fetchWeatherFromAPI() { - const response = await fetch( - `http://api.weatherapi.com/v1/forecast.json?key=${process.env.WEATHER_API_KEY}&q=${COORDS.lat},${COORDS.lon}&days=7&aqi=no` - ) - - if (!response.ok) { - throw new Error('Failed to fetch weather data') + console.log('Fetching weather from WeatherAPI...') + + if (!process.env.WEATHER_API_KEY) { + throw new Error('WEATHER_API_KEY environment variable is not set') } - - return response.json() + + const response = await fetch( + `http://api.weatherapi.com/v1/forecast.json?key=${process.env.WEATHER_API_KEY}&q=${COORDS.lat},${COORDS.lon}&days=7&aqi=no`, + { + headers: { + 'User-Agent': 'SolarPrediction/1.0' + } + } + ) + + if (!response.ok) { + const errorText = await response.text() + console.error('WeatherAPI Error:', response.status, errorText) + throw new Error(`Weather API failed: ${response.status} - ${errorText}`) + } + + const data = await response.json() + console.log('Weather data fetched successfully:', !!data.forecast?.forecastday) + + return data } -export async function GET() { - try { - // Ensure database connection - const { db } = await connectToDatabase() +function convertToInternalFormat(weatherApiData) { + if (!weatherApiData.forecast?.forecastday) { + throw new Error('Invalid weather data structure from WeatherAPI') + } + + return weatherApiData.forecast.forecastday.map(day => ({ + date: day.date, + temp_min: day.day.mintemp_c, + temp_max: day.day.maxtemp_c, + humidity: day.day.avghumidity, + wind_speed: day.day.maxwind_kph / 3.6, + icon: day.day.condition.icon, + description: day.day.condition.text.toLowerCase(), + timestamp: new Date(day.date) + })) +} + +export async function GET(request) { + console.log('Weather API GET called') + + try { + const { searchParams } = new URL(request.url) + const forceRefresh = searchParams.get('refresh') === 'true' + + const { db } = await connectToDatabase() + console.log('Database connected successfully') - // Check if we have cached data const cachedWeather = await db.collection('weather').findOne({ 'location.lat': COORDS.lat, 'location.lon': COORDS.lon }) const now = new Date() - const isCacheValid = cachedWeather && - (now - new Date(cachedWeather.lastUpdated)) < CACHE_DURATION + const isCacheValid = cachedWeather && + (now - new Date(cachedWeather.lastUpdated)) < CACHE_DURATION - // If cache is valid, return cached data - if (isCacheValid) { + console.log('Cache status:', { + hasCached: !!cachedWeather, + isCacheValid, + forceRefresh + }) + + if (isCacheValid && !forceRefresh) { + console.log('Returning cached weather data') return NextResponse.json(cachedWeather) } - // If no cache or cache is invalid, fetch fresh data + console.log('Fetching fresh weather data...') const weatherData = await fetchWeatherFromAPI() - - // Add metadata to the weather data + const weatherWithMetadata = { ...weatherData, location: COORDS, lastUpdated: now } - // Update or create weather document await db.collection('weather').updateOne( - { - 'location.lat': COORDS.lat, - 'location.lon': COORDS.lon - }, - { $set: weatherWithMetadata }, - { upsert: true } + { + 'location.lat': COORDS.lat, + 'location.lon': COORDS.lon + }, + { $set: weatherWithMetadata }, + { upsert: true } ) + const internalFormatData = convertToInternalFormat(weatherData) + + await db.collection('weather_data').updateOne( + { type: 'forecast' }, + { + $set: { + forecasts: internalFormatData, + timestamp: now, + source: 'weatherapi' + } + }, + { upsert: true } + ) + + console.log('Weather data stored in both formats successfully') + return NextResponse.json(weatherWithMetadata) + } catch (error) { console.error('Weather API Error:', error) - // If we have cached data but failed to fetch new data, return cached data - if (cachedWeather) { - return NextResponse.json(cachedWeather) + + try { + const { db } = await connectToDatabase() + const cachedWeather = await db.collection('weather').findOne({ + 'location.lat': COORDS.lat, + 'location.lon': COORDS.lon + }) + + if (cachedWeather) { + console.log('Returning stale cached data due to API error') + return NextResponse.json({ + ...cachedWeather, + isStale: true, + error: 'Using cached data due to API error' + }) + } + } catch (dbError) { + console.error('Database error while fetching cached data:', dbError) } + return NextResponse.json( - { error: 'Failed to fetch weather data' }, - { status: 500 } + { + error: 'Failed to fetch weather data', + details: error.message, + timestamp: new Date().toISOString() + }, + { status: 500 } ) } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/app/page.js b/src/app/page.js index 9ebd5f6..da079db 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -6,9 +6,7 @@ import { BatteryStatus } from '@/components/BatteryStatus' import { PowerStats } from '@/components/PowerStats' import { SystemStatus } from '@/components/SystemStatus' import { SolarChart } from '@/components/SolarChart' -import { LastRefresh } from '@/components/LastRefresh' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { motion, AnimatePresence } from 'framer-motion' +import { motion } from 'framer-motion' import { Navbar } from '@/components/Navbar' import WeatherForecast from '@/components/WeatherForecast' import PowerPredictionGraph from '@/components/PowerPredictionGraph' @@ -32,7 +30,6 @@ export default function Dashboard() { if (newData.length > 0) { setCurrentData(prevData => { - // Merge new data with existing data, avoiding duplicates const mergedData = [...prevData] newData.forEach(newItem => { const newTimestamp = newItem.timestamp.$date || newItem.timestamp @@ -48,11 +45,9 @@ export default function Dashboard() { return mergedData }) - // Update last update time const latestTimestamp = newData[newData.length - 1].timestamp.$date || newData[newData.length - 1].timestamp setLastUpdateTime(latestTimestamp) - // Update chart data if viewing today's data if (chartTimeRange === 'today') { setData(prevData => { const mergedData = [...prevData] @@ -78,7 +73,7 @@ export default function Dashboard() { const fetchHistoricalData = async () => { try { - setLastUpdateTime(null) // Reset last update time when changing time range + setLastUpdateTime(null); const response = await fetch(`/api/solar-data?timeRange=${chartTimeRange}`) const newData = await response.json() setData(newData) @@ -90,7 +85,6 @@ export default function Dashboard() { } useEffect(() => { - // Set minimum loading time const minLoadingTimer = setTimeout(() => { setMinLoadingComplete(true) }, 500) diff --git a/src/components/PowerPredictionGraph.js b/src/components/PowerPredictionGraph.js index f148889..82fa9fd 100644 --- a/src/components/PowerPredictionGraph.js +++ b/src/components/PowerPredictionGraph.js @@ -13,121 +13,310 @@ import { Legend } from 'recharts'; import { motion } from 'framer-motion'; +import { RefreshCw, Clock, Database, AlertCircle } from 'lucide-react'; export default function PowerPredictionGraph() { const [predictions, setPredictions] = useState(null); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); + const [lastUpdated, setLastUpdated] = useState(null); + const [fromCache, setFromCache] = useState(false); + const [debugInfo, setDebugInfo] = useState(''); + + const fetchPredictions = async (forceRefresh = false) => { + const debugLog = []; + + try { + debugLog.push(`Starting fetch - forceRefresh: ${forceRefresh}`); + + if (forceRefresh) { + setRefreshing(true); + debugLog.push('Setting refreshing state to true'); + } else { + setLoading(true); + debugLog.push('Setting loading state to true'); + } + + const url = forceRefresh ? '/api/predictions?refresh=true' : '/api/predictions'; + debugLog.push(`Fetching from URL: ${url}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + debugLog.push(`Response status: ${response.status}`); + debugLog.push(`Response ok: ${response.ok}`); + + if (!response.ok) { + const errorText = await response.text(); + debugLog.push(`Error response text: ${errorText}`); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const data = await response.json(); + debugLog.push(`Response data received - predictions: ${data.predictions?.length}, fromCache: ${data.fromCache}`); + + if (!data.predictions || !Array.isArray(data.predictions)) { + debugLog.push('Invalid data structure received'); + throw new Error('Invalid response data structure'); + } + + setPredictions(data.predictions); + setLastUpdated(new Date(data.lastUpdated)); + setFromCache(data.fromCache); + setError(null); + debugLog.push('State updated successfully'); + + } catch (err) { + debugLog.push(`Error caught: ${err.message}`); + console.error('Fetch error:', err); + setError(err.message); + } finally { + setLoading(false); + setRefreshing(false); + setDebugInfo(debugLog.join('\n')); + debugLog.push('Loading states reset'); + } + }; useEffect(() => { - const fetchPredictions = async () => { - try { - const response = await fetch('/api/predictions'); - if (!response.ok) throw new Error('Failed to fetch predictions'); - const data = await response.json(); - setPredictions(data); - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } - }; - + console.log('Component mounted, fetching initial predictions'); fetchPredictions(); }, []); + const handleRefresh = () => { + console.log('Refresh button clicked'); + fetchPredictions(true); + }; + + const formatLastUpdated = (date) => { + if (!date) return 'Unknown'; + + const now = new Date(); + const diffMs = now - date; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins} min ago`; + if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; + return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; + }; + if (loading) { return ( - - - - - - - - - - + + + + + + + + + + ); } if (error) { return ( - - - Error loading predictions - - + + + + + Error loading predictions + {error} + + + {refreshing ? 'Retrying...' : 'Try Again'} + + + {/* Debug information */} + {process.env.NODE_ENV === 'development' && debugInfo && ( + + Debug Info + + {debugInfo} + + + )} + + ); } if (!predictions?.length) { - return null; + return ( + + + + + No prediction data available + Click generate to create predictions + + + {refreshing ? 'Generating...' : 'Generate Predictions'} + + + + ); } const chartData = predictions.map(pred => ({ - date: new Date(pred.date).toLocaleDateString('en-US', { weekday: 'short' }), + date: new Date(pred.date).toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric' + }), predicted: pred.predictedPower, confidence: pred.confidence * 100 })); return ( - - 7-Day Power Generation Forecast - - - - - - - - - - - - - - + + {/* Header with title and refresh button */} + + 7-Day Power Generation Forecast + + {/* Cache indicator */} + + {fromCache ? ( + <> + + Cached + > + ) : ( + <> + + Fresh + > + )} + + + {/* Last updated */} + + + {formatLastUpdated(lastUpdated)} + + + {/* Refresh button */} + + + {refreshing ? 'Updating...' : 'Refresh'} + + + + + + + + + + + [ + `${value}${name === 'Predicted Power' ? 'W' : '%'}`, + name === 'Predicted Power' ? 'Predicted Power' : 'Confidence' + ]} + /> + + + + + + + + + + + Avg. Predicted + + {Math.round(predictions.reduce((sum, p) => sum + p.predictedPower, 0) / predictions.length)}W + + + + Best Day + + {Math.max(...predictions.map(p => p.predictedPower))}W + + + + Worst Day + + {Math.min(...predictions.map(p => p.predictedPower))}W + + + + Avg. Confidence + + {Math.round(predictions.reduce((sum, p) => sum + p.confidence, 0) / predictions.length * 100)}% + + + + + + {process.env.NODE_ENV === 'development' && ( + + Predictions: {predictions.length} | From Cache: {fromCache ? 'Yes' : 'No'} + + )} + ); -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/SolarChart.js b/src/components/SolarChart.js index 85294c8..a15b0b5 100644 --- a/src/components/SolarChart.js +++ b/src/components/SolarChart.js @@ -47,11 +47,9 @@ export function SolarChart({ data, type, timeRange, onTimeRangeChange }) { const formatData = (data) => { return getFilteredData(data).map(item => { - // Handle both MongoDB date format and regular date string const dateStr = item.timestamp.$date || item.timestamp const date = new Date(dateStr) - // Check if date is valid if (isNaN(date.getTime())) { console.error('Invalid date:', dateStr) return null diff --git a/src/lib/models/prediction.js b/src/lib/models/prediction.js index c160b82..33e1ede 100644 --- a/src/lib/models/prediction.js +++ b/src/lib/models/prediction.js @@ -8,23 +8,70 @@ const predictionSchema = new mongoose.Schema({ }, predictedPower: { type: Number, - required: true + required: true, + min: 0 }, weatherData: { - type: Object, - required: true + date: String, + temp_min: Number, + temp_max: Number, + humidity: Number, + wind_speed: Number, + icon: String, + description: String, + timestamp: Date }, confidence: { type: Number, - required: true + required: true, + min: 0, + max: 1 + }, + factors: { + cloudImpact: { + type: Number, + required: true, + min: 0, + max: 1 + }, + tempImpact: { + type: Number, + required: true, + min: 0, + max: 1 + }, + rainImpact: { + type: Number, + required: true, + min: 0, + max: 1 + }, + weatherFactor: { + type: Number, + required: true, + min: 0, + max: 1 + } }, lastUpdated: { type: Date, - default: Date.now + default: Date.now, + index: true } +}, { + timestamps: true }); -// Create compound index for efficient querying -predictionSchema.index({ date: 1, lastUpdated: 1 }); +predictionSchema.index({ date: 1, lastUpdated: -1 }); -export const Prediction = mongoose.models.Prediction || mongoose.model('Prediction', predictionSchema); \ No newline at end of file +predictionSchema.methods.isStale = function() { + const sixHoursAgo = new Date(Date.now() - 6 * 60 * 60 * 1000); + return this.lastUpdated < sixHoursAgo; +}; + +predictionSchema.statics.getFreshPredictions = function() { + const sixHoursAgo = new Date(Date.now() - 6 * 60 * 60 * 1000); + return this.find({ lastUpdated: { $gte: sixHoursAgo } }).sort({ date: 1 }); +}; + +export const Prediction = mongoose.models.Prediction || mongoose.model('Prediction', predictionSchema); \ No newline at end of file diff --git a/src/lib/mongodb.js b/src/lib/mongodb.js index ca93436..1f488b1 100644 --- a/src/lib/mongodb.js +++ b/src/lib/mongodb.js @@ -11,15 +11,12 @@ let client let clientPromise if (process.env.NODE_ENV === 'development') { - // In development mode, use a global variable so that the value - // is preserved across module reloads caused by HMR (Hot Module Replacement). if (!global._mongoClientPromise) { client = new MongoClient(uri, options) global._mongoClientPromise = client.connect() } clientPromise = global._mongoClientPromise } else { - // In production mode, it's best to not use a global variable. client = new MongoClient(uri, options) clientPromise = client.connect() } diff --git a/src/lib/weather.js b/src/lib/weather.js index 8ed1191..9f6a514 100644 --- a/src/lib/weather.js +++ b/src/lib/weather.js @@ -32,7 +32,6 @@ export async function fetchWeatherData() { ) const data = await response.json() - // Process the data to get daily forecasts const dailyForecasts = data.list.reduce((acc, item) => { const date = new Date(item.dt * 1000) const day = date.toISOString().split('T')[0] @@ -67,10 +66,8 @@ export async function getWeatherData() { const { db } = await connectToDatabase() const collection = db.collection('weather_data') - // Get the latest weather data const latestData = await collection.findOne({}, { sort: { timestamp: -1 } }) - // If no data exists or data is older than 2 hours, fetch new data if (!latestData || (Date.now() - latestData.timestamp.getTime() > 2 * 60 * 60 * 1000)) { const newData = await fetchWeatherData() await collection.insertOne({
Error loading predictions
{error}
+ {debugInfo} +
No prediction data available
Click generate to create predictions