From ef3388ab48fc92b75554440cf2cc647c28d85445 Mon Sep 17 00:00:00 2001 From: echska Date: Sat, 9 May 2026 14:27:21 +0300 Subject: [PATCH 1/2] Align Render frontend API env with deployed backend URL --- parental-control-system/backend/Dockerfile | 7 ++ parental-control-system/backend/main.py | 81 +++++++++++++++++ .../backend/requirements.txt | 3 + .../backend/vault_calls.json | 1 + .../backend/vault_locations.json | 1 + .../backend/vault_messages.json | 1 + .../backend/vault_notifications.json | 0 parental-control-system/frontend/package.json | 15 ++++ parental-control-system/frontend/src/App.js | 7 ++ .../frontend/src/components/AlertsPanel.js | 18 ++++ .../frontend/src/components/CallsTable.js | 31 +++++++ .../frontend/src/components/InstagramPanel.js | 5 ++ .../frontend/src/components/LocationsMap.js | 51 +++++++++++ .../src/components/LocationsTimeline.js | 29 ++++++ .../frontend/src/components/MessagesTable.js | 31 +++++++ .../frontend/src/components/RouterMonitor.js | 5 ++ .../frontend/src/components/WhatsAppPanel.js | 5 ++ .../frontend/src/pages/Dashboard.js | 89 +++++++++++++++++++ .../frontend/src/pages/Login.js | 5 ++ .../frontend/src/services/api.js | 25 ++++++ .../frontend/tailwind.config.js | 7 ++ render.yaml | 29 ++++++ 22 files changed, 446 insertions(+) create mode 100644 parental-control-system/backend/Dockerfile create mode 100644 parental-control-system/backend/main.py create mode 100644 parental-control-system/backend/requirements.txt create mode 100644 parental-control-system/backend/vault_calls.json create mode 100644 parental-control-system/backend/vault_locations.json create mode 100644 parental-control-system/backend/vault_messages.json create mode 100644 parental-control-system/backend/vault_notifications.json create mode 100644 parental-control-system/frontend/package.json create mode 100644 parental-control-system/frontend/src/App.js create mode 100644 parental-control-system/frontend/src/components/AlertsPanel.js create mode 100644 parental-control-system/frontend/src/components/CallsTable.js create mode 100644 parental-control-system/frontend/src/components/InstagramPanel.js create mode 100644 parental-control-system/frontend/src/components/LocationsMap.js create mode 100644 parental-control-system/frontend/src/components/LocationsTimeline.js create mode 100644 parental-control-system/frontend/src/components/MessagesTable.js create mode 100644 parental-control-system/frontend/src/components/RouterMonitor.js create mode 100644 parental-control-system/frontend/src/components/WhatsAppPanel.js create mode 100644 parental-control-system/frontend/src/pages/Dashboard.js create mode 100644 parental-control-system/frontend/src/pages/Login.js create mode 100644 parental-control-system/frontend/src/services/api.js create mode 100644 parental-control-system/frontend/tailwind.config.js create mode 100644 render.yaml diff --git a/parental-control-system/backend/Dockerfile b/parental-control-system/backend/Dockerfile new file mode 100644 index 0000000..ed2e106 --- /dev/null +++ b/parental-control-system/backend/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.12-slim +WORKDIR /app +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 5000 +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "main:app", "--workers", "2", "--threads", "4", "--timeout", "60"] diff --git a/parental-control-system/backend/main.py b/parental-control-system/backend/main.py new file mode 100644 index 0000000..9c78750 --- /dev/null +++ b/parental-control-system/backend/main.py @@ -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/", 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) diff --git a/parental-control-system/backend/requirements.txt b/parental-control-system/backend/requirements.txt new file mode 100644 index 0000000..4fb2116 --- /dev/null +++ b/parental-control-system/backend/requirements.txt @@ -0,0 +1,3 @@ +flask==3.0.3 +flask-cors==4.0.1 +gunicorn==22.0.0 diff --git a/parental-control-system/backend/vault_calls.json b/parental-control-system/backend/vault_calls.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/parental-control-system/backend/vault_calls.json @@ -0,0 +1 @@ +[] diff --git a/parental-control-system/backend/vault_locations.json b/parental-control-system/backend/vault_locations.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/parental-control-system/backend/vault_locations.json @@ -0,0 +1 @@ +[] diff --git a/parental-control-system/backend/vault_messages.json b/parental-control-system/backend/vault_messages.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/parental-control-system/backend/vault_messages.json @@ -0,0 +1 @@ +[] diff --git a/parental-control-system/backend/vault_notifications.json b/parental-control-system/backend/vault_notifications.json new file mode 100644 index 0000000..e69de29 diff --git a/parental-control-system/frontend/package.json b/parental-control-system/frontend/package.json new file mode 100644 index 0000000..f96489f --- /dev/null +++ b/parental-control-system/frontend/package.json @@ -0,0 +1,15 @@ +{ + "name": "parental-control-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build" + }, + "dependencies": { + "leaflet": "^1.9.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1" + } +} diff --git a/parental-control-system/frontend/src/App.js b/parental-control-system/frontend/src/App.js new file mode 100644 index 0000000..8108ac8 --- /dev/null +++ b/parental-control-system/frontend/src/App.js @@ -0,0 +1,7 @@ +import Dashboard from "./pages/Dashboard"; + +function App() { + return ; +} + +export default App; diff --git a/parental-control-system/frontend/src/components/AlertsPanel.js b/parental-control-system/frontend/src/components/AlertsPanel.js new file mode 100644 index 0000000..d49d2a6 --- /dev/null +++ b/parental-control-system/frontend/src/components/AlertsPanel.js @@ -0,0 +1,18 @@ +function AlertsPanel({ notifications }) { + return ( +
+

🚨 Notifications

+
    + {notifications.map((item, index) => ( +
  • +

    {item.title || "Alert"}

    +

    {item.message || "No details"}

    +

    {item.timestamp || "-"}

    +
  • + ))} +
+
+ ); +} + +export default AlertsPanel; diff --git a/parental-control-system/frontend/src/components/CallsTable.js b/parental-control-system/frontend/src/components/CallsTable.js new file mode 100644 index 0000000..c1530fe --- /dev/null +++ b/parental-control-system/frontend/src/components/CallsTable.js @@ -0,0 +1,31 @@ +function CallsTable({ calls }) { + return ( +
+

📞 Calls

+ + + + + + + + + + + + {calls.map((call, index) => ( + + + + + + + + ))} + +
DeviceNumberDurationTypeTimestamp
{call.device || "Unknown"}{call.number || "-"}{call.duration || "-"}{call.call_type || "-"}{call.timestamp || "-"}
+
+ ); +} + +export default CallsTable; diff --git a/parental-control-system/frontend/src/components/InstagramPanel.js b/parental-control-system/frontend/src/components/InstagramPanel.js new file mode 100644 index 0000000..d705913 --- /dev/null +++ b/parental-control-system/frontend/src/components/InstagramPanel.js @@ -0,0 +1,5 @@ +function InstagramPanel() { + return
InstagramPanel content will appear here.
; +} + +export default InstagramPanel; diff --git a/parental-control-system/frontend/src/components/LocationsMap.js b/parental-control-system/frontend/src/components/LocationsMap.js new file mode 100644 index 0000000..7cba167 --- /dev/null +++ b/parental-control-system/frontend/src/components/LocationsMap.js @@ -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 ( +
+

📍 Device Route Timeline

+
+
+ ); +} + +export default LocationsMap; diff --git a/parental-control-system/frontend/src/components/LocationsTimeline.js b/parental-control-system/frontend/src/components/LocationsTimeline.js new file mode 100644 index 0000000..dce5280 --- /dev/null +++ b/parental-control-system/frontend/src/components/LocationsTimeline.js @@ -0,0 +1,29 @@ +function LocationsTimeline({ locations }) { + return ( +
+

🕒 Location Timeline

+ + + + + + + + + + + {locations.map((loc, index) => ( + + + + + + + ))} + +
DeviceLatitudeLongitudeTimestamp
{loc.device || "Unknown"}{loc.lat ?? "-"}{loc.lng ?? "-"}{loc.timestamp || "-"}
+
+ ); +} + +export default LocationsTimeline; diff --git a/parental-control-system/frontend/src/components/MessagesTable.js b/parental-control-system/frontend/src/components/MessagesTable.js new file mode 100644 index 0000000..8e8237d --- /dev/null +++ b/parental-control-system/frontend/src/components/MessagesTable.js @@ -0,0 +1,31 @@ +function MessagesTable({ messages }) { + return ( +
+

💬 Messages

+ + + + + + + + + + + + {messages.map((msg, index) => ( + + + + + + + + ))} + +
DeviceFromToContentTimestamp
{msg.device || "Unknown"}{msg.from || "-"}{msg.to || "-"}{msg.content || "-"}{msg.timestamp || "-"}
+
+ ); +} + +export default MessagesTable; diff --git a/parental-control-system/frontend/src/components/RouterMonitor.js b/parental-control-system/frontend/src/components/RouterMonitor.js new file mode 100644 index 0000000..aadec4c --- /dev/null +++ b/parental-control-system/frontend/src/components/RouterMonitor.js @@ -0,0 +1,5 @@ +function RouterMonitor() { + return
RouterMonitor content will appear here.
; +} + +export default RouterMonitor; diff --git a/parental-control-system/frontend/src/components/WhatsAppPanel.js b/parental-control-system/frontend/src/components/WhatsAppPanel.js new file mode 100644 index 0000000..cbdf535 --- /dev/null +++ b/parental-control-system/frontend/src/components/WhatsAppPanel.js @@ -0,0 +1,5 @@ +function WhatsAppPanel() { + return
WhatsAppPanel content will appear here.
; +} + +export default WhatsAppPanel; diff --git a/parental-control-system/frontend/src/pages/Dashboard.js b/parental-control-system/frontend/src/pages/Dashboard.js new file mode 100644 index 0000000..1292b7e --- /dev/null +++ b/parental-control-system/frontend/src/pages/Dashboard.js @@ -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 ; + case "router": + return ; + case "calls": + return ; + case "messages": + return ; + case "locations": + return ( + <> + + + + ); + case "whatsapp": + return ; + case "instagram": + return ; + default: + return ; + } + }, [activeTab, calls, messages, locations, notifications]); + + return ( +
+

🔒 Unified Parental Control Dashboard

+
+ {TABS.map((tab) => ( + + ))} +
+
{tabContent}
+
+ ); +} + +export default Dashboard; diff --git a/parental-control-system/frontend/src/pages/Login.js b/parental-control-system/frontend/src/pages/Login.js new file mode 100644 index 0000000..3ae22ec --- /dev/null +++ b/parental-control-system/frontend/src/pages/Login.js @@ -0,0 +1,5 @@ +function Login() { + return
Login page placeholder
; +} + +export default Login; diff --git a/parental-control-system/frontend/src/services/api.js b/parental-control-system/frontend/src/services/api.js new file mode 100644 index 0000000..edda4b8 --- /dev/null +++ b/parental-control-system/frontend/src/services/api.js @@ -0,0 +1,25 @@ +const API_BASE_URL = + process.env.REACT_APP_API_BASE_URL || + "https://parental-control-backend-wib3.onrender.com/api/v1"; + +export async function fetchCategory(category) { + const response = await fetch(`${API_BASE_URL}/data/${category}`); + if (!response.ok) { + throw new Error(`Failed to fetch ${category}`); + } + return response.json(); +} + +export async function syncCategory(payload) { + const response = await fetch(`${API_BASE_URL}/sync`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`Failed to sync payload`); + } + + return response.json(); +} diff --git a/parental-control-system/frontend/tailwind.config.js b/parental-control-system/frontend/tailwind.config.js new file mode 100644 index 0000000..3b9aaf4 --- /dev/null +++ b/parental-control-system/frontend/tailwind.config.js @@ -0,0 +1,7 @@ +module.exports = { + content: ["./src/**/*.{js,jsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..05286ce --- /dev/null +++ b/render.yaml @@ -0,0 +1,29 @@ +previews: + generation: automatic + +services: + - type: web + name: parental-control-backend + runtime: docker + rootDir: parental-control-system/backend + plan: starter + autoDeploy: true + healthCheckPath: /health + envVars: + - key: PYTHONUNBUFFERED + value: "1" + + - type: web + name: parental-control-frontend + runtime: static + rootDir: parental-control-system/frontend + buildCommand: npm install && npm run build + staticPublishPath: build + pullRequestPreviewsEnabled: true + envVars: + - key: REACT_APP_API_BASE_URL + value: https://parental-control-backend-wib3.onrender.com/api/v1 + routes: + - type: rewrite + source: /* + destination: /index.html From fc2b1a155108bb739ed17a910648ade119f55799 Mon Sep 17 00:00:00 2001 From: echska Date: Sat, 9 May 2026 15:40:09 +0300 Subject: [PATCH 2/2] Harden backend validation and production runtime defaults --- .gitignore | 2 ++ parental-control-system/backend/main.py | 36 ++++++++++++++++++++----- 2 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b02f9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +parental-control-system/frontend/node_modules/ diff --git a/parental-control-system/backend/main.py b/parental-control-system/backend/main.py index 9c78750..8dc9af6 100644 --- a/parental-control-system/backend/main.py +++ b/parental-control-system/backend/main.py @@ -9,6 +9,7 @@ CORS(app) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +MAX_ITEMS_PER_CATEGORY = 5000 FILES = { "calls": os.path.join(BASE_DIR, "vault_calls.json"), "messages": os.path.join(BASE_DIR, "vault_messages.json"), @@ -38,7 +39,20 @@ def write_items(path: str, items: List[Dict[str, Any]]) -> None: json.dump(items, file, indent=2, ensure_ascii=False) -def save_data(category: str, payload: Dict[str, Any]) -> None: +def validate_payload(category: str, payload: Dict[str, Any]) -> str | None: + required = { + "calls": ["number"], + "messages": ["content"], + "locations": ["lat", "lng"], + "notifications": ["message"], + } + missing = [field for field in required[category] if field not in payload] + if missing: + return f"Missing required fields: {', '.join(missing)}" + return None + + +def save_data(category: str, payload: Dict[str, Any]) -> Dict[str, Any]: path = FILES[category] current = read_items(path) @@ -46,12 +60,19 @@ def save_data(category: str, payload: Dict[str, Any]) -> None: record["timestamp"] = datetime.datetime.now(datetime.timezone.utc).isoformat() current.append(record) + if len(current) > MAX_ITEMS_PER_CATEGORY: + current = current[-MAX_ITEMS_PER_CATEGORY:] + write_items(path, current) + return record + + +ensure_storage_files() @app.route("/health", methods=["GET"]) def health_check(): - return jsonify({"status": "ok"}) + return jsonify({"status": "ok", "service": "parental-control-backend"}) @app.route("/api/v1/sync", methods=["POST"]) @@ -64,8 +85,12 @@ def sync_data(): if category not in FILES: return jsonify({"status": "error", "message": "Invalid data type."}), 400 - save_data(category, payload) - return jsonify({"status": "success"}), 201 + validation_error = validate_payload(category, payload) + if validation_error: + return jsonify({"status": "error", "message": validation_error}), 400 + + record = save_data(category, payload) + return jsonify({"status": "success", "record": record}), 201 @app.route("/api/v1/data/", methods=["GET"]) @@ -77,5 +102,4 @@ def get_data(category: str): if __name__ == "__main__": - ensure_storage_files() - app.run(host="0.0.0.0", port=5000, debug=True) + app.run(host="0.0.0.0", port=5000, debug=False)