diff --git a/Web/frontend/src/App.css b/Web/frontend/src/App.css index f90339d..50f5e16 100644 --- a/Web/frontend/src/App.css +++ b/Web/frontend/src/App.css @@ -182,3 +182,14 @@ border-right-color: var(--border); } } + +.severity-badge { + padding: 4px 12px; + border-radius: 999px; + font-size: 0.85rem; + font-weight: 600; +} +.severity-badge.low { background: #d1fae5; color: #065f46; } +.severity-badge.medium { background: #fef3c7; color: #92400e; } +.severity-badge.high { background: #fee2e2; color: #991b1b; } +.severity-badge.critical { background: #7f1d1d; color: white; } diff --git a/Web/frontend/src/components/ReportCard.jsx b/Web/frontend/src/components/ReportCard.jsx index 9c6a6e1..fb4b6cd 100644 --- a/Web/frontend/src/components/ReportCard.jsx +++ b/Web/frontend/src/components/ReportCard.jsx @@ -22,6 +22,25 @@ function ReportCard({ report, actions }) { + {/* Add this block somewhere visible, e.g. after description */} +
+ {report.severity_label && ( + + Severity: {report.severity_label} ({report.severity_score || "?"}) + + )} + + Escalation Level: {report.escalation_level || 1} + +
+

Reported By

diff --git a/Web/frontend/src/pages/Dashboard.jsx b/Web/frontend/src/pages/Dashboard.jsx index 1e944f9..9300cd5 100644 --- a/Web/frontend/src/pages/Dashboard.jsx +++ b/Web/frontend/src/pages/Dashboard.jsx @@ -1,3 +1,5 @@ +// src/pages/Dashboard.jsx +import { useEffect, useState } from "react"; import { Bar, BarChart, @@ -15,12 +17,18 @@ import MapView from "../components/MapView"; import ReportCard from "../components/ReportCard"; import StatsPanel from "../components/StatsPanel"; import StatusAlert from "../components/StatusAlert"; +import { + fetchReports, + acknowledgeReport, + resolveReport, +} from "../services/api"; + +// Keep your mock data only for charts (you can replace these later with real aggregated data) import { encroachmentTrend, mapMarkers, monthlyMonitoring, portalStats, - reportCards, } from "../data/mockData"; function ChartPanel({ title, subtitle, children }) { @@ -36,6 +44,60 @@ function ChartPanel({ title, subtitle, children }) { } function Dashboard() { + const [reports, setReports] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch real reports from backend + useEffect(() => { + const loadReports = async () => { + try { + setLoading(true); + const data = await fetchReports(); + setReports(data || []); + setError(null); + } catch (err) { + console.error("Failed to load reports:", err); + setError("Failed to load reports from server. Showing demo data."); + // Fallback to mock only if you want (optional) + // setReports(reportCards); + } finally { + setLoading(false); + } + }; + + loadReports(); + + // Optional: refresh every 30 seconds for "live" feel + const interval = setInterval(loadReports, 30000); + return () => clearInterval(interval); + }, []); + + // Handler for Acknowledge button + const handleAcknowledge = async (reportId) => { + try { + await acknowledgeReport(reportId); + // Refresh list after action + const updated = await fetchReports(); + setReports(updated); + } catch (err) { + alert("Failed to acknowledge report"); + console.error(err); + } + }; + + // Handler for Resolve button + const handleResolve = async (reportId) => { + try { + await resolveReport(reportId); + const updated = await fetchReports(); + setReports(updated); + } catch (err) { + alert("Failed to resolve report"); + console.error(err); + } + }; + return (
@@ -59,66 +121,121 @@ function Dashboard() { tone="error" /> -
- -
- - - - - - - - - - - -
-
- - -
- - - - - - - - - - - -
-
-
+ {error && ( + + )} -
-
-
-

Geospatial View

-

Critical encroachment locations

-
- + {loading ? ( +
+
-
-
-

Recent Cases

-

Latest report activity

-
- {reportCards.slice(0, 2).map((report) => ( - - ))} -
-
+ ) : ( + <> +
+ +
+ + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + + +
+
+
+ +
+
+
+

Geospatial View

+

Critical encroachment locations

+
+ +
+ +
+
+

Recent Cases

+

Latest report activity

+
+ + {reports.length === 0 ? ( +
+ No recent reports yet. Submit one to see live data. +
+ ) : ( + reports.slice(0, 4).map((report) => ( // show latest 4 + + + +
+ ) + } + /> + )) + )} +
+ + + )}
); } -export default Dashboard; +export default Dashboard; \ No newline at end of file diff --git a/Web/frontend/src/services/api.js b/Web/frontend/src/services/api.js index 851d375..d13b951 100644 --- a/Web/frontend/src/services/api.js +++ b/Web/frontend/src/services/api.js @@ -3,6 +3,17 @@ import axios from "axios"; const api = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || "http://localhost:5000/api", timeout: 10000, + headers: { + "Content-Type": "application/json", + }, +}); + +api.interceptors.request.use((config) => { + const token = localStorage.getItem("supabase.auth.token"); // or from your auth context + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; }); export async function fetchReports() { @@ -15,6 +26,16 @@ export async function submitEncroachmentReport(payload) { return data; } +export const acknowledgeReport = async (id) => { + const { data } = await api.put(`/api/reports/${id}/acknowledge`); + return data; +}; + +export const resolveReport = async (id) => { + const { data } = await api.put(`/api/reports/${id}/resolve`); + return data; +}; + export async function fetchWaterbodies() { const { data } = await api.get("/waterbodies"); return data; diff --git a/backend/controllers/reportsController.js b/backend/controllers/reportsController.js index 93da2f0..867320d 100644 --- a/backend/controllers/reportsController.js +++ b/backend/controllers/reportsController.js @@ -1,5 +1,9 @@ +// backend/controllers/reportsController.js const { supabaseAdmin } = require("../database/supabase"); const { sendNotification } = require("../services/notifications"); +const { calculateSeverity } = require("../services/aiSeverity"); // ← AI added +const { sendInitialAlert } = require("../escalation"); // ← escalation added + async function getReports(_req, res, next) { try { @@ -35,6 +39,9 @@ async function createReport(req, res, next) { }); } + // 🔥 AI Severity Scoring + const severity = calculateSeverity({ type: title, source, confidence: 75 }); + const payload = { title, description, @@ -46,18 +53,58 @@ async function createReport(req, res, next) { user_id: userId || null, source, status: "pending", + severity_score: severity.score, + severity_label: severity.label, + escalation_level: 1, }; const { data, error } = await supabaseAdmin.from("reports").insert(payload).select("*").single(); if (error) throw error; + // Send immediate alert + start escalation timer + await sendInitialAlert(data); await sendNotification({ - title: "New ArenIQ Encroachment Report", - message: `${data.title} reported at ${data.location_name || `${data.latitude}, ${data.longitude}`}`, - priority: "high", + title: `New Report • ${severity.label} Severity`, + message: `${data.title} at ${data.location_name || `${data.latitude}, ${data.longitude}`}`, + priority: severity.score > 70 ? "urgent" : "high", }); - res.status(201).json(data); + res.status(201).json({ ...data, severity }); + } catch (error) { + next(error); + } +} + +// NEW: Officer actions (used by your dashboard buttons) +async function acknowledgeReport(req, res, next) { + try { + const { id } = req.params; + const { data, error } = await supabaseAdmin + .from("reports") + .update({ status: "acknowledged", acknowledged_at: new Date().toISOString() }) + .eq("id", id) + .select() + .single(); + + if (error) throw error; + res.json({ message: "Acknowledged – escalation paused", report: data }); + } catch (error) { + next(error); + } +} + +async function resolveReport(req, res, next) { + try { + const { id } = req.params; + const { data, error } = await supabaseAdmin + .from("reports") + .update({ status: "resolved", resolved_at: new Date().toISOString() }) + .eq("id", id) + .select() + .single(); + + if (error) throw error; + res.json({ message: "Report resolved", report: data }); } catch (error) { next(error); } @@ -66,4 +113,6 @@ async function createReport(req, res, next) { module.exports = { createReport, getReports, -}; + acknowledgeReport, // ← new + resolveReport, // ← new +}; \ No newline at end of file diff --git a/backend/escalation.js b/backend/escalation.js index 75c1845..d9a18e8 100644 --- a/backend/escalation.js +++ b/backend/escalation.js @@ -18,14 +18,14 @@ require('dotenv').config(); console.log('=== ENV DEBUG ==='); console.log('SUPABASE_URL :', process.env.SUPABASE_URL); -console.log('SUPABASE_SERVICE_ROLE :', process.env.SUPABASE_SERVICE_ROLE); +console.log('SUPABASE_SERVICE_ROLE :', process.env.SUPABASE_SERVICE_ROLE_KEY); console.log('PORT :', process.env.PORT); console.log('=== END DEBUG ==='); const { createClient } = require('@supabase/supabase-js'); const supabase = createClient( process.env.SUPABASE_URL, - process.env.SUPABASE_SERVICE_ROLE // Use service key for backend operations + process.env.SUPABASE_SERVICE_ROLE_KEY // Use service key for backend operations ); // ───────────────────────────────────────────── diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 0000000..a95b1e6 --- /dev/null +++ b/backend/middleware/auth.js @@ -0,0 +1,24 @@ +// backend/middleware/auth.js +const { supabaseAdmin } = require('../database/supabase'); + +/** + * Protects officer-only routes using Supabase JWT + * Frontend already sends Authorization: Bearer + */ +async function requireOfficer(req, res, next) { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'No token provided' }); + + try { + const { data: { user }, error } = await supabaseAdmin.auth.getUser(token); + if (error || !user) throw new Error('Invalid token'); + + // Optional: check role from users table (you can extend later) + req.user = user; + next(); + } catch (err) { + res.status(403).json({ error: 'Unauthorized – officer access only' }); + } +} + +module.exports = { requireOfficer }; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 6c7e7fb..e749b87 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,7 +12,10 @@ "@supabase/supabase-js": "^2.99.0", "cors": "^2.8.6", "dotenv": "^17.3.1", - "express": "^5.2.1" + "express": "^5.2.1", + "express-rate-limit": "^8.3.1", + "helmet": "^8.1.0", + "node-fetch": "^2.7.0" } }, "node_modules/@supabase/auth-js": { @@ -406,6 +409,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -527,6 +548,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -578,6 +608,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -663,6 +702,26 @@ "node": ">= 0.6" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -939,6 +998,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -983,6 +1048,22 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index c924621..e466e8d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,6 +16,9 @@ "@supabase/supabase-js": "^2.99.0", "cors": "^2.8.6", "dotenv": "^17.3.1", - "express": "^5.2.1" + "express": "^5.2.1", + "express-rate-limit": "^8.3.1", + "helmet": "^8.1.0", + "node-fetch": "^2.7.0" } } diff --git a/backend/routes/reports.js b/backend/routes/reports.js index 1d7f451..86cc91d 100644 --- a/backend/routes/reports.js +++ b/backend/routes/reports.js @@ -1,9 +1,14 @@ const express = require("express"); -const { createReport, getReports } = require("../controllers/reportsController"); +const { createReport, getReports, acknowledgeReport, resolveReport } = require("../controllers/reportsController"); +const { requireOfficer } = require("../middleware/auth"); // ← protection const router = express.Router(); router.get("/reports", getReports); router.post("/report", createReport); -module.exports = router; +// Officer-only actions (protected) +router.put("/reports/:id/acknowledge", requireOfficer, acknowledgeReport); +router.put("/reports/:id/resolve", requireOfficer, resolveReport); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index ad06fde..ff6c96c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -8,9 +8,14 @@ const uploadRouter = require("./routes/upload"); const waterbodiesRouter = require("./routes/waterbodies"); const satelliteRouter = require("./routes/satellite"); const weatherRouter = require("./routes/weather"); +const helmet = require("helmet"); +const rateLimit = require("express-rate-limit"); +const { startEscalationScheduler } = require("./escalation"); const app = express(); +app.use(helmet()); + app.use( cors({ origin: process.env.FRONTEND_URL || "http://localhost:5173", @@ -41,7 +46,24 @@ app.use((error, _req, res, _next) => { }); }); + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + message: { error: "Too many requests – please try again later" } +}); +app.use(limiter); + +// START ESCALATION ENGINE +startEscalationScheduler(); + const PORT = process.env.PORT || 5000; +app.listen(PORT, () => { + console.log(`[ArenIQ] Backend listening on http://localhost:${PORT}`); + console.log(`[✓] Escalation scheduler running (every hour)`); + console.log(`[✓] Helmet + Rate limiting enabled (production mode)`); +}); + app.listen(PORT, () => { console.log(`[ArenIQ] Backend listening on http://localhost:${PORT}`); diff --git a/backend/services/aiSeverity.js b/backend/services/aiSeverity.js new file mode 100644 index 0000000..8ad494b --- /dev/null +++ b/backend/services/aiSeverity.js @@ -0,0 +1,39 @@ +// backend/aiSeverity.js +/** + * Very simple rule-based + weighted scoring for demo "AI severity" + * Returns score 0–100 and label + */ +function calculateSeverity(report) { + let score = 0; + + // Source multiplier + if (report.source === 'satellite') score += 35; + if (report.source === 'citizen') score += 20; + + // Type severity + const typeScores = { + construction: 40, + 'sand mining': 55, + 'waste dumping':35, + 'land filling': 45, + other: 25 + }; + score += typeScores[report.type?.toLowerCase()] || 20; + + // Confidence from ML (if exists) + if (report.confidence && report.confidence > 70) score += 15; + + // Area / impact proxy + if (report.area_px && report.area_px > 500) score += 10; + + score = Math.min(100, Math.max(0, Math.round(score))); + + let label = 'Low'; + if (score >= 75) label = 'Critical'; + else if (score >= 50) label = 'High'; + else if (score >= 30) label = 'Medium'; + + return { score, label }; +} + +module.exports = { calculateSeverity }; \ No newline at end of file