const express = require('express'); const cors = require('cors'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { spawn } = require('child_process'); const sqlite3 = require('sqlite3').verbose(); const sharp = require('sharp'); const { v4: uuidv4 } = require('uuid'); require('dotenv').config(); const app = express(); // cPanel and other hosts use the PORT environment variable const PORT = process.env.PORT || 5000; // --- Middleware --- // Enable CORS for all routes app.use(cors()); // Parse JSON bodies for API requests app.use(express.json()); app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // Ensure directories exist const dirs = ['uploads', 'temp', 'results']; // Removed 'database' to prevent permission errors dirs.forEach(dir => { const dirPath = path.join(__dirname, dir); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); // Still need temp dirs for uploads } }); // --- DATABASE DISABLED --- // The database is temporarily disabled for simplified deployment on Hugging Face. // All database operations will be skipped, and the app will run without persistence. console.log('DATABASE: Temporarily disabled for deployment. No data will be saved or retrieved.'); const db = null; // --- UNIFIED MULTER CONFIG --- // A single configuration for handling all file uploads to a temporary directory. const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, path.join(__dirname, 'temp/')); }, filename: (req, file, cb) => { const uniqueName = `${file.fieldname}-${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`; cb(null, uniqueName); } }); const upload = multer({ storage: storage, limits: { fileSize: 50 * 1024 * 1024 }, // 50MB general limit }); // --- HELPER FUNCTIONS --- function fallbackAnalysis(imagePath, waterSource) { console.log('--- Executing Node.js Fallback Analysis ---'); // This simulates the Python script's output when it's not available. return { ph: 7.1, chlorine: 0.5, nitrates: 5, hardness: 150, alkalinity: 120, bacteria: 0, confidence: 40, // Lower confidence to indicate it's a fallback processingMethod: "Node.js Fallback" }; } // Refactored helper to call any Python script, handle JSON, and manage errors. function callPythonScript(scriptName, args = [], inputData = null) { return new Promise((resolve, reject) => { const scriptPath = path.join(__dirname, 'python', scriptName); const pythonProcess = spawn('python3', [scriptPath, ...args]); let dataString = ''; let errorString = ''; pythonProcess.stdout.on('data', (data) => { dataString += data.toString(); }); pythonProcess.stderr.on('data', (data) => { errorString += data.toString(); }); if (inputData) { pythonProcess.stdin.write(JSON.stringify(inputData)); pythonProcess.stdin.end(); } pythonProcess.on('close', (code) => { if (code === 0) { try { resolve(JSON.parse(dataString)); } catch (error) { console.error(`Error parsing JSON from ${scriptName}:`, dataString); reject(new Error(`Failed to parse Python output from ${scriptName}`)); } } else { console.error(`Error in ${scriptName} (code ${code}): ${errorString}`); reject(new Error(errorString || `Python script ${scriptName} exited with code ${code}`)); } }); }); } // --- API ROUTES --- // --- HEALTH CHECK ENDPOINT --- // This is crucial for platforms like Hugging Face to know the app is ready. app.get('/api/health', (req, res) => { res.status(200).json({ status: 'ok', message: 'Backend is running' }); }); // --- BIODIVERSITYEAR AUDIO ANALYSIS ENDPOINT --- app.post('/api/analyze-audio', upload.single('audioFile'), async (req, res) => { if (!req.file) return res.status(400).json({ message: 'Audio file is missing.' }); try { console.log('▶️ Calling Python AI script for audio analysis...'); const result = await callPythonScript('audio_analyzer.py', [req.file.path]); res.status(200).json(result); } catch (error) { res.status(500).json({ message: 'Error during audio analysis.', error: error.message }); } finally { // Clean up the temporary file fs.unlink(req.file.path, (err) => { if (err) console.error(`- Error deleting temp audio file: ${err.message}`); }); } }); // --- BIO-STREAM AI DNA ANALYSIS ENDPOINT --- app.post('/api/analyze-dna', upload.single('dnaFile'), async (req, res) => { if (!req.file) return res.status(400).json({ message: 'DNA file is missing.' }); try { console.log('▶️ Calling Python AI script for DNA analysis...'); const result = await callPythonScript('dna_analyzer.py', [req.file.path]); res.status(200).json(result); } catch (error) { res.status(500).json({ message: 'Error during DNA analysis.', error: error.message }); } finally { fs.unlink(req.file.path, (err) => { if (err) console.error(`- Error deleting temp DNA file: ${err.message}`); }); } }); // --- JSON-based Analysis Endpoints (Footprint, E-Waste) --- async function handleJsonAnalysis(req, res, scriptName, logMessage) { console.log(`✅ Backend: Received ${logMessage} request.`); try { const result = await callPythonScript(scriptName, [], req.body); res.status(200).json(result); } catch (error) { res.status(500).json({ message: `Error during ${logMessage} analysis.`, error: error.message }); } } app.post('/api/analyze-footprint', (req, res) => handleJsonAnalysis(req, res, 'phantom_footprint_analyzer.py', 'Phantom Footprint')); app.post('/api/analyze-ewaste', (req, res) => handleJsonAnalysis(req, res, 'ewaste_analyzer.py', 'E-Waste')); // --- AQUALENS WATER ANALYSIS ENDPOINT --- app.post('/api/analyze-water', upload.single('image'), async (req, res) => { const startTime = Date.now(); try { if (!req.file) { return res.status(400).json({ error: 'No image file provided' }); } const { waterSource, latitude, longitude, userId } = req.body; const imagePath = req.file.path; const testId = uuidv4(); console.log(`🧪 Starting water analysis for test ${testId}`); console.log(`📍 Location: ${latitude}, ${longitude}`); console.log(`🚰 Water Source: ${waterSource}`); // Step 1: Image preprocessing with Sharp (Node.js) const preprocessedPath = path.join(__dirname, 'temp', `preprocessed_${testId}.jpg`); await sharp(imagePath) .resize(800, 600, { fit: 'inside' }) .normalize() .sharpen() .jpeg({ quality: 95 }) .toFile(preprocessedPath); console.log('✅ Image preprocessing completed'); // Step 2: AI analysis with Python let analysisResult; try { analysisResult = await callPythonScript('water_analysis.py', [preprocessedPath, waterSource || 'unknown']); console.log('✅ AI analysis completed'); } catch (error) { // If the python script fails, use the fallback. console.log(`Python script for water analysis failed, using fallback. Error: ${error.message}`); analysisResult = fallbackAnalysis(preprocessedPath, waterSource); } const processingTime = (Date.now() - startTime) / 1000; // Step 4: Determine overall quality and safety const { ph, chlorine, nitrates, hardness, alkalinity, bacteria } = analysisResult; let overallQuality = 'Excellent'; let safetyLevel = 'Safe'; let alerts = []; let recommendations = []; if (ph < 6.5 || ph > 8.5) { alerts.push(`pH levels outside safe range: ${ph.toFixed(1)}`); recommendations.push('Consider pH adjustment or filtration system'); if (ph < 5.0 || ph > 9.5) { safetyLevel = 'Unsafe'; overallQuality = 'Poor'; recommendations.push('Contact water authority immediately - pH outside safe drinking range'); } else { overallQuality = 'Good'; } } if (chlorine > 4.0) { alerts.push(`High chlorine levels: ${chlorine.toFixed(1)} ppm`); recommendations.push('Let water sit uncovered for 30 minutes before drinking'); safetyLevel = 'Unsafe'; overallQuality = 'Poor'; } else if (chlorine < 0.2 && waterSource === 'Tap Water') { alerts.push('Low chlorine in tap water may indicate contamination risk'); recommendations.push('Consider boiling water or using filtration'); } if (nitrates > 10) { alerts.push(`Elevated nitrate levels: ${nitrates} ppm`); recommendations.push('Consider reverse osmosis filtration or alternative water source'); if (nitrates > 50) { safetyLevel = 'Unsafe'; overallQuality = 'Poor'; recommendations.push('DO NOT DRINK - Especially dangerous for infants and pregnant women'); } else { overallQuality = 'Fair'; } } if (bacteria > 0) { alerts.push('Bacterial contamination detected'); recommendations.push('Boil water for 1 minute before drinking or use alternative source'); safetyLevel = 'Unsafe'; overallQuality = 'Poor'; } if (hardness > 180) { alerts.push(`Very hard water: ${hardness} ppm`); recommendations.push('Consider water softener to protect plumbing and improve taste'); if (overallQuality === 'Excellent') overallQuality = 'Good'; } // Step 5: Save to database if (db) { // Database logic would go here. } else { console.log('DATABASE: Skipping save operation for water analysis results.'); } setTimeout(() => { [preprocessedPath, imagePath].forEach(p => { if (fs.existsSync(p) && p !== imagePath) { fs.unlinkSync(p); } }); }, 5000); console.log(`✅ Analysis completed in ${processingTime.toFixed(2)}s`); res.json({ success: true, testId: testId, results: { ph: parseFloat(ph.toFixed(1)), chlorine: parseFloat(chlorine.toFixed(1)), nitrates: Math.round(nitrates), hardness: Math.round(hardness), alkalinity: Math.round(alkalinity), bacteria: bacteria }, overallQuality: overallQuality, safetyLevel: safetyLevel, alerts: alerts, recommendations: recommendations, confidence: Math.round(analysisResult.confidence || 95), processingTime: parseFloat(processingTime.toFixed(2)), colorAccuracy: analysisResult.colorAccuracy || '94%', processingMethod: analysisResult.processingMethod || 'AI Analysis', colorChannels: analysisResult.colorChannels || {}, timestamp: new Date().toISOString(), location: { latitude: parseFloat(latitude) || null, longitude: parseFloat(longitude) || null } }); } catch (error) { console.error('❌ Analysis error:', error); res.status(500).json({ error: 'Analysis failed', details: error.message, timestamp: new Date().toISOString() }); } finally { if (req.file) fs.unlink(req.file.path, (err) => { if (err) console.error(`- Error deleting temp image file: ${err.message}`); }); } }); // Get water quality map data app.get('/api/water-map', (req, res) => { // Since the database is disabled, return an empty array for map data. console.log('DATABASE: /api/water-map called, returning empty dataset as DB is disabled.'); res.json({ success: true, data: [], count: 0 }); }); // --- STATIC FILE SERVING FOR PRODUCTION --- // This is the crucial part for serving your React app in production. // It serves the built React app from the 'web/build' folder. const buildPath = path.join(__dirname, '..', 'web', 'build'); app.use(express.static(buildPath)); // The "catchall" handler: for any request that doesn't match an API route or a static file, // send back the main index.html file. This allows React Router to handle client-side routing. app.get('*', (req, res) => { res.sendFile(path.join(buildPath, 'index.html')); }); // --- Start Server --- app.listen(PORT, () => { console.log(`🌿 GreenPlus by GXS Main Backend Server running on port ${PORT}`); console.log(`🚀 Serving production React app from: ${buildPath}`); });