gaialive's picture
Update backend/server.js
e9fa848 verified
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}`);
});