Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions parental-control-system/backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
import datetime
import json
import os
from typing import Any, Dict, List

app = Flask(__name__)
CORS(app)

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
FILES = {
"calls": os.path.join(BASE_DIR, "vault_calls.json"),
"messages": os.path.join(BASE_DIR, "vault_messages.json"),
"locations": os.path.join(BASE_DIR, "vault_locations.json"),
"notifications": os.path.join(BASE_DIR, "vault_notifications.json"),
}


def ensure_storage_files() -> None:
for path in FILES.values():
if not os.path.exists(path):
with open(path, "w", encoding="utf-8") as file:
json.dump([], file)


def read_items(path: str) -> List[Dict[str, Any]]:
with open(path, "r", encoding="utf-8") as file:
try:
data = json.load(file)
return data if isinstance(data, list) else []
except json.JSONDecodeError:
return []


def write_items(path: str, items: List[Dict[str, Any]]) -> None:
with open(path, "w", encoding="utf-8") as file:
json.dump(items, file, indent=2, ensure_ascii=False)


def save_data(category: str, payload: Dict[str, Any]) -> None:
path = FILES[category]
current = read_items(path)

record = dict(payload)
record["timestamp"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
current.append(record)

write_items(path, current)


@app.route("/health", methods=["GET"])
def health_check():
return jsonify({"status": "ok"})


@app.route("/api/v1/sync", methods=["POST"])
def sync_data():
payload = request.get_json(silent=True)
if not payload:
return jsonify({"status": "error", "message": "JSON body is required."}), 400

category = payload.get("type")
if category not in FILES:
return jsonify({"status": "error", "message": "Invalid data type."}), 400

save_data(category, payload)
return jsonify({"status": "success"}), 201


@app.route("/api/v1/data/<category>", methods=["GET"])
def get_data(category: str):
if category not in FILES:
return jsonify({"status": "error", "message": "Invalid category."}), 400

return jsonify(read_items(FILES[category]))


if __name__ == "__main__":
ensure_storage_files()
app.run(host="0.0.0.0", port=5000, debug=True)
1 change: 1 addition & 0 deletions parental-control-system/backend/vault_calls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
1 change: 1 addition & 0 deletions parental-control-system/backend/vault_locations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
1 change: 1 addition & 0 deletions parental-control-system/backend/vault_messages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Empty file.
15 changes: 15 additions & 0 deletions parental-control-system/frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "parental-control-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
Comment thread
echska marked this conversation as resolved.
},
"dependencies": {
"leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1"
}
}
7 changes: 7 additions & 0 deletions parental-control-system/frontend/src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Dashboard from "./pages/Dashboard";

function App() {
return <Dashboard />;
}

export default App;
18 changes: 18 additions & 0 deletions parental-control-system/frontend/src/components/AlertsPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function AlertsPanel({ notifications }) {
return (
<div>
<h2 className="text-xl mb-2">🚨 Notifications</h2>
<ul className="space-y-2">
{notifications.map((item, index) => (
<li key={`notif-${index}`} className="bg-gray-700 rounded p-3">
<p className="font-semibold">{item.title || "Alert"}</p>
<p className="text-sm text-gray-200">{item.message || "No details"}</p>
<p className="text-xs text-gray-400 mt-1">{item.timestamp || "-"}</p>
</li>
))}
</ul>
</div>
);
}

export default AlertsPanel;
31 changes: 31 additions & 0 deletions parental-control-system/frontend/src/components/CallsTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
function CallsTable({ calls }) {
return (
<div>
<h2 className="text-xl mb-2">📞 Calls</h2>
<table className="table-auto w-full text-left">
<thead>
<tr>
<th className="px-2 py-1">Device</th>
<th className="px-2 py-1">Number</th>
<th className="px-2 py-1">Duration</th>
<th className="px-2 py-1">Type</th>
<th className="px-2 py-1">Timestamp</th>
</tr>
</thead>
<tbody>
{calls.map((call, index) => (
<tr key={`call-${index}`} className="border-t border-gray-700">
<td className="px-2 py-1">{call.device || "Unknown"}</td>
<td className="px-2 py-1">{call.number || "-"}</td>
<td className="px-2 py-1">{call.duration || "-"}</td>
<td className="px-2 py-1">{call.call_type || "-"}</td>
<td className="px-2 py-1">{call.timestamp || "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

export default CallsTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function InstagramPanel() {
return <div className="text-gray-200">InstagramPanel content will appear here.</div>;
}

export default InstagramPanel;
51 changes: 51 additions & 0 deletions parental-control-system/frontend/src/components/LocationsMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect, useRef } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";

function LocationsMap({ locations }) {
const mapRef = useRef(null);
const containerRef = useRef(null);
const layersRef = useRef([]);

useEffect(() => {
if (!containerRef.current || mapRef.current) return;

mapRef.current = L.map(containerRef.current).setView([36.19, 44.01], 12);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors",
}).addTo(mapRef.current);

return () => {
mapRef.current?.remove();
mapRef.current = null;
};
}, []);

useEffect(() => {
if (!mapRef.current) return;

layersRef.current.forEach((layer) => mapRef.current.removeLayer(layer));
layersRef.current = [];

const points = locations
.filter((loc) => Number.isFinite(Number(loc.lat)) && Number.isFinite(Number(loc.lng)))
.map((loc) => [Number(loc.lat), Number(loc.lng)]);

if (points.length === 0) return;

const polyline = L.polyline(points, { color: "#2563eb" }).addTo(mapRef.current);
const marker = L.marker(points[points.length - 1]).addTo(mapRef.current).bindPopup("Latest location");
mapRef.current.fitBounds(polyline.getBounds(), { padding: [20, 20] });

layersRef.current.push(polyline, marker);
}, [locations]);

return (
<div>
<h2 className="text-xl mb-2">📍 Device Route Timeline</h2>
<div ref={containerRef} className="h-96 rounded-lg shadow-lg" />
</div>
);
}

export default LocationsMap;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function LocationsTimeline({ locations }) {
return (
<div className="bg-gray-800 p-4 rounded-lg shadow-lg mt-6">
<h2 className="text-xl mb-2">🕒 Location Timeline</h2>
<table className="table-auto w-full text-left">
<thead>
<tr>
<th className="px-2 py-1">Device</th>
<th className="px-2 py-1">Latitude</th>
<th className="px-2 py-1">Longitude</th>
<th className="px-2 py-1">Timestamp</th>
</tr>
</thead>
<tbody>
{locations.map((loc, index) => (
<tr key={`${loc.device || "device"}-${index}`} className="border-t border-gray-700">
<td className="px-2 py-1">{loc.device || "Unknown"}</td>
<td className="px-2 py-1">{loc.lat ?? "-"}</td>
<td className="px-2 py-1">{loc.lng ?? "-"}</td>
<td className="px-2 py-1">{loc.timestamp || "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

export default LocationsTimeline;
31 changes: 31 additions & 0 deletions parental-control-system/frontend/src/components/MessagesTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
function MessagesTable({ messages }) {
return (
<div>
<h2 className="text-xl mb-2">💬 Messages</h2>
<table className="table-auto w-full text-left">
<thead>
<tr>
<th className="px-2 py-1">Device</th>
<th className="px-2 py-1">From</th>
<th className="px-2 py-1">To</th>
<th className="px-2 py-1">Content</th>
<th className="px-2 py-1">Timestamp</th>
</tr>
</thead>
<tbody>
{messages.map((msg, index) => (
<tr key={`msg-${index}`} className="border-t border-gray-700">
<td className="px-2 py-1">{msg.device || "Unknown"}</td>
<td className="px-2 py-1">{msg.from || "-"}</td>
<td className="px-2 py-1">{msg.to || "-"}</td>
<td className="px-2 py-1">{msg.content || "-"}</td>
<td className="px-2 py-1">{msg.timestamp || "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

export default MessagesTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function RouterMonitor() {
return <div className="text-gray-200">RouterMonitor content will appear here.</div>;
}

export default RouterMonitor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function WhatsAppPanel() {
return <div className="text-gray-200">WhatsAppPanel content will appear here.</div>;
}

export default WhatsAppPanel;
89 changes: 89 additions & 0 deletions parental-control-system/frontend/src/pages/Dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useEffect, useMemo, useState } from "react";
import CallsTable from "../components/CallsTable";
import MessagesTable from "../components/MessagesTable";
import LocationsMap from "../components/LocationsMap";
import LocationsTimeline from "../components/LocationsTimeline";
import RouterMonitor from "../components/RouterMonitor";
import AlertsPanel from "../components/AlertsPanel";
import WhatsAppPanel from "../components/WhatsAppPanel";
import InstagramPanel from "../components/InstagramPanel";
import { fetchCategory } from "../services/api";

const TABS = ["alerts", "router", "calls", "messages", "locations", "whatsapp", "instagram"];

function Dashboard() {
const [activeTab, setActiveTab] = useState("alerts");
const [calls, setCalls] = useState([]);
const [messages, setMessages] = useState([]);
const [locations, setLocations] = useState([]);
const [notifications, setNotifications] = useState([]);

useEffect(() => {
let intervalId;

const loadData = async () => {
const [c, m, l, n] = await Promise.allSettled([
fetchCategory("calls"),
fetchCategory("messages"),
fetchCategory("locations"),
fetchCategory("notifications"),
]);

setCalls(c.status === "fulfilled" && Array.isArray(c.value) ? c.value : []);
setMessages(m.status === "fulfilled" && Array.isArray(m.value) ? m.value : []);
setLocations(l.status === "fulfilled" && Array.isArray(l.value) ? l.value : []);
setNotifications(n.status === "fulfilled" && Array.isArray(n.value) ? n.value : []);
};

loadData();
intervalId = setInterval(loadData, 10000);

return () => clearInterval(intervalId);
}, []);

const tabContent = useMemo(() => {
switch (activeTab) {
case "alerts":
return <AlertsPanel notifications={notifications} />;
case "router":
return <RouterMonitor />;
case "calls":
return <CallsTable calls={calls} />;
case "messages":
return <MessagesTable messages={messages} />;
case "locations":
return (
<>
<LocationsMap locations={locations} />
<LocationsTimeline locations={locations} />
</>
);
case "whatsapp":
return <WhatsAppPanel />;
case "instagram":
return <InstagramPanel />;
default:
return <AlertsPanel notifications={notifications} />;
}
}, [activeTab, calls, messages, locations, notifications]);

return (
<div className="bg-gray-900 text-white min-h-screen p-6">
<h1 className="text-4xl font-bold mb-6 text-center">🔒 Unified Parental Control Dashboard</h1>
<div className="flex flex-wrap gap-3 mb-6">
{TABS.map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 rounded-lg transition ${activeTab === tab ? "bg-blue-600" : "bg-gray-700"} hover:bg-blue-700`}
>
{tab.toUpperCase()}
</button>
))}
</div>
<div className="bg-gray-800 p-4 rounded-lg shadow-lg">{tabContent}</div>
</div>
);
}

export default Dashboard;
5 changes: 5 additions & 0 deletions parental-control-system/frontend/src/pages/Login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function Login() {
return <div className="p-6">Login page placeholder</div>;
}

export default Login;
Loading