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