gaialive commited on
Commit
4b4aeda
·
verified ·
1 Parent(s): 7a2ae7c

Update backend/server.js

Browse files
Files changed (1) hide show
  1. backend/server.js +340 -667
backend/server.js CHANGED
@@ -1,667 +1,340 @@
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 });
29
- }
30
- });
31
-
32
- // --- DATABASE SETUP ---
33
- const dbPath = path.join(__dirname, 'database', 'water_quality.db');
34
- const db = new sqlite3.Database(dbPath, (err) => {
35
- if (err) {
36
- console.error('Error opening database', err.message);
37
- } else {
38
- console.log(`Connected to the SQLite database at ${dbPath}`);
39
- }
40
- });
41
- db.serialize(() => {
42
- db.run(`CREATE TABLE IF NOT EXISTS water_tests (
43
- id TEXT PRIMARY KEY,
44
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
45
- user_id TEXT,
46
- latitude REAL,
47
- longitude REAL,
48
- water_source TEXT,
49
- image_path TEXT,
50
- ph REAL,
51
- chlorine REAL,
52
- nitrates REAL,
53
- hardness REAL,
54
- alkalinity REAL,
55
- bacteria INTEGER,
56
- overall_quality TEXT,
57
- safety_level TEXT,
58
- confidence REAL,
59
- processing_time REAL,
60
- alerts TEXT,
61
- color_analysis TEXT
62
- )`);
63
-
64
- db.run(`CREATE TABLE IF NOT EXISTS water_alerts (
65
- id TEXT PRIMARY KEY,
66
- test_id TEXT,
67
- alert_type TEXT,
68
- severity TEXT,
69
- message TEXT,
70
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
71
- latitude REAL,
72
- longitude REAL,
73
- FOREIGN KEY(test_id) REFERENCES water_tests(id)
74
- )`);
75
-
76
- db.run(`CREATE TABLE IF NOT EXISTS calibration_data (
77
- id TEXT PRIMARY KEY,
78
- parameter TEXT,
79
- color_value TEXT,
80
- actual_value REAL,
81
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
82
- )`);
83
- });
84
-
85
-
86
- // --- MULTER CONFIG FOR IMAGE UPLOADS ---
87
- const imageStorage = multer.diskStorage({
88
- destination: (req, file, cb) => {
89
- cb(null, path.join(__dirname, 'uploads/'));
90
- },
91
- filename: (req, file, cb) => {
92
- const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
93
- cb(null, uniqueName);
94
- }
95
- });
96
-
97
- const imageUpload = multer({
98
- storage: imageStorage,
99
- limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
100
- fileFilter: (req, file, cb) => {
101
- if (file.mimetype.startsWith('image/')) {
102
- cb(null, true);
103
- } else {
104
- cb(new Error('Only image files are allowed!'), false);
105
- }
106
- }
107
- });
108
-
109
- // --- MULTER CONFIG FOR AUDIO UPLOADS ---
110
- const audioStorage = multer.diskStorage({
111
- destination: function (req, file, cb) {
112
- cb(null, path.join(__dirname, 'temp/')); // Save audio files in a temp directory
113
- },
114
- filename: function (req, file, cb) {
115
- const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
116
- cb(null, 'audio-' + uniqueSuffix + path.extname(file.originalname));
117
- }
118
- });
119
-
120
- const audioUpload = multer({
121
- storage: audioStorage,
122
- limits: { fileSize: 25 * 1024 * 1024 }, // 25MB limit for audio
123
- fileFilter: (req, file, cb) => {
124
- if (file.mimetype.startsWith('audio/')) {
125
- cb(null, true);
126
- } else {
127
- cb(new Error('Only audio files are allowed!'), false);
128
- }
129
- }
130
- });
131
-
132
- // --- MULTER CONFIG FOR DNA SEQUENCE FILES ---
133
- const dnaFileStorage = multer.diskStorage({
134
- destination: function (req, file, cb) {
135
- cb(null, path.join(__dirname, 'temp/')); // Save DNA files in the same temp directory
136
- },
137
- filename: function (req, file, cb) {
138
- const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
139
- cb(null, 'dna-' + uniqueSuffix + path.extname(file.originalname));
140
- }
141
- });
142
-
143
- const dnaUpload = multer({
144
- storage: dnaFileStorage,
145
- limits: { fileSize: 50 * 1024 * 1024 }, // 50MB limit for sequence files
146
- fileFilter: (req, file, cb) => {
147
- // Loosely accept text-based formats common for DNA
148
- const allowedTypes = ['.fasta', '.fastq', '.txt', '.fa', '.fq'];
149
- if (allowedTypes.includes(path.extname(file.originalname).toLowerCase())) {
150
- cb(null, true);
151
- } else {
152
- cb(new Error('Invalid file type for DNA analysis!'), false);
153
- }
154
- }
155
- });
156
-
157
- // --- HELPER FUNCTIONS ---
158
-
159
- function fallbackAnalysis(imagePath, waterSource) {
160
- console.log('--- Executing Node.js Fallback Analysis ---');
161
- // This simulates the Python script's output when it's not available.
162
- return {
163
- ph: 7.1,
164
- chlorine: 0.5,
165
- nitrates: 5,
166
- hardness: 150,
167
- alkalinity: 120,
168
- bacteria: 0,
169
- confidence: 40, // Lower confidence to indicate it's a fallback
170
- processingMethod: "Node.js Fallback"
171
- };
172
- }
173
-
174
- function callPythonAnalysis(imagePath, waterSource) {
175
- return new Promise((resolve, reject) => {
176
- const scriptPath = path.join(__dirname, 'python', 'water_analysis.py');
177
- const pythonProcess = spawn('python', [scriptPath, imagePath, waterSource || 'unknown']);
178
-
179
- let dataString = '';
180
- let errorString = '';
181
-
182
- pythonProcess.stdout.on('data', (data) => {
183
- dataString += data.toString();
184
- });
185
-
186
- pythonProcess.stderr.on('data', (data) => {
187
- errorString += data.toString();
188
- });
189
-
190
- pythonProcess.on('close', (code) => {
191
- if (code === 0) {
192
- try {
193
- const result = JSON.parse(dataString);
194
- resolve(result);
195
- } catch (error) {
196
- reject(new Error('Failed to parse Python output'));
197
- }
198
- } else {
199
- console.log(`Python script at ${scriptPath} not available or failed, using fallback. Error: ${errorString}`);
200
- resolve(fallbackAnalysis(imagePath, waterSource));
201
- }
202
- });
203
- });
204
- }
205
-
206
- // Dummy C++ preprocessor call for demonstration
207
- async function callCppPreprocessing(imagePath) {
208
- return new Promise((resolve, reject) => {
209
- // In a real scenario, you would spawn a C++ executable.
210
- // For this demo, we'll just simulate a failure to show the fallback.
211
- reject(new Error("C++ preprocessor not found or failed."));
212
- });
213
- }
214
-
215
- // --- API ROUTES ---
216
-
217
- // --- BIODIVERSITYEAR AUDIO ANALYSIS ENDPOINT ---
218
- app.post('/api/analyze-audio', audioUpload.single('audioFile'), (req, res) => {
219
- console.log(' Backend: Received audio file analysis request.');
220
-
221
- if (!req.file || req.file.size === 0) {
222
- if (req.file) {
223
- fs.unlink(req.file.path, (err) => {
224
- if (err) console.error(`- Error deleting empty file: ${err.message}`);
225
- });
226
- }
227
- return res.status(400).json({ message: 'The provided audio file is empty or missing. Please record for at least one second.' });
228
- }
229
-
230
- console.log(`- Audio file saved to: ${req.file.path}`);
231
- console.log(`- Region: ${req.body.region}, Habitat: ${req.body.habitat}`);
232
- console.log('▶️ Calling Python AI script for audio analysis...');
233
-
234
- const pythonProcess = spawn('python', [
235
- path.join(__dirname, 'python', 'audio_analyzer.py'),
236
- req.file.path
237
- ]);
238
-
239
- let analysisResult = '';
240
- let errorOutput = '';
241
-
242
- pythonProcess.stdout.on('data', (data) => {
243
- analysisResult += data.toString();
244
- });
245
-
246
- pythonProcess.stderr.on('data', (data) => {
247
- errorOutput += data.toString();
248
- });
249
-
250
- pythonProcess.on('close', (code) => {
251
- // Clean up the uploaded audio file after analysis
252
- fs.unlink(req.file.path, (err) => {
253
- if (err) console.error(`- Error deleting temp audio file: ${err.message}`);
254
- else console.log(`- Temporary audio file ${req.file.path} deleted.`);
255
- });
256
-
257
- if (code === 0) {
258
- console.log('✅ Backend: Python audio script finished successfully.');
259
- try {
260
- const jsonData = JSON.parse(analysisResult);
261
- res.status(200).json(jsonData);
262
- } catch (e) {
263
- console.error('❌ Backend: Error parsing JSON from Python audio script.', e);
264
- res.status(500).json({ message: 'Failed to parse audio analysis result.' });
265
- }
266
- } else {
267
- console.error(`❌ Backend: Python audio script exited with error code ${code}.`);
268
- console.error(`- Python Error: ${errorOutput}`);
269
- res.status(500).json({ message: 'Error during audio analysis.', error: errorOutput });
270
- }
271
- });
272
- });
273
-
274
- // --- BIO-STREAM AI DNA ANALYSIS ENDPOINT ---
275
- app.post('/api/analyze-dna', dnaUpload.single('dnaFile'), (req, res) => {
276
- console.log('✅ Backend: Received DNA file analysis request for Bio-Stream AI.');
277
-
278
- if (!req.file) {
279
- return res.status(400).json({ message: 'No DNA file was uploaded.' });
280
- }
281
-
282
- console.log(`- DNA file saved to: ${req.file.path}`);
283
- console.log('▶️ Calling Python AI script for DNA analysis...');
284
-
285
- const pythonProcess = spawn('python', [
286
- path.join(__dirname, 'python', 'dna_analyzer.py'),
287
- req.file.path // Pass the full path of the uploaded file to the script
288
- ]);
289
-
290
- let analysisResult = '';
291
- let errorOutput = '';
292
-
293
- pythonProcess.stdout.on('data', (data) => {
294
- analysisResult += data.toString();
295
- });
296
-
297
- pythonProcess.stderr.on('data', (data) => {
298
- errorOutput += data.toString();
299
- });
300
-
301
- pythonProcess.on('close', (code) => {
302
- // IMPORTANT: Clean up the uploaded DNA file after analysis is complete
303
- fs.unlink(req.file.path, (err) => {
304
- if (err) console.error(`- Error deleting temp DNA file: ${err.message}`);
305
- else console.log(`- Temporary DNA file ${req.file.path} deleted.`);
306
- });
307
-
308
- if (code === 0) {
309
- console.log('✅ Backend: Python DNA analysis script finished successfully.');
310
- try {
311
- const jsonData = JSON.parse(analysisResult);
312
- res.status(200).json(jsonData);
313
- } catch (e) {
314
- console.error('❌ Backend: Error parsing JSON from Python DNA script.', e);
315
- res.status(500).json({ message: 'Failed to parse DNA analysis result.' });
316
- }
317
- } else {
318
- console.error(`❌ Backend: Python DNA script exited with error code ${code}.`);
319
- console.error(`- Python Error: ${errorOutput}`);
320
- res.status(500).json({ message: 'Error during DNA analysis.', error: errorOutput });
321
- }
322
- });
323
- });
324
-
325
- // --- PHANTOM FOOTPRINT ANALYSIS ENDPOINT ---
326
- app.post('/api/analyze-footprint', (req, res) => {
327
- const { url } = req.body;
328
- console.log(`✅ Backend: Received Phantom Footprint analysis request for URL: ${url}`);
329
-
330
- if (!url) {
331
- return res.status(400).json({ message: 'Product URL is required.' });
332
- }
333
-
334
- const pythonProcess = spawn('python', [
335
- path.join(__dirname, 'python', 'phantom_footprint_analyzer.py')
336
- ]);
337
-
338
- let analysisResult = '';
339
- let errorOutput = '';
340
-
341
- pythonProcess.stdin.write(JSON.stringify({ url: url }));
342
- pythonProcess.stdin.end();
343
-
344
- pythonProcess.stdout.on('data', (data) => {
345
- analysisResult += data.toString();
346
- });
347
-
348
- pythonProcess.stderr.on('data', (data) => {
349
- errorOutput += data.toString();
350
- });
351
-
352
- pythonProcess.on('close', (code) => {
353
- if (code === 0) {
354
- console.log('✅ Backend: Python footprint script finished successfully.');
355
- try {
356
- const jsonData = JSON.parse(analysisResult);
357
- res.status(200).json(jsonData);
358
- } catch (e) {
359
- console.error('❌ Backend: Error parsing JSON from Python script.', e);
360
- res.status(500).json({ message: 'Failed to parse analysis result.' });
361
- }
362
- } else {
363
- console.error(`❌ Backend: Python footprint script exited with error code ${code}.`);
364
- console.error(`- Python Error: ${errorOutput}`);
365
- res.status(500).json({ message: 'Error during footprint analysis.', error: errorOutput });
366
- }
367
- });
368
- });
369
-
370
- // --- E-WASTE ANALYSIS ENDPOINT ---
371
- app.post('/api/analyze-ewaste', (req, res) => {
372
- console.log('✅ Backend: Received e-waste form analysis request.');
373
-
374
- const pythonProcess = spawn('python', [
375
- path.join(__dirname, 'python', 'ewaste_analyzer.py')
376
- ]);
377
-
378
- let analysisResult = '';
379
- let errorOutput = '';
380
-
381
- pythonProcess.stdin.write(JSON.stringify(req.body));
382
- pythonProcess.stdin.end();
383
-
384
- pythonProcess.stdout.on('data', (data) => {
385
- analysisResult += data.toString();
386
- });
387
-
388
- pythonProcess.stderr.on('data', (data) => {
389
- errorOutput += data.toString();
390
- });
391
-
392
- pythonProcess.on('close', (code) => {
393
- if (code === 0) {
394
- console.log('✅ Backend: Python e-waste script finished successfully.');
395
- try {
396
- const jsonData = JSON.parse(analysisResult);
397
- res.status(200).json(jsonData);
398
- } catch (e) {
399
- console.error('❌ Backend: Error parsing JSON from Python script.', e);
400
- res.status(500).json({ message: 'Failed to parse e-waste analysis result.' });
401
- }
402
- } else {
403
- console.error(`❌ Backend: Python e-waste script exited with error code ${code}.`);
404
- console.error(`- Python Error: ${errorOutput}`);
405
- res.status(500).json({ message: 'Error during e-waste analysis.', error: errorOutput });
406
- }
407
- });
408
- });
409
-
410
- // --- AQUALENS WATER ANALYSIS ENDPOINT ---
411
- app.post('/api/analyze-water', imageUpload.single('image'), async (req, res) => {
412
- const startTime = Date.now();
413
-
414
- try {
415
- if (!req.file) {
416
- return res.status(400).json({ error: 'No image file provided' });
417
- }
418
-
419
- const { waterSource, latitude, longitude, userId } = req.body;
420
- const imagePath = req.file.path;
421
- const testId = uuidv4();
422
-
423
- console.log(`🧪 Starting water analysis for test ${testId}`);
424
- console.log(`📍 Location: ${latitude}, ${longitude}`);
425
- console.log(`🚰 Water Source: ${waterSource}`);
426
-
427
- // Step 1: Image preprocessing with Sharp (Node.js)
428
- const preprocessedPath = path.join(__dirname, 'temp', `preprocessed_${testId}.jpg`);
429
- await sharp(imagePath)
430
- .resize(800, 600, { fit: 'inside' })
431
- .normalize()
432
- .sharpen()
433
- .jpeg({ quality: 95 })
434
- .toFile(preprocessedPath);
435
-
436
- console.log('✅ Image preprocessing completed');
437
-
438
- // Step 2: Advanced preprocessing with C++ (if available)
439
- let processedImagePath = preprocessedPath;
440
- try {
441
- processedImagePath = await callCppPreprocessing(preprocessedPath);
442
- console.log('✅ C++ preprocessing completed');
443
- } catch (error) {
444
- console.log('⚠️ C++ preprocessing not available, using Sharp preprocessing');
445
- }
446
-
447
- // Step 3: AI analysis with Python
448
- const analysisResult = await callPythonAnalysis(processedImagePath, waterSource);
449
- console.log('✅ AI analysis completed');
450
-
451
- const processingTime = (Date.now() - startTime) / 1000;
452
-
453
- // Step 4: Determine overall quality and safety
454
- const { ph, chlorine, nitrates, hardness, alkalinity, bacteria } = analysisResult;
455
-
456
- let overallQuality = 'Excellent';
457
- let safetyLevel = 'Safe';
458
- let alerts = [];
459
- let recommendations = [];
460
-
461
- if (ph < 6.5 || ph > 8.5) {
462
- alerts.push(`pH levels outside safe range: ${ph.toFixed(1)}`);
463
- recommendations.push('Consider pH adjustment or filtration system');
464
- if (ph < 5.0 || ph > 9.5) {
465
- safetyLevel = 'Unsafe';
466
- overallQuality = 'Poor';
467
- recommendations.push('Contact water authority immediately - pH outside safe drinking range');
468
- } else {
469
- overallQuality = 'Good';
470
- }
471
- }
472
-
473
- if (chlorine > 4.0) {
474
- alerts.push(`High chlorine levels: ${chlorine.toFixed(1)} ppm`);
475
- recommendations.push('Let water sit uncovered for 30 minutes before drinking');
476
- safetyLevel = 'Unsafe';
477
- overallQuality = 'Poor';
478
- } else if (chlorine < 0.2 && waterSource === 'Tap Water') {
479
- alerts.push('Low chlorine in tap water may indicate contamination risk');
480
- recommendations.push('Consider boiling water or using filtration');
481
- }
482
-
483
- if (nitrates > 10) {
484
- alerts.push(`Elevated nitrate levels: ${nitrates} ppm`);
485
- recommendations.push('Consider reverse osmosis filtration or alternative water source');
486
- if (nitrates > 50) {
487
- safetyLevel = 'Unsafe';
488
- overallQuality = 'Poor';
489
- recommendations.push('DO NOT DRINK - Especially dangerous for infants and pregnant women');
490
- } else {
491
- overallQuality = 'Fair';
492
- }
493
- }
494
-
495
- if (bacteria > 0) {
496
- alerts.push('Bacterial contamination detected');
497
- recommendations.push('Boil water for 1 minute before drinking or use alternative source');
498
- safetyLevel = 'Unsafe';
499
- overallQuality = 'Poor';
500
- }
501
-
502
- if (hardness > 180) {
503
- alerts.push(`Very hard water: ${hardness} ppm`);
504
- recommendations.push('Consider water softener to protect plumbing and improve taste');
505
- if (overallQuality === 'Excellent') overallQuality = 'Good';
506
- }
507
-
508
- // Step 5: Save to database
509
- const dbData = {
510
- id: testId,
511
- user_id: userId || null,
512
- latitude: parseFloat(latitude) || null,
513
- longitude: parseFloat(longitude) || null,
514
- water_source: waterSource || 'Unknown',
515
- image_path: imagePath,
516
- ph: ph,
517
- chlorine: chlorine,
518
- nitrates: nitrates,
519
- hardness: hardness,
520
- alkalinity: alkalinity,
521
- bacteria: bacteria,
522
- overall_quality: overallQuality,
523
- safety_level: safetyLevel,
524
- confidence: analysisResult.confidence || 95,
525
- processing_time: processingTime,
526
- alerts: JSON.stringify(alerts),
527
- color_analysis: JSON.stringify(analysisResult.colorChannels || {})
528
- };
529
-
530
- db.run(`INSERT INTO water_tests (
531
- id, user_id, latitude, longitude, water_source, image_path,
532
- ph, chlorine, nitrates, hardness, alkalinity, bacteria,
533
- overall_quality, safety_level, confidence, processing_time, alerts, color_analysis
534
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
535
- Object.values(dbData), function(err) {
536
- if (err) {
537
- console.error('❌ Database error:', err);
538
- } else {
539
- console.log('✅ Test results saved to database');
540
- }
541
- });
542
-
543
- if (safetyLevel === 'Unsafe') {
544
- const alertId = uuidv4();
545
- db.run(`INSERT INTO water_alerts (id, test_id, alert_type, severity, message, latitude, longitude)
546
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
547
- [alertId, testId, 'contamination', 'high', `Unsafe water detected: ${alerts.join(', ')}`,
548
- parseFloat(latitude) || null, parseFloat(longitude) || null]);
549
- console.log('🚨 Safety alert created');
550
- }
551
-
552
- setTimeout(() => {
553
- [preprocessedPath, processedImagePath].forEach(p => {
554
- if (fs.existsSync(p) && p !== imagePath) {
555
- fs.unlinkSync(p);
556
- }
557
- });
558
- }, 5000);
559
-
560
- console.log(`✅ Analysis completed in ${processingTime.toFixed(2)}s`);
561
-
562
- res.json({
563
- success: true,
564
- testId: testId,
565
- results: {
566
- ph: parseFloat(ph.toFixed(1)),
567
- chlorine: parseFloat(chlorine.toFixed(1)),
568
- nitrates: Math.round(nitrates),
569
- hardness: Math.round(hardness),
570
- alkalinity: Math.round(alkalinity),
571
- bacteria: bacteria
572
- },
573
- overallQuality: overallQuality,
574
- safetyLevel: safetyLevel,
575
- alerts: alerts,
576
- recommendations: recommendations,
577
- confidence: Math.round(analysisResult.confidence || 95),
578
- processingTime: parseFloat(processingTime.toFixed(2)),
579
- colorAccuracy: analysisResult.colorAccuracy || '94%',
580
- processingMethod: analysisResult.processingMethod || 'AI Analysis',
581
- colorChannels: analysisResult.colorChannels || {},
582
- timestamp: new Date().toISOString(),
583
- location: {
584
- latitude: parseFloat(latitude) || null,
585
- longitude: parseFloat(longitude) || null
586
- }
587
- });
588
-
589
- } catch (error) {
590
- console.error('❌ Analysis error:', error);
591
- res.status(500).json({
592
- error: 'Analysis failed',
593
- details: error.message,
594
- timestamp: new Date().toISOString()
595
- });
596
- }
597
- });
598
-
599
- // Get water quality map data
600
- app.get('/api/water-map', (req, res) => {
601
- const { lat, lng, radius = 10 } = req.query;
602
-
603
- let query = `SELECT id, latitude, longitude, water_source, overall_quality,
604
- safety_level, timestamp, alerts, ph, chlorine, nitrates
605
- FROM water_tests
606
- WHERE latitude IS NOT NULL AND longitude IS NOT NULL`;
607
-
608
- let params = [];
609
-
610
- if (lat && lng) {
611
- const latRadius = radius / 111;
612
- const lngRadius = radius / (111 * Math.cos(lat * Math.PI / 180));
613
-
614
- query += ` AND latitude BETWEEN ? AND ? AND longitude BETWEEN ? AND ?`;
615
- params = [
616
- parseFloat(lat) - latRadius,
617
- parseFloat(lat) + latRadius,
618
- parseFloat(lng) - lngRadius,
619
- parseFloat(lng) + lngRadius
620
- ];
621
- }
622
-
623
- query += ` ORDER BY timestamp DESC LIMIT 1000`;
624
-
625
- db.all(query, params, (err, rows) => {
626
- if (err) {
627
- res.status(500).json({ error: 'Database error' });
628
- return;
629
- }
630
-
631
- const mapData = rows.map(row => ({
632
- id: row.id,
633
- latitude: row.latitude,
634
- longitude: row.longitude,
635
- waterSource: row.water_source,
636
- quality: row.overall_quality,
637
- safety: row.safety_level,
638
- timestamp: row.timestamp,
639
- alerts: JSON.parse(row.alerts || '[]'),
640
- parameters: {
641
- ph: row.ph,
642
- chlorine: row.chlorine,
643
- nitrates: row.nitrates
644
- }
645
- }));
646
-
647
- res.json({ success: true, data: mapData, count: mapData.length });
648
- });
649
- });
650
-
651
- // --- STATIC FILE SERVING FOR PRODUCTION ---
652
- // This is the crucial part for serving your React app in production.
653
- // It serves the built React app from the 'web/build' folder.
654
- const buildPath = path.join(__dirname, '..', 'web', 'build');
655
- app.use(express.static(buildPath));
656
-
657
- // The "catchall" handler: for any request that doesn't match one of our API routes,
658
- // send back the React app's index.html file. This allows React Router to handle the route.
659
- app.get('*', (req, res) => {
660
- res.sendFile(path.join(buildPath, 'index.html'));
661
- });
662
-
663
- // --- Start Server ---
664
- app.listen(PORT, () => {
665
- console.log(`🌿 GreenPlus by GXS Main Backend Server running on port ${PORT}`);
666
- console.log(`🚀 Serving production React app from: ${buildPath}`);
667
- });
 
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;