gaialive commited on
Commit
7c97d6f
Β·
verified Β·
1 Parent(s): e46cfca

Update backend/server.js

Browse files
Files changed (1) hide show
  1. backend/server.js +538 -340
backend/server.js CHANGED
@@ -1,340 +1,538 @@
1
- const express = require('express');
2
- const cors = require('cors');
3
- const multer = require('multer');
4
- const path = require('path');
5
- const fs = require('fs');
6
- const { spawn } = require('child_process');
7
- const sqlite3 = require('sqlite3').verbose();
8
- const sharp = require('sharp');
9
- const { v4: uuidv4 } = require('uuid');
10
- require('dotenv').config();
11
-
12
- const app = express();
13
- // Hugging Face and other hosts provide the PORT environment variable. Default to 5000 for local dev.
14
- const PORT = process.env.PORT || 5000;
15
-
16
- // --- Middleware ---
17
- app.use(cors());
18
- app.use(express.json());
19
- app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
20
-
21
- // --- Directory Setup ---
22
- // Create ephemeral directories for temporary file uploads and processing.
23
- // The Dockerfile ensures these exist and have the correct permissions.
24
- const ephemeralDirs = ['uploads', 'temp', 'results'];
25
- ephemeralDirs.forEach(dir => {
26
- const dirPath = path.join(__dirname, dir);
27
- if (!fs.existsSync(dirPath)) {
28
- fs.mkdirSync(dirPath, { recursive: true });
29
- }
30
- });
31
-
32
- // --- In-Memory Database Setup ---
33
- // For environments without persistent storage, use an in-memory database.
34
- // Data will be lost on restart, which is acceptable for a demo/prototype.
35
- const db = new sqlite3.Database(':memory:', (err) => {
36
- if (err) {
37
- console.error('Error opening in-memory database', err.message);
38
- } else {
39
- console.log('Connected to the SQLite in-memory database.');
40
- console.log('NOTE: Database is in-memory and will be reset on application restart.');
41
- }
42
- });
43
-
44
- db.serialize(() => {
45
- db.run(`CREATE TABLE IF NOT EXISTS water_tests (
46
- id TEXT PRIMARY KEY,
47
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
48
- user_id TEXT,
49
- latitude REAL,
50
- longitude REAL,
51
- water_source TEXT,
52
- image_path TEXT,
53
- ph REAL,
54
- chlorine REAL,
55
- nitrates REAL,
56
- hardness REAL,
57
- alkalinity REAL,
58
- bacteria INTEGER,
59
- overall_quality TEXT,
60
- safety_level TEXT,
61
- confidence REAL,
62
- processing_time REAL,
63
- alerts TEXT,
64
- color_analysis TEXT
65
- )`);
66
-
67
- db.run(`CREATE TABLE IF NOT EXISTS water_alerts (
68
- id TEXT PRIMARY KEY,
69
- test_id TEXT,
70
- alert_type TEXT,
71
- severity TEXT,
72
- message TEXT,
73
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
74
- latitude REAL,
75
- longitude REAL,
76
- FOREIGN KEY(test_id) REFERENCES water_tests(id)
77
- )`);
78
- });
79
-
80
-
81
- // --- MULTER CONFIG FOR FILE UPLOADS ---
82
- const storage = multer.diskStorage({
83
- destination: (req, file, cb) => {
84
- cb(null, path.join(__dirname, 'temp/'));
85
- },
86
- filename: (req, file, cb) => {
87
- const uniqueName = `${file.fieldname}-${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
88
- cb(null, uniqueName);
89
- }
90
- });
91
-
92
- const upload = multer({
93
- storage: storage,
94
- limits: { fileSize: 50 * 1024 * 1024 }, // 50MB general limit
95
- });
96
-
97
-
98
- // --- HELPER FUNCTIONS ---
99
- function callPythonScript(scriptName, ...args) {
100
- return new Promise((resolve, reject) => {
101
- const scriptPath = path.join(__dirname, 'python', scriptName);
102
- const pythonProcess = spawn('python3', [scriptPath, ...args]);
103
-
104
- let dataString = '';
105
- let errorString = '';
106
-
107
- pythonProcess.stdout.on('data', (data) => { dataString += data.toString(); });
108
- pythonProcess.stderr.on('data', (data) => { errorString += data.toString(); });
109
-
110
- pythonProcess.on('close', (code) => {
111
- if (code === 0) {
112
- try {
113
- resolve(JSON.parse(dataString));
114
- } catch (error) {
115
- console.error(`Error parsing JSON from ${scriptName}:`, dataString);
116
- reject(new Error(`Failed to parse Python output from ${scriptName}`));
117
- }
118
- } else {
119
- console.error(`Error in ${scriptName} (code ${code}): ${errorString}`);
120
- reject(new Error(errorString));
121
- }
122
- });
123
- });
124
- }
125
-
126
- // --- API ROUTES ---
127
-
128
- // Health check endpoint
129
- app.get('/api/health', (req, res) => {
130
- res.json({ status: 'ok', message: 'Backend is running' });
131
- });
132
-
133
- // --- BIODIVERSITYEAR AUDIO ANALYSIS ENDPOINT ---
134
- app.post('/api/analyze-audio', upload.single('audioFile'), async (req, res) => {
135
- if (!req.file) return res.status(400).json({ message: 'Audio file is missing.' });
136
-
137
- try {
138
- console.log('▢️ Calling Python AI script for audio analysis...');
139
- const result = await callPythonScript('audio_analyzer.py', req.file.path);
140
- res.status(200).json(result);
141
- } catch (error) {
142
- res.status(500).json({ message: 'Error during audio analysis.', error: error.message });
143
- } finally {
144
- fs.unlink(req.file.path, (err) => {
145
- if (err) console.error(`- Error deleting temp audio file: ${err.message}`);
146
- });
147
- }
148
- });
149
-
150
- // --- BIO-STREAM AI DNA ANALYSIS ENDPOINT ---
151
- app.post('/api/analyze-dna', upload.single('dnaFile'), async (req, res) => {
152
- if (!req.file) return res.status(400).json({ message: 'DNA file is missing.' });
153
-
154
- try {
155
- console.log('▢️ Calling Python AI script for DNA analysis...');
156
- const result = await callPythonScript('dna_analyzer.py', req.file.path);
157
- res.status(200).json(result);
158
- } catch (error) {
159
- res.status(500).json({ message: 'Error during DNA analysis.', error: error.message });
160
- } finally {
161
- fs.unlink(req.file.path, (err) => {
162
- if (err) console.error(`- Error deleting temp DNA file: ${err.message}`);
163
- });
164
- }
165
- });
166
-
167
- // --- PHANTOM FOOTPRINT, E-WASTE, AND OTHER JSON-BASED ENDPOINTS ---
168
- function createJsonAnalysisEndpoint(apiPath, scriptName, logMessage) {
169
- app.post(apiPath, (req, res) => {
170
- console.log(`βœ… Backend: Received ${logMessage} request.`);
171
- const pythonProcess = spawn('python3', [path.join(__dirname, 'python', scriptName)]);
172
-
173
- let analysisResult = '';
174
- let errorOutput = '';
175
-
176
- pythonProcess.stdin.write(JSON.stringify(req.body));
177
- pythonProcess.stdin.end();
178
-
179
- pythonProcess.stdout.on('data', (data) => { analysisResult += data.toString(); });
180
- pythonProcess.stderr.on('data', (data) => { errorOutput += data.toString(); });
181
-
182
- pythonProcess.on('close', (code) => {
183
- if (code === 0) {
184
- try {
185
- res.status(200).json(JSON.parse(analysisResult));
186
- } catch (e) {
187
- res.status(500).json({ message: `Failed to parse analysis result for ${logMessage}.` });
188
- }
189
- } else {
190
- res.status(500).json({ message: `Error during ${logMessage} analysis.`, error: errorOutput });
191
- }
192
- });
193
- });
194
- }
195
-
196
- createJsonAnalysisEndpoint('/api/analyze-footprint', 'phantom_footprint_analyzer.py', 'Phantom Footprint');
197
- createJsonAnalysisEndpoint('/api/analyze-ewaste', 'ewaste_analyzer.py', 'E-Waste');
198
-
199
- // --- AQUALENS WATER ANALYSIS ENDPOINT ---
200
- app.post('/api/analyze-water', upload.single('image'), async (req, res) => {
201
- const startTime = Date.now();
202
- if (!req.file) return res.status(400).json({ error: 'No image file provided' });
203
-
204
- const { waterSource, latitude, longitude, userId } = req.body;
205
- const imagePath = req.file.path;
206
- const testId = uuidv4();
207
-
208
- try {
209
- console.log(`πŸ§ͺ Starting water analysis for test ${testId}`);
210
-
211
- // Preprocess image with Sharp
212
- const preprocessedPath = path.join(__dirname, 'temp', `preprocessed_${testId}.jpg`);
213
- await sharp(imagePath)
214
- .resize(800, 600, { fit: 'inside' })
215
- .normalize()
216
- .toFile(preprocessedPath);
217
-
218
- // AI analysis with Python
219
- const analysisResult = await callPythonScript('water_analysis.py', preprocessedPath, waterSource || 'unknown');
220
- console.log('βœ… AI analysis completed');
221
-
222
- const processingTime = (Date.now() - startTime) / 1000;
223
-
224
- // Determine overall quality and safety
225
- const { ph, chlorine, nitrates, hardness, alkalinity, bacteria } = analysisResult;
226
- let overallQuality = 'Excellent';
227
- let safetyLevel = 'Safe';
228
- let alerts = [];
229
- let recommendations = [];
230
-
231
- if (ph < 6.5 || ph > 8.5) {
232
- alerts.push(`pH levels outside safe range: ${ph.toFixed(1)}`);
233
- safetyLevel = 'Caution';
234
- overallQuality = 'Good';
235
- }
236
- if (chlorine > 4.0) {
237
- alerts.push(`High chlorine levels: ${chlorine.toFixed(1)} ppm`);
238
- safetyLevel = 'Unsafe';
239
- overallQuality = 'Poor';
240
- }
241
- if (nitrates > 10) {
242
- alerts.push(`Elevated nitrate levels: ${nitrates} ppm`);
243
- safetyLevel = 'Unsafe';
244
- overallQuality = 'Poor';
245
- }
246
- if (bacteria > 0) {
247
- alerts.push('Bacterial contamination detected');
248
- safetyLevel = 'Unsafe';
249
- overallQuality = 'Poor';
250
- }
251
-
252
- // Save to database
253
- const dbData = {
254
- id: testId,
255
- user_id: userId || null,
256
- latitude: parseFloat(latitude) || null,
257
- longitude: parseFloat(longitude) || null,
258
- water_source: waterSource || 'Unknown',
259
- image_path: imagePath,
260
- ph, chlorine, nitrates, hardness, alkalinity, bacteria,
261
- overall_quality: overallQuality,
262
- safety_level: safetyLevel,
263
- confidence: analysisResult.confidence || 95,
264
- processing_time: processingTime,
265
- alerts: JSON.stringify(alerts),
266
- color_analysis: JSON.stringify(analysisResult.colorChannels || {})
267
- };
268
-
269
- db.run(`INSERT INTO water_tests (
270
- id, user_id, latitude, longitude, water_source, image_path,
271
- ph, chlorine, nitrates, hardness, alkalinity, bacteria,
272
- overall_quality, safety_level, confidence, processing_time, alerts, color_analysis
273
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
274
- Object.values(dbData), function(err) {
275
- if (err) console.error('❌ Database error:', err);
276
- else console.log('βœ… Test results saved to database');
277
- });
278
-
279
- if (safetyLevel === 'Unsafe') {
280
- const alertId = uuidv4();
281
- db.run(`INSERT INTO water_alerts (id, test_id, alert_type, severity, message, latitude, longitude)
282
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
283
- [alertId, testId, 'contamination', 'high', `Unsafe water detected: ${alerts.join(', ')}`,
284
- parseFloat(latitude) || null, parseFloat(longitude) || null]);
285
- }
286
-
287
- res.json({
288
- success: true,
289
- testId,
290
- results: { ph, chlorine, nitrates, hardness, alkalinity, bacteria },
291
- overallQuality,
292
- safetyLevel,
293
- alerts,
294
- recommendations,
295
- confidence: Math.round(analysisResult.confidence || 95),
296
- processingTime: parseFloat(processingTime.toFixed(2)),
297
- });
298
-
299
- } catch (error) {
300
- console.error('❌ Analysis error:', error);
301
- res.status(500).json({ error: 'Analysis failed', details: error.message });
302
- } finally {
303
- fs.unlink(imagePath, (err) => {
304
- if (err) console.error(`- Error deleting temp image file: ${err.message}`);
305
- });
306
- }
307
- });
308
-
309
- // Get water quality map data
310
- app.get('/api/water-map', (req, res) => {
311
- db.all(`SELECT id, latitude, longitude, overall_quality, safety_level, timestamp
312
- FROM water_tests
313
- WHERE latitude IS NOT NULL AND longitude IS NOT NULL
314
- ORDER BY timestamp DESC LIMIT 1000`, [], (err, rows) => {
315
- if (err) {
316
- res.status(500).json({ error: 'Database error' });
317
- return;
318
- }
319
- res.json({ success: true, data: rows });
320
- });
321
- });
322
-
323
- // --- STATIC FILE SERVING FOR PRODUCTION ---
324
- // This serves the built React app from the 'web/build' folder.
325
- const buildPath = path.join(__dirname, '..', 'web', 'build');
326
- app.use(express.static(buildPath));
327
- console.log(`Serving production React app from: ${buildPath}`);
328
-
329
- // The "catchall" handler: for any request that doesn't match an API route,
330
- // send back the React app's index.html file.
331
- app.get('*', (req, res) => {
332
- res.sendFile(path.join(buildPath, 'index.html'));
333
- });
334
-
335
- // --- Start Server ---
336
- app.listen(PORT, () => {
337
- console.log(`🌿 GreenPlus by GXS Main Backend Server running on port ${PORT}`);
338
- });
339
-
340
- module.exports = app;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const multer = require('multer');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const { spawn } = require('child_process');
7
+ const sqlite3 = require('sqlite3').verbose();
8
+ const sharp = require('sharp');
9
+ const { v4: uuidv4 } = require('uuid');
10
+ require('dotenv').config();
11
+
12
+ const app = express();
13
+ // cPanel and other hosts use the PORT environment variable
14
+ const PORT = process.env.PORT || 5000;
15
+
16
+ // --- Middleware ---
17
+ // Enable CORS for all routes
18
+ app.use(cors());
19
+ // Parse JSON bodies for API requests
20
+ app.use(express.json());
21
+ app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
22
+
23
+ // Ensure directories exist
24
+ const dirs = ['uploads', 'temp', 'results', 'database'];
25
+ dirs.forEach(dir => {
26
+ const dirPath = path.join(__dirname, dir);
27
+ if (!fs.existsSync(dirPath)) {
28
+ fs.mkdirSync(dirPath, { recursive: true }); // Still need temp dirs for uploads
29
+ }
30
+ });
31
+
32
+ // --- DATABASE DISABLED ---
33
+ // The database is temporarily disabled for simplified deployment on Hugging Face.
34
+ // All database operations will be skipped, and the app will run without persistence.
35
+ console.log('DATABASE: Temporarily disabled for deployment. No data will be saved or retrieved.');
36
+ const db = null;
37
+
38
+
39
+ // --- MULTER CONFIG FOR IMAGE UPLOADS ---
40
+ const imageStorage = multer.diskStorage({
41
+ destination: (req, file, cb) => {
42
+ cb(null, path.join(__dirname, 'uploads/'));
43
+ },
44
+ filename: (req, file, cb) => {
45
+ const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
46
+ cb(null, uniqueName);
47
+ }
48
+ });
49
+
50
+ const imageUpload = multer({
51
+ storage: imageStorage,
52
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
53
+ fileFilter: (req, file, cb) => {
54
+ if (file.mimetype.startsWith('image/')) {
55
+ cb(null, true);
56
+ } else {
57
+ cb(new Error('Only image files are allowed!'), false);
58
+ }
59
+ }
60
+ });
61
+
62
+ // --- MULTER CONFIG FOR AUDIO UPLOADS ---
63
+ const audioStorage = multer.diskStorage({
64
+ destination: function (req, file, cb) {
65
+ cb(null, path.join(__dirname, 'temp/')); // Save audio files in a temp directory
66
+ },
67
+ filename: function (req, file, cb) {
68
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
69
+ cb(null, 'audio-' + uniqueSuffix + path.extname(file.originalname));
70
+ }
71
+ });
72
+
73
+ const audioUpload = multer({
74
+ storage: audioStorage,
75
+ limits: { fileSize: 25 * 1024 * 1024 }, // 25MB limit for audio
76
+ fileFilter: (req, file, cb) => {
77
+ if (file.mimetype.startsWith('audio/')) {
78
+ cb(null, true);
79
+ } else {
80
+ cb(new Error('Only audio files are allowed!'), false);
81
+ }
82
+ }
83
+ });
84
+
85
+ // --- MULTER CONFIG FOR DNA SEQUENCE FILES ---
86
+ const dnaFileStorage = multer.diskStorage({
87
+ destination: function (req, file, cb) {
88
+ cb(null, path.join(__dirname, 'temp/')); // Save DNA files in the same temp directory
89
+ },
90
+ filename: function (req, file, cb) {
91
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
92
+ cb(null, 'dna-' + uniqueSuffix + path.extname(file.originalname));
93
+ }
94
+ });
95
+
96
+ const dnaUpload = multer({
97
+ storage: dnaFileStorage,
98
+ limits: { fileSize: 50 * 1024 * 1024 }, // 50MB limit for sequence files
99
+ fileFilter: (req, file, cb) => {
100
+ // Loosely accept text-based formats common for DNA
101
+ const allowedTypes = ['.fasta', '.fastq', '.txt', '.fa', '.fq'];
102
+ if (allowedTypes.includes(path.extname(file.originalname).toLowerCase())) {
103
+ cb(null, true);
104
+ } else {
105
+ cb(new Error('Invalid file type for DNA analysis!'), false);
106
+ }
107
+ }
108
+ });
109
+
110
+ // --- HELPER FUNCTIONS ---
111
+
112
+ function fallbackAnalysis(imagePath, waterSource) {
113
+ console.log('--- Executing Node.js Fallback Analysis ---');
114
+ // This simulates the Python script's output when it's not available.
115
+ return {
116
+ ph: 7.1,
117
+ chlorine: 0.5,
118
+ nitrates: 5,
119
+ hardness: 150,
120
+ alkalinity: 120,
121
+ bacteria: 0,
122
+ confidence: 40, // Lower confidence to indicate it's a fallback
123
+ processingMethod: "Node.js Fallback"
124
+ };
125
+ }
126
+
127
+ function callPythonAnalysis(imagePath, waterSource) {
128
+ return new Promise((resolve, reject) => {
129
+ const scriptPath = path.join(__dirname, 'python', 'water_analysis.py');
130
+ const pythonProcess = spawn('python', [scriptPath, imagePath, waterSource || 'unknown']);
131
+
132
+ let dataString = '';
133
+ let errorString = '';
134
+
135
+ pythonProcess.stdout.on('data', (data) => {
136
+ dataString += data.toString();
137
+ });
138
+
139
+ pythonProcess.stderr.on('data', (data) => {
140
+ errorString += data.toString();
141
+ });
142
+
143
+ pythonProcess.on('close', (code) => {
144
+ if (code === 0) {
145
+ try {
146
+ const result = JSON.parse(dataString);
147
+ resolve(result);
148
+ } catch (error) {
149
+ reject(new Error('Failed to parse Python output'));
150
+ }
151
+ } else {
152
+ console.log(`Python script at ${scriptPath} not available or failed, using fallback. Error: ${errorString}`);
153
+ resolve(fallbackAnalysis(imagePath, waterSource));
154
+ }
155
+ });
156
+ });
157
+ }
158
+
159
+ // Dummy C++ preprocessor call for demonstration
160
+ async function callCppPreprocessing(imagePath) {
161
+ return new Promise((resolve, reject) => {
162
+ // In a real scenario, you would spawn a C++ executable.
163
+ // For this demo, we'll just simulate a failure to show the fallback.
164
+ reject(new Error("C++ preprocessor not found or failed."));
165
+ });
166
+ }
167
+
168
+ // --- API ROUTES ---
169
+
170
+ // --- BIODIVERSITYEAR AUDIO ANALYSIS ENDPOINT ---
171
+ app.post('/api/analyze-audio', audioUpload.single('audioFile'), (req, res) => {
172
+ console.log('βœ… Backend: Received audio file analysis request.');
173
+
174
+ if (!req.file || req.file.size === 0) {
175
+ if (req.file) {
176
+ fs.unlink(req.file.path, (err) => {
177
+ if (err) console.error(`- Error deleting empty file: ${err.message}`);
178
+ });
179
+ }
180
+ return res.status(400).json({ message: 'The provided audio file is empty or missing. Please record for at least one second.' });
181
+ }
182
+
183
+ console.log(`- Audio file saved to: ${req.file.path}`);
184
+ console.log(`- Region: ${req.body.region}, Habitat: ${req.body.habitat}`);
185
+ console.log('▢️ Calling Python AI script for audio analysis...');
186
+
187
+ const pythonProcess = spawn('python', [
188
+ path.join(__dirname, 'python', 'audio_analyzer.py'),
189
+ req.file.path
190
+ ]);
191
+
192
+ let analysisResult = '';
193
+ let errorOutput = '';
194
+
195
+ pythonProcess.stdout.on('data', (data) => {
196
+ analysisResult += data.toString();
197
+ });
198
+
199
+ pythonProcess.stderr.on('data', (data) => {
200
+ errorOutput += data.toString();
201
+ });
202
+
203
+ pythonProcess.on('close', (code) => {
204
+ // Clean up the uploaded audio file after analysis
205
+ fs.unlink(req.file.path, (err) => {
206
+ if (err) console.error(`- Error deleting temp audio file: ${err.message}`);
207
+ else console.log(`- Temporary audio file ${req.file.path} deleted.`);
208
+ });
209
+
210
+ if (code === 0) {
211
+ console.log('βœ… Backend: Python audio script finished successfully.');
212
+ try {
213
+ const jsonData = JSON.parse(analysisResult);
214
+ res.status(200).json(jsonData);
215
+ } catch (e) {
216
+ console.error('❌ Backend: Error parsing JSON from Python audio script.', e);
217
+ res.status(500).json({ message: 'Failed to parse audio analysis result.' });
218
+ }
219
+ } else {
220
+ console.error(`❌ Backend: Python audio script exited with error code ${code}.`);
221
+ console.error(`- Python Error: ${errorOutput}`);
222
+ res.status(500).json({ message: 'Error during audio analysis.', error: errorOutput });
223
+ }
224
+ });
225
+ });
226
+
227
+ // --- BIO-STREAM AI DNA ANALYSIS ENDPOINT ---
228
+ app.post('/api/analyze-dna', dnaUpload.single('dnaFile'), (req, res) => {
229
+ console.log('βœ… Backend: Received DNA file analysis request for Bio-Stream AI.');
230
+
231
+ if (!req.file) {
232
+ return res.status(400).json({ message: 'No DNA file was uploaded.' });
233
+ }
234
+
235
+ console.log(`- DNA file saved to: ${req.file.path}`);
236
+ console.log('▢️ Calling Python AI script for DNA analysis...');
237
+
238
+ const pythonProcess = spawn('python', [
239
+ path.join(__dirname, 'python', 'dna_analyzer.py'),
240
+ req.file.path // Pass the full path of the uploaded file to the script
241
+ ]);
242
+
243
+ let analysisResult = '';
244
+ let errorOutput = '';
245
+
246
+ pythonProcess.stdout.on('data', (data) => {
247
+ analysisResult += data.toString();
248
+ });
249
+
250
+ pythonProcess.stderr.on('data', (data) => {
251
+ errorOutput += data.toString();
252
+ });
253
+
254
+ pythonProcess.on('close', (code) => {
255
+ // IMPORTANT: Clean up the uploaded DNA file after analysis is complete
256
+ fs.unlink(req.file.path, (err) => {
257
+ if (err) console.error(`- Error deleting temp DNA file: ${err.message}`);
258
+ else console.log(`- Temporary DNA file ${req.file.path} deleted.`);
259
+ });
260
+
261
+ if (code === 0) {
262
+ console.log('βœ… Backend: Python DNA analysis script finished successfully.');
263
+ try {
264
+ const jsonData = JSON.parse(analysisResult);
265
+ res.status(200).json(jsonData);
266
+ } catch (e) {
267
+ console.error('❌ Backend: Error parsing JSON from Python DNA script.', e);
268
+ res.status(500).json({ message: 'Failed to parse DNA analysis result.' });
269
+ }
270
+ } else {
271
+ console.error(`❌ Backend: Python DNA script exited with error code ${code}.`);
272
+ console.error(`- Python Error: ${errorOutput}`);
273
+ res.status(500).json({ message: 'Error during DNA analysis.', error: errorOutput });
274
+ }
275
+ });
276
+ });
277
+
278
+ // --- PHANTOM FOOTPRINT ANALYSIS ENDPOINT ---
279
+ app.post('/api/analyze-footprint', (req, res) => {
280
+ const { url } = req.body;
281
+ console.log(`βœ… Backend: Received Phantom Footprint analysis request for URL: ${url}`);
282
+
283
+ if (!url) {
284
+ return res.status(400).json({ message: 'Product URL is required.' });
285
+ }
286
+
287
+ const pythonProcess = spawn('python', [
288
+ path.join(__dirname, 'python', 'phantom_footprint_analyzer.py')
289
+ ]);
290
+
291
+ let analysisResult = '';
292
+ let errorOutput = '';
293
+
294
+ pythonProcess.stdin.write(JSON.stringify({ url: url }));
295
+ pythonProcess.stdin.end();
296
+
297
+ pythonProcess.stdout.on('data', (data) => {
298
+ analysisResult += data.toString();
299
+ });
300
+
301
+ pythonProcess.stderr.on('data', (data) => {
302
+ errorOutput += data.toString();
303
+ });
304
+
305
+ pythonProcess.on('close', (code) => {
306
+ if (code === 0) {
307
+ console.log('βœ… Backend: Python footprint script finished successfully.');
308
+ try {
309
+ const jsonData = JSON.parse(analysisResult);
310
+ res.status(200).json(jsonData);
311
+ } catch (e) {
312
+ console.error('❌ Backend: Error parsing JSON from Python script.', e);
313
+ res.status(500).json({ message: 'Failed to parse analysis result.' });
314
+ }
315
+ } else {
316
+ console.error(`❌ Backend: Python footprint script exited with error code ${code}.`);
317
+ console.error(`- Python Error: ${errorOutput}`);
318
+ res.status(500).json({ message: 'Error during footprint analysis.', error: errorOutput });
319
+ }
320
+ });
321
+ });
322
+
323
+ // --- E-WASTE ANALYSIS ENDPOINT ---
324
+ app.post('/api/analyze-ewaste', (req, res) => {
325
+ console.log('βœ… Backend: Received e-waste form analysis request.');
326
+
327
+ const pythonProcess = spawn('python', [
328
+ path.join(__dirname, 'python', 'ewaste_analyzer.py')
329
+ ]);
330
+
331
+ let analysisResult = '';
332
+ let errorOutput = '';
333
+
334
+ pythonProcess.stdin.write(JSON.stringify(req.body));
335
+ pythonProcess.stdin.end();
336
+
337
+ pythonProcess.stdout.on('data', (data) => {
338
+ analysisResult += data.toString();
339
+ });
340
+
341
+ pythonProcess.stderr.on('data', (data) => {
342
+ errorOutput += data.toString();
343
+ });
344
+
345
+ pythonProcess.on('close', (code) => {
346
+ if (code === 0) {
347
+ console.log('βœ… Backend: Python e-waste script finished successfully.');
348
+ try {
349
+ const jsonData = JSON.parse(analysisResult);
350
+ res.status(200).json(jsonData);
351
+ } catch (e) {
352
+ console.error('❌ Backend: Error parsing JSON from Python script.', e);
353
+ res.status(500).json({ message: 'Failed to parse e-waste analysis result.' });
354
+ }
355
+ } else {
356
+ console.error(`❌ Backend: Python e-waste script exited with error code ${code}.`);
357
+ console.error(`- Python Error: ${errorOutput}`);
358
+ res.status(500).json({ message: 'Error during e-waste analysis.', error: errorOutput });
359
+ }
360
+ });
361
+ });
362
+
363
+ // --- AQUALENS WATER ANALYSIS ENDPOINT ---
364
+ app.post('/api/analyze-water', imageUpload.single('image'), async (req, res) => {
365
+ const startTime = Date.now();
366
+
367
+ try {
368
+ if (!req.file) {
369
+ return res.status(400).json({ error: 'No image file provided' });
370
+ }
371
+
372
+ const { waterSource, latitude, longitude, userId } = req.body;
373
+ const imagePath = req.file.path;
374
+ const testId = uuidv4();
375
+
376
+ console.log(`πŸ§ͺ Starting water analysis for test ${testId}`);
377
+ console.log(`πŸ“ Location: ${latitude}, ${longitude}`);
378
+ console.log(`🚰 Water Source: ${waterSource}`);
379
+
380
+ // Step 1: Image preprocessing with Sharp (Node.js)
381
+ const preprocessedPath = path.join(__dirname, 'temp', `preprocessed_${testId}.jpg`);
382
+ await sharp(imagePath)
383
+ .resize(800, 600, { fit: 'inside' })
384
+ .normalize()
385
+ .sharpen()
386
+ .jpeg({ quality: 95 })
387
+ .toFile(preprocessedPath);
388
+
389
+ console.log('βœ… Image preprocessing completed');
390
+
391
+ // Step 2: Advanced preprocessing with C++ (if available)
392
+ let processedImagePath = preprocessedPath;
393
+ try {
394
+ processedImagePath = await callCppPreprocessing(preprocessedPath);
395
+ console.log('βœ… C++ preprocessing completed');
396
+ } catch (error) {
397
+ console.log('⚠️ C++ preprocessing not available, using Sharp preprocessing');
398
+ }
399
+
400
+ // Step 3: AI analysis with Python
401
+ const analysisResult = await callPythonAnalysis(processedImagePath, waterSource);
402
+ console.log('βœ… AI analysis completed');
403
+
404
+ const processingTime = (Date.now() - startTime) / 1000;
405
+
406
+ // Step 4: Determine overall quality and safety
407
+ const { ph, chlorine, nitrates, hardness, alkalinity, bacteria } = analysisResult;
408
+
409
+ let overallQuality = 'Excellent';
410
+ let safetyLevel = 'Safe';
411
+ let alerts = [];
412
+ let recommendations = [];
413
+
414
+ if (ph < 6.5 || ph > 8.5) {
415
+ alerts.push(`pH levels outside safe range: ${ph.toFixed(1)}`);
416
+ recommendations.push('Consider pH adjustment or filtration system');
417
+ if (ph < 5.0 || ph > 9.5) {
418
+ safetyLevel = 'Unsafe';
419
+ overallQuality = 'Poor';
420
+ recommendations.push('Contact water authority immediately - pH outside safe drinking range');
421
+ } else {
422
+ overallQuality = 'Good';
423
+ }
424
+ }
425
+
426
+ if (chlorine > 4.0) {
427
+ alerts.push(`High chlorine levels: ${chlorine.toFixed(1)} ppm`);
428
+ recommendations.push('Let water sit uncovered for 30 minutes before drinking');
429
+ safetyLevel = 'Unsafe';
430
+ overallQuality = 'Poor';
431
+ } else if (chlorine < 0.2 && waterSource === 'Tap Water') {
432
+ alerts.push('Low chlorine in tap water may indicate contamination risk');
433
+ recommendations.push('Consider boiling water or using filtration');
434
+ }
435
+
436
+ if (nitrates > 10) {
437
+ alerts.push(`Elevated nitrate levels: ${nitrates} ppm`);
438
+ recommendations.push('Consider reverse osmosis filtration or alternative water source');
439
+ if (nitrates > 50) {
440
+ safetyLevel = 'Unsafe';
441
+ overallQuality = 'Poor';
442
+ recommendations.push('DO NOT DRINK - Especially dangerous for infants and pregnant women');
443
+ } else {
444
+ overallQuality = 'Fair';
445
+ }
446
+ }
447
+
448
+ if (bacteria > 0) {
449
+ alerts.push('Bacterial contamination detected');
450
+ recommendations.push('Boil water for 1 minute before drinking or use alternative source');
451
+ safetyLevel = 'Unsafe';
452
+ overallQuality = 'Poor';
453
+ }
454
+
455
+ if (hardness > 180) {
456
+ alerts.push(`Very hard water: ${hardness} ppm`);
457
+ recommendations.push('Consider water softener to protect plumbing and improve taste');
458
+ if (overallQuality === 'Excellent') overallQuality = 'Good';
459
+ }
460
+
461
+ // Step 5: Save to database
462
+ if (db) {
463
+ // Database logic would go here.
464
+ } else {
465
+ console.log('DATABASE: Skipping save operation for water analysis results.');
466
+ }
467
+
468
+ setTimeout(() => {
469
+ [preprocessedPath, processedImagePath].forEach(p => {
470
+ if (fs.existsSync(p) && p !== imagePath) {
471
+ fs.unlinkSync(p);
472
+ }
473
+ });
474
+ }, 5000);
475
+
476
+ console.log(`βœ… Analysis completed in ${processingTime.toFixed(2)}s`);
477
+
478
+ res.json({
479
+ success: true,
480
+ testId: testId,
481
+ results: {
482
+ ph: parseFloat(ph.toFixed(1)),
483
+ chlorine: parseFloat(chlorine.toFixed(1)),
484
+ nitrates: Math.round(nitrates),
485
+ hardness: Math.round(hardness),
486
+ alkalinity: Math.round(alkalinity),
487
+ bacteria: bacteria
488
+ },
489
+ overallQuality: overallQuality,
490
+ safetyLevel: safetyLevel,
491
+ alerts: alerts,
492
+ recommendations: recommendations,
493
+ confidence: Math.round(analysisResult.confidence || 95),
494
+ processingTime: parseFloat(processingTime.toFixed(2)),
495
+ colorAccuracy: analysisResult.colorAccuracy || '94%',
496
+ processingMethod: analysisResult.processingMethod || 'AI Analysis',
497
+ colorChannels: analysisResult.colorChannels || {},
498
+ timestamp: new Date().toISOString(),
499
+ location: {
500
+ latitude: parseFloat(latitude) || null,
501
+ longitude: parseFloat(longitude) || null
502
+ }
503
+ });
504
+
505
+ } catch (error) {
506
+ console.error('❌ Analysis error:', error);
507
+ res.status(500).json({
508
+ error: 'Analysis failed',
509
+ details: error.message,
510
+ timestamp: new Date().toISOString()
511
+ });
512
+ }
513
+ });
514
+
515
+ // Get water quality map data
516
+ app.get('/api/water-map', (req, res) => {
517
+ // Since the database is disabled, return an empty array for map data.
518
+ console.log('DATABASE: /api/water-map called, returning empty dataset as DB is disabled.');
519
+ res.json({ success: true, data: [], count: 0 });
520
+ });
521
+
522
+ // --- STATIC FILE SERVING FOR PRODUCTION ---
523
+ // This is the crucial part for serving your React app in production.
524
+ // It serves the built React app from the 'web/build' folder.
525
+ const buildPath = path.join(__dirname, '..', 'web', 'build');
526
+ app.use(express.static(buildPath));
527
+
528
+ // The "catchall" handler: for any request that doesn't match one of our API routes,
529
+ // send back the React app's index.html file. This allows React Router to handle the route.
530
+ app.get('*', (req, res) => {
531
+ res.sendFile(path.join(buildPath, 'index.html'));
532
+ });
533
+
534
+ // --- Start Server ---
535
+ app.listen(PORT, () => {
536
+ console.log(`🌿 GreenPlus by GXS Main Backend Server running on port ${PORT}`);
537
+ console.log(`πŸš€ Serving production React app from: ${buildPath}`);
538
+ });