diff --git a/backend/cd b/backend/cd new file mode 100644 index 0000000..e69de29 diff --git a/backend/package-lock.json b/backend/package-lock.json index dcdaff1..7160d5a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "axios": "^1.13.6", "bcryptjs": "^3.0.3", + "compression": "^1.8.1", "cors": "^2.8.6", "dns": "^0.2.2", "dotenv": "^17.4.2", @@ -478,6 +479,60 @@ "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", "integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==" }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -2162,6 +2217,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/backend/package.json b/backend/package.json index 3485f34..b2f18c3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,7 @@ "dependencies": { "axios": "^1.13.6", "bcryptjs": "^3.0.3", + "compression": "^1.8.1", "cors": "^2.8.6", "dns": "^0.2.2", "dotenv": "^17.4.2", diff --git a/backend/routes/historyRoutes.js b/backend/routes/historyRoutes.js index b75a5b3..c2544f1 100644 --- a/backend/routes/historyRoutes.js +++ b/backend/routes/historyRoutes.js @@ -15,6 +15,37 @@ router.get("/", protect, getHistory); // Delete one history item router.delete("/:id", protect, deleteHistoryItem); +// Bulk delete history items +router.delete("/bulk-delete", protect, async (req, res) => { + try { + const { ids } = req.body; // Expecting an array of IDs in the request body + + if (!ids || !Array.isArray(ids) || ids.length === 0) { + return res.status(400).json({ + success: false, + message: "Invalid request. 'ids' must be a non-empty array." + }); + } + + const result = await History.deleteMany({ + _id: { $in: ids }, + user: req.user.id + }); + + res.json({ + success: true, + deletedCount: result.deletedCount, + message: `${result.deletedCount} items deleted successfully` + }); + + } catch (error) { + res.status(500).json({ + success: false, + message: error.message + }); + } +}); + // Clear all history router.delete("/", protect, clearHistory); diff --git a/backend/server.js b/backend/server.js index 2187d89..b1df93e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -8,6 +8,7 @@ const express = require("express"); const seedAdminUser = require("./seeders/adminSeeder"); const { getHealthStatus } = require('./utils/healthCheck'); const cors = require("cors"); +const compression = require('compression'); const { v4: uuidv4 } = require('uuid'); const axios = require("axios"); @@ -61,10 +62,33 @@ const connectWithRetry = async (retries=5, delay=5000) => { } }; +if(process.env.NODE_ENV === 'development'){ + //Log all queries in development mode + mongoose.set('debug',true); +} else { + // Log only slow queries in production mode + const originalExec = mongoose.Query.prototype.exec; + mongoose.Query.prototype.exec = async function() { + const start = Date.now(); + const result = await originalExec.apply(this, arguments); + const duration = Date.now() - start; + + if(duration > 100){ // Log queries taking longer than 100ms + console.log(`🐢 [${new Date().toISOString()}] Slow Query (${duration}ms):`); + console.log(` Collection: ${this._collection.collectionName}`); + console.log(` Query:`, JSON.stringify(this._conditions)); + } + + return result; + }; +} + // Start connection with retry connectWithRetry(); app.use(cors()); +app.use(compression()); +app.use(express.json()); app.use(express.json({limit: '1mb'})); app.use(express.urlencoded({ extended: true, limit: '1mb' })); app.get('/health', (req, res) => { diff --git a/frontend/src/components/History.jsx b/frontend/src/components/History.jsx index 3705d3f..8cd576a 100644 --- a/frontend/src/components/History.jsx +++ b/frontend/src/components/History.jsx @@ -1,116 +1,106 @@ -import { useEffect, useState } from "react"; -import api from "../utils/axiosInstance"; - -function History({ darkMode }) { - const [history, setHistory] = useState([]); - const [loading, setLoading] = useState(true); - - const fetchHistory = async () => { - try { - const res = await api.get("/api/history"); - setHistory(res.data); - } catch (err) { - console.error("Failed to load history:", err); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchHistory(); - }, []); - - const deleteItem = async (id) => { - try { - await api.delete(`/api/history/${id}`); - - setHistory((prev) => - prev.filter((item) => item._id !== id) - ); - } catch (err) { - console.error(err); - } - }; - - const clearAll = async () => { - try { - await api.delete("/api/history"); - - setHistory([]); - } catch (err) { - console.error(err); - } - }; - - if (loading) { - return

Loading history...

; - } - - return ( -
-
-

- 📜 History -

- - {history.length > 0 && ( - - )} -
- - {history.length === 0 ? ( -

No history found.

- ) : ( -
- {history.map((item) => ( -
-

- {item.query} -

- -

- Result:{" "} - - {item.prediction} - -

- -

- Type: {item.type} -

- -

- {new Date( - item.createdAt - ).toLocaleString()} -

- - -
- ))} +import { useState, useEffect } from 'react'; +import axios from 'axios'; + +const History = () => { + const [history, setHistory] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); + + const fetchHistory = async () => { + try { + const token = localStorage.getItem('token'); + const res = await axios.get('/api/history', { + headers: { Authorization: `Bearer ${token}` } + }); + setHistory(res.data.data || []); + } catch (error) { + console.error('Error fetching history:', error); + } + }; + + useEffect(() => { + fetchHistory(); + }, []); + + const handleBulkDelete = async () => { + if (!confirm(`Delete ${selectedItems.length} item(s)?`)) return; + + try { + const token = localStorage.getItem('token'); + await axios.delete('/api/history/bulk-delete', { + headers: { Authorization: `Bearer ${token}` }, + data: { ids: selectedItems } + }); + setSelectedItems([]); + fetchHistory(); + } catch (error) { + alert('Failed to delete items'); + } + }; + + const toggleSelect = (id) => { + setSelectedItems(prev => + prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] + ); + }; + + return ( +
+

History

+ + {selectedItems.length > 0 && ( + + )} + + {history.length === 0 ? ( +

No history found.

+ ) : ( + history.map(item => ( +
+ toggleSelect(item._id)} + /> + {item.query} + + {item.prediction} + +
+ )) + )}
- )} -
- ); -} + ); +}; export default History; \ No newline at end of file