From 416d52892343585653cc37897a3a423b55c8b738 Mon Sep 17 00:00:00 2001 From: krishnaGoutam Date: Tue, 18 Feb 2025 11:31:44 +0530 Subject: [PATCH 1/5] Added Tracker Tool --- images/tracker.png | Bin 0 -> 25827 bytes src/frontEnd/Application.py | 182 +++++++++++- src/frontEnd/TrackerTool/app.py | 291 +++++++++++++++++++ src/frontEnd/TrackerTool/main.py | 416 ++++++++++++++++++++++++++++ src/frontEnd/TrackerTool/tracker.py | 142 ++++++++++ src/frontEnd/Workspace.py | 2 + src/frontEnd/tracker.py | 124 +++++++++ 7 files changed, 1154 insertions(+), 3 deletions(-) create mode 100644 images/tracker.png create mode 100644 src/frontEnd/TrackerTool/app.py create mode 100644 src/frontEnd/TrackerTool/main.py create mode 100644 src/frontEnd/TrackerTool/tracker.py create mode 100644 src/frontEnd/tracker.py diff --git a/images/tracker.png b/images/tracker.png new file mode 100644 index 0000000000000000000000000000000000000000..3d772fa4ce30f02e4478dd95aa4eba39743eec29 GIT binary patch literal 25827 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 Kl`0i1!u|&wl{Ak4 literal 0 HcmV?d00001 diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 5d76bf9d7..1dc10dc35 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -34,16 +34,94 @@ from configuration.Appconfig import Appconfig from frontEnd import ProjectExplorer from frontEnd import Workspace -from frontEnd import DockArea +from frontEnd import DockArea,tracker from projManagement.openProject import OpenProjectInfo from projManagement.newProject import NewProjectInfo from projManagement.Kicad import Kicad from projManagement.Validation import Validation from projManagement import Worker +import subprocess,json,sys,logging +from datetime import datetime +from multiprocessing import Process + + + +LOG_DIR = "logs" +if not os.path.exists(LOG_DIR): + os.makedirs(LOG_DIR) + +def log_capture(user_id): + log_file_path = os.path.join(LOG_DIR, f"{user_id}_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt") + + # Set up logging to capture only warnings and errors + logging.basicConfig(filename=log_file_path, + level=logging.WARNING, # Log WARNING, ERROR, and CRITICAL only + format="%(asctime)s - %(levelname)s - %(message)s") + + # Redirect stdout and stderr to log file + sys.stdout = sys.stderr = open(log_file_path, 'a') + print("Log capturing started...") + + return log_file_path + + +class UserPreferenceDialog(QtWidgets.QDialog): + """Dialog to ask the user for session tracking preferences.""" + def __init__(self, parent=None): + super().__init__(parent) + + self.setWindowTitle("Session Tracking") + self.setFixedSize(400, 250) # Set dialog size + + layout = QtWidgets.QVBoxLayout() + + # Display username (Read-only) + username = tracker.generate_username() # Replace with TrackerTool.generate_username() + self.name_label = QtWidgets.QLabel(f"👤 Username: {username}") + layout.addWidget(self.name_label) + + # Session tracking option + self.track_checkbox = QtWidgets.QCheckBox("Track this session") + layout.addWidget(self.track_checkbox) + + # Remember choice option + self.remember_checkbox = QtWidgets.QCheckBox("Remember my choice") + layout.addWidget(self.remember_checkbox) + + # Buttons + self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + layout.addWidget(self.button_box) + + # Info Icon at the bottom right + info_layout = QtWidgets.QHBoxLayout() + info_layout.addStretch() + + self.info_icon = QtWidgets.QLabel() + self.info_icon.setPixmap(self.style().standardIcon(QtWidgets.QStyle.SP_MessageBoxInformation).pixmap(24, 24)) + self.info_icon.setToolTip("Tracking will log session start/end time and activity data.") + self.info_icon.mousePressEvent = self.show_info # Make icon clickable + info_layout.addWidget(self.info_icon) + + layout.addLayout(info_layout) + self.setLayout(layout) + + def show_info(self, event): + """Show detailed information when the icon is clicked.""" + QtWidgets.QMessageBox.information(self, "Session Tracking Info", + "This feature logs your session start and end time, along with activity data. " + "You can disable tracking at any time in settings.""You can go and delete the unwanted sessions details from the tracker Tool bar.") + + def getPreferences(self): + """Return user preferences.""" + return { + "username":tracker.generate_username(), # Fetch username dynamically + "track_session": self.track_checkbox.isChecked(), + "remember_choice": self.remember_checkbox.isChecked(), + } # Its our main window of application. - - class Application(QtWidgets.QMainWindow): """This class initializes all objects used in this file.""" global project_name @@ -129,12 +207,20 @@ def initToolBar(self): self.helpfile.setShortcut('Ctrl+H') self.helpfile.triggered.connect(self.help_project) + self.trackerTool = QtWidgets.QAction( + QtGui.QIcon(init_path + 'images/tracker.png'), + 'Tracker Tool', self + ) + self.trackerTool.setShortcut('Ctrl+H') + self.trackerTool.triggered.connect(self.tracker_tool) + self.topToolbar = self.addToolBar('Top Tool Bar') self.topToolbar.addAction(self.newproj) self.topToolbar.addAction(self.openproj) self.topToolbar.addAction(self.closeproj) self.topToolbar.addAction(self.wrkspce) self.topToolbar.addAction(self.helpfile) + self.topToolbar.addAction(self.trackerTool) # ## This part is meant for SoC Generation which is currently ## # ## under development and will be will be required in future. ## @@ -390,6 +476,26 @@ def help_project(self): self.obj_appconfig.print_info('Help is called') print("Current Project is : ", self.obj_appconfig.current_project) self.obj_Mainview.obj_dockarea.usermanual() + + def tracker_tool(self, event): + """ + Opens the Tracker Tool application. + """ + # Dynamically locate the tracker tool script + # base_dir = os.path.dirname(os.path.abspath(__file__)) # Get the current directory of this script + # tracker_script_path = os.path.join(base_dir, "TrackerTool", "main.py") + home_dir = os.path.expanduser("~") # Get the user's home directory + tracker_script_path = os.path.join(home_dir, "Downloads", "eSim-2.4", "src", "TrackerTool", "main.py") + try: + # Check if the tracker tool script exists + if os.path.exists(tracker_script_path): + # Use subprocess to run the tracker tool + subprocess.Popen(["python3", tracker_script_path]) + print("Tracker Tool launched successfully.") + else: + print("Tracker Tool script not found:", tracker_script_path) + except Exception as e: + print("Error launching Tracker Tool:", e) @QtCore.pyqtSlot(QtCore.QProcess.ExitStatus, int) def plotSimulationData(self, exitCode, exitStatus): @@ -715,6 +821,8 @@ def __init__(self, *args): # It is main function of the module and starts the application def main(args): + user_id = tracker.generate_username() + log_capture(user_id) """ The splash screen opened at the starting of screen is performed by this function. @@ -749,10 +857,78 @@ def main(args): except IOError: work = 0 + def show_preferences(): + global tracker_thread + preferences_file = os.path.expanduser("~/.esim/preferences.json") + user_preferences = {} + + + # Load existing preferences if available + if os.path.exists(preferences_file): + with open(preferences_file, "r") as file: + user_preferences = json.load(file) + + # Show dialog if the user has not chosen to remember preferences + if not user_preferences.get("remember_choice", False): + dialog = UserPreferenceDialog() + if dialog.exec_() == QtWidgets.QDialog.Accepted: + preferences = dialog.getPreferences() + + # Save preferences if the user chose to remember + if preferences["remember_choice"]: + os.makedirs(os.path.dirname(preferences_file), exist_ok=True) + with open(preferences_file, "w") as file: + json.dump(preferences, file) + + # Start session tracking if the user chose to track + if preferences["track_session"]: + print("Session tracking enabled.") + start_tracking(preferences["username"]) # Start tracking in a separate process + else: + print("Session tracking disabled.") + else: + print("User cancelled. Exiting application.") + sys.exit(0) + else: + # Act based on remembered preferences + if user_preferences.get("track_session", False): + print("Session tracking enabled (from remembered preferences).") + start_tracking(user_preferences.get("username", "Unknown")) + else: + print("Session tracking disabled (from remembered preferences).") + + def start_tracking(username): + """ + Start tracking the session by launching tracker.py as a completely independent process. + """ + home_dir = os.path.expanduser("~") + tracker_script = os.path.join(home_dir, "Downloads", "eSim-2.4", "src", "TrackerTool", "tracker.py") + #command = [sys.executable, "tracker.py", username] + command = [sys.executable, tracker_script, username] + + if sys.platform.startswith("win"): + # Windows: DETACHED_PROCESS ensures process runs independently + DETACHED_PROCESS = 0x00000008 + subprocess.Popen(command, creationflags=DETACHED_PROCESS, close_fds=True) + else: + # Linux/macOS: Use setsid to detach process + subprocess.Popen(command, preexec_fn=os.setsid, close_fds=True) + + def after_workspace_selection(): + splash.close() + appView.show() + QtCore.QTimer.singleShot(100, show_preferences) + + def on_workspace_closed(): + appView.obj_workspace.close() + after_workspace_selection() + if work != 0: appView.obj_workspace.defaultWorkspace() + after_workspace_selection() else: appView.obj_workspace.show() + appView.obj_workspace.okbtn.clicked.connect(on_workspace_closed) sys.exit(app.exec_()) diff --git a/src/frontEnd/TrackerTool/app.py b/src/frontEnd/TrackerTool/app.py new file mode 100644 index 000000000..7bca9a07c --- /dev/null +++ b/src/frontEnd/TrackerTool/app.py @@ -0,0 +1,291 @@ +from flask import Flask, jsonify, request, send_from_directory +from flask_cors import CORS +import psycopg2 +import os +from datetime import timedelta + +# Initialize Flask app +app = Flask(__name__) +CORS(app) # Enable CORS for frontend interaction + +# Database connection helper +def connect_db(): + return psycopg2.connect( + dbname="esim_tracker", + user="esim_tracker_user", + password="iusEtWgeL6xXkpYVOkC532tFenmaik2x", + host="dpg-cu6tr3l6l47c73c3snh0-a.oregon-postgres.render.com", + port="5432" + ) + +# Serve the front-end (index.html) +@app.route('/') +def serve_frontend(): + return send_from_directory('static', 'index.html') # Ensure your index.html is in a 'static' folder + +@app.route('/statstics', methods=['GET']) +def get_stats(): + conn = connect_db() + cursor = conn.cursor() + + user_id = request.args.get("user") # Fetch user filter from API request + + if user_id: + # Fetch stats for a specific user (Fixed: use %s instead of ?) + cursor.execute("SELECT COUNT(*) FROM sessions WHERE user_id = %s", (user_id,)) + total_sessions = cursor.fetchone()[0] or 0 + + cursor.execute("SELECT SUM(total_duration) FROM sessions WHERE user_id = %s", (user_id,)) + total_hours = cursor.fetchone()[0] or 0 + + cursor.execute("SELECT AVG(total_duration) FROM sessions WHERE user_id = %s", (user_id,)) + avg_duration = cursor.fetchone()[0] or 0 + + else: + # Fetch overall stats (all users) + cursor.execute("SELECT COUNT(DISTINCT user_id) FROM sessions") + active_users = cursor.fetchone()[0] or 0 + + cursor.execute("SELECT COUNT(*) FROM sessions") + total_sessions = cursor.fetchone()[0] or 0 + + cursor.execute("SELECT SUM(total_duration) FROM sessions") + total_hours = cursor.fetchone()[0] or 0 + + cursor.execute("SELECT AVG(total_duration) FROM sessions") + avg_duration = cursor.fetchone()[0] or 0 + + conn.close() + + # Convert `total_hours` and `avg_duration` from timedelta if needed + if isinstance(avg_duration, timedelta): + avg_duration = avg_duration.total_seconds() / 3600 # Convert to hours + + if isinstance(total_hours, timedelta): + total_hours = total_hours.total_seconds() / 3600 # Convert to hours + + return jsonify({ + "total_sessions": total_sessions, + "active_users": active_users if not user_id else 1, # Show active user count only for all users + "total_hours": float(total_hours), # Convert to numeric format + "avg_duration": float(avg_duration), # Convert to numeric format + }) +# API Endpoint: Get all sessions +@app.route('/sessions', methods=['GET']) +def get_sessions(): + user_filter = request.args.get('user') # Get user filter from query parameter + conn = connect_db() + cursor = conn.cursor() + + if user_filter: + cursor.execute("SELECT * FROM sessions WHERE user_id = %s", (user_filter,)) + else: + cursor.execute("SELECT * FROM sessions") + + sessions = cursor.fetchall() + + # Format session data for easier handling in frontend + formatted_sessions = [] + for session in sessions: + session_data = { + 'session_id': session[0], + 'user_id': session[1], + 'session_start': session[2].strftime('%Y-%m-%d %H:%M:%S'), + 'session_end': session[3].strftime('%Y-%m-%d %H:%M:%S') if session[3] else '', + 'total_duration': str(session[4]) if session[4] else 'N/A' + } + formatted_sessions.append(session_data) + + conn.close() + return jsonify(formatted_sessions) + + +@app.route('/logs', methods=['GET']) +def get_logs(): + """Fetch logs only for the requested user.""" + user_filter = request.args.get('user') # Get user filter from query parameter + + conn = connect_db() + cursor = conn.cursor() + + if user_filter: + cursor.execute("SELECT log_id, user_id, log_timestamp, log_content FROM logs WHERE user_id = %s", (user_filter,)) + else: + cursor.execute("SELECT log_id, user_id, log_timestamp, log_content FROM logs") + + logs = cursor.fetchall() + conn.close() + + # Format log data for JSON response + formatted_logs = [ + { + "log_id": log[0], + "user_id": log[1], + "log_timestamp": log[2].strftime('%Y-%m-%d %H:%M:%S'), + "log_content": log[3] + } + for log in logs + ] + + return jsonify(formatted_logs) + +@app.route("/get_users", methods=["GET"]) +def get_users(): + conn = connect_db() + cursor = conn.cursor() + cursor.execute("SELECT DISTINCT user_id FROM sessions;") + users = [row[0] for row in cursor.fetchall()] + conn.close() + return jsonify(users) + +@app.route("/get_summary", methods=["GET"]) +def get_summary(): + user = request.args.get("user", "All Users") + conn = connect_db() + cursor = conn.cursor() + + if user == "All Users": + cursor.execute("SELECT COUNT(*), SUM(total_duration) FROM sessions;") + else: + cursor.execute("SELECT COUNT(*), SUM(total_duration) FROM sessions WHERE user_id=%s;", (user,)) + + result = cursor.fetchone() + conn.close() + + summary = { + "total_sessions": result[0] if result else 0, + "total_duration": str(result[1]) if result[1] else "0:00:00" + } + return jsonify(summary) +@app.route('/add-session', methods=['POST']) +def add_session(): + data = request.json + conn = connect_db() + cursor = conn.cursor() + + # Ensure total_duration is in INTERVAL format (in hours) + total_duration_interval = f"{data['total_duration']} hours" + + try: + # Insert session with correct type casting for total_duration + cursor.execute(''' + INSERT INTO sessions (user_id, session_start, session_end, total_duration) + VALUES (%s, %s, %s, %s::INTERVAL) + ''', (data['user_id'], data['session_start'], data['session_end'], total_duration_interval)) + conn.commit() + conn.close() + return jsonify({"message": "Session added successfully"}) + + except Exception as e: + return jsonify({"error": f"Error occurred: {str(e)}"}), 50 + + +# API Endpoint: Add a log +@app.route('/add-log', methods=['POST']) +def add_log(): + data = request.json + conn = connect_db() + cursor = conn.cursor() + cursor.execute('''INSERT INTO logs (user_id, log_timestamp, log_content) + VALUES (%s, %s, %s)''', + (data['user_id'], data['log_timestamp'], data['log_content'])) + conn.commit() + conn.close() + return jsonify({"message": "Log added successfully"}) + +@app.route('/metrics', methods=['GET']) +def get_metrics(): + try: + user_filter = request.args.get('user') + conn = connect_db() + cursor = conn.cursor() + + # Total active users + cursor.execute("SELECT COUNT(DISTINCT user_id) FROM sessions") + active_users = cursor.fetchone()[0] + + # Total hours logged (Handle timedelta conversion) + cursor.execute("SELECT SUM(total_duration) FROM sessions") # duration should be timedelta + total_duration = cursor.fetchone()[0] or timedelta(0) # Handle null or empty result + + # Convert total_duration to hours + total_hours = total_duration.total_seconds() / 3600 # Convert to hours + + # Average time spent per session (Handle timedelta conversion) + cursor.execute("SELECT AVG(total_duration) FROM sessions") # duration should be timedelta + avg_time = cursor.fetchone()[0] or timedelta(0) # Handle null or empty result + + # Convert avg_time to hours + avg_duration = avg_time.total_seconds() / 3600 # Convert to hours + + conn.close() + + # Return as JSON + return jsonify({ + "active-users": active_users, + "total-hours": round(total_hours, 2), + "avg-duration": round(avg_duration, 2) + }) + except Exception as e: + print(f"Error: {e}") + return jsonify({"error": "There was a problem with the database query.", "details": str(e)}), 500 + + +@app.route('/export-data', methods=['GET']) +def export_data(): + """Fetches session data for export.""" + user_filter = request.args.get("user_filter") + conn = connect_db() + cursor = conn.cursor() + + if user_filter and user_filter != "All Users": + cursor.execute("SELECT user_id, session_start, session_end, total_duration FROM sessions WHERE user_id = %s", (user_filter,)) + else: + cursor.execute("SELECT user_id, session_start, session_end, total_duration FROM sessions") + + records = cursor.fetchall() + conn.close() + + if not records: + return jsonify({"error": "No data found"}), 404 # Return error if no data + + try: + data = [ + { + "user_id": r[0] if r[0] is not None else "Unknown", + "session_start": r[1].isoformat() if r[1] is not None else "N/A", + "session_end": r[2].isoformat() if r[2] is not None else "N/A", + "total_duration": r[3].total_seconds() if r[3] is not None else 0 # Convert timedelta to seconds + } + for r in records + ] + return jsonify(data) + + except Exception as e: + print(f"Error: {e}") + return jsonify({"error": "Failed to serialize data"}), 500 + +@app.route('/delete-session', methods=['DELETE']) +def delete_session(): + """Deletes a session based on user and start time.""" + data = request.json + user_id = data.get("user") # Fix: Ensure correct key name + session_start = data.get("session_start") + + conn = connect_db() + cursor = conn.cursor() + + cursor.execute("DELETE FROM sessions WHERE user_id = %s AND session_start = %s", (user_id, session_start)) + conn.commit() + + if cursor.rowcount == 0: # No rows deleted (session not found) + conn.close() + return jsonify({"error": "Session not found or already deleted"}), 404 + + conn.close() + return jsonify({"message": f"Session for {user_id} at {session_start} deleted successfully."}) +# Run the app +if __name__ == '__main__': + app.run(debug=True) + from waitress import serve + serve(app, host='0.0.0.0', port=8080) diff --git a/src/frontEnd/TrackerTool/main.py b/src/frontEnd/TrackerTool/main.py new file mode 100644 index 000000000..5f60f4b34 --- /dev/null +++ b/src/frontEnd/TrackerTool/main.py @@ -0,0 +1,416 @@ +from PyQt5.QtWidgets import ( + QApplication, QListWidget,QDialog,QWidget,QFileDialog, QVBoxLayout,QScrollArea,QHeaderView, QFrame,QAbstractItemView,QPushButton, QLabel, QComboBox,QMessageBox,QHBoxLayout, QFileDialog,QInputDialog,QTableWidget, QTableWidgetItem +) +from PyQt5.QtCore import Qt +import sys,platform,os +import threading +import subprocess +import csv +#from tracker import TrackerTool +import sqlite3 +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure +import matplotlib.pyplot as plt +from datetime import datetime +import requests +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QComboBox, QPushButton,QTextEdit +import socket +from getmac import get_mac_address + +API_BASE_URL = "https://tooltracker-afxj.onrender.com" +#API_BASE_URL = "http://127.0.0.1:5000" + + +class TrackerApp(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("eSim Tool Tracker") + self.setGeometry(100, 100, 500, 400) + + # Layout + layout = QVBoxLayout() + + # View Statistics Button + self.view_stats_button = QPushButton("View Statistics") + self.view_stats_button.clicked.connect(self.view_statistics) + layout.addWidget(self.view_stats_button) + + # View User Activity Button + self.view_user_activity_button = QPushButton("View User Activity") + self.view_user_activity_button.clicked.connect(self.view_user_activity) + layout.addWidget(self.view_user_activity_button) + + # View Logs Button + self.view_logs_button = QPushButton("View Logs") + self.view_logs_button.clicked.connect(self.view_logs) + layout.addWidget(self.view_logs_button) + + # Quit Button + self.quit_button = QPushButton("Quit") + self.quit_button.clicked.connect(self.quit_app) + layout.addWidget(self.quit_button) + + + self.setLayout(layout) + @staticmethod + def generate_username(): + pc_name = socket.gethostname() + mac_address = get_mac_address() # Get the real MAC address of the active network interface + if mac_address: + return f"{pc_name}_{mac_address.replace(':', '_')}" + else: + raise Exception("Unable to retrieve the MAC address.") + + def view_statistics(self): + # Create the Statistics window + stats_window = QDialog(self) + stats_window.setWindowTitle("Statistics") + stats_window.setGeometry(100, 100, 700, 500) + + layout = QVBoxLayout(stats_window) + + # Fetch the current user's username + self.selected_user = self.generate_username() # Ensure this function returns the correct username + + # Add a placeholder for the summary and table layout (Move this before calling `display_summary`) + self.summary_container = QVBoxLayout() + layout.addLayout(self.summary_container) + + # Display summary metrics for the specific user + self.display_summary(stats_window, self.selected_user) + + # Export Data Button + export_button = QPushButton("Export Data") + export_button.clicked.connect(lambda: self.export_data(self.selected_user)) + layout.addWidget(export_button) + + # Delete Data Button + delete_button = QPushButton("Delete Data") + delete_button.clicked.connect(lambda: self.delete_data(self.selected_user, stats_window)) + layout.addWidget(delete_button) + + stats_window.setLayout(layout) + stats_window.exec_() + + def export_data(self, user_filter): + """Exports session data to a CSV file via API.""" + + print(f"Selected User for Export: {user_filter}") # Debugging + + url = f"{API_BASE_URL}/export-data" + params = {"user_filter": user_filter} # Correctly passing user filter + response = requests.get(url, params=params) + + if response.status_code == 200: + records = response.json() + print(f"Received Records: {records}") # Debugging + + if not records: + QMessageBox.warning(self, "Export Failed", "No data available for export.") + return + + # Ask user where to save the file + file_path, _ = QFileDialog.getSaveFileName( + self, "Save File", "", "CSV Files (*.csv);;All Files (*)" + ) + + if file_path: + with open(file_path, mode='w', newline='') as file: + writer = csv.writer(file) + writer.writerow(["User", "Start Time", "End Time", "Duration"]) + + for record in records: + writer.writerow([ + record["user_id"], + record["session_start"], + record["session_end"], + record["total_duration"] + ]) + + QMessageBox.information(self, "Export Successful", "Data exported successfully!") + else: + QMessageBox.warning(self, "Export Failed", "Failed to fetch data from server.") + + def delete_data(self, user_filter, event=None): + """Deletes a session by calling API and allowing user selection.""" + url = f"{API_BASE_URL}/sessions" + params = {"user": user_filter} # Fix: Ensure correct parameter name + response = requests.get(url, params=params) + + if response.status_code == 200: + records = response.json() + + if not records: + QMessageBox.warning(self, "No Data", "No sessions available to delete.") + return + + # Create dropdown with sessions for the selected user only + items = [f"{r['session_start']}" for r in records] # Fix: Only show relevant sessions + item, ok = QInputDialog.getItem(self, "Select Session", "Select a session to delete:", items, 0, False) + + if ok and item: + session_start = item # Only session_start is needed since user_id is fixed + + confirmation = QMessageBox.question( + self, "Confirm Deletion", + f"Are you sure you want to delete the session for '{user_filter}' at '{session_start}'?", + QMessageBox.Yes | QMessageBox.No + ) + + if confirmation == QMessageBox.Yes: + delete_url = f"{API_BASE_URL}/delete-session" + delete_response = requests.delete(delete_url, json={"user": user_filter, "session_start": session_start}) + + if delete_response.status_code == 200: + QMessageBox.information(self, "Deletion Successful", "Session deleted successfully.") + self.display_summary(user_filter) # Refresh UI after deletion + else: + QMessageBox.warning(self, "Deletion Failed", "Failed to delete session.") + else: + QMessageBox.warning(self, "Fetch Failed", "Failed to fetch session data.") + + def clear_layout_recursive(self, layout): + # Loop through all items in the layout + while layout.count(): + item = layout.takeAt(0) + if item.widget(): + item.widget().deleteLater() # Remove the widget + elif item.layout(): + self.clear_layout_recursive(item.layout()) # Recursively clear nested layouts + + def display_summary(self, stats_window, user_filter=None): + # Ensure `user_filter` is always set to the logged-in user + current_user = self.generate_username() # Get the generated username + user_filter = current_user # Override the user filter + + # Clear existing data in the summary layout + self.clear_layout_recursive(self.summary_container) + summary_layout = QVBoxLayout() + + try: + # Make API request to get statistics for the current user only + api_url = f"{API_BASE_URL}/statstics" + params = {"user": user_filter} + + response = requests.get(api_url, params=params) + response.raise_for_status() # Raise an error if the request fails + data = response.json() + + # Extract statistics + total_sessions = data.get("total_sessions", 0) + total_hours = data.get("total_hours", 0.0) + avg_duration = data.get("avg_duration", 0.0) + + # Display summary metrics + metrics = [ + ("Total Hours Logged:", f"{total_hours:.2f} hours"), + ("Average Duration per Session:", f"{avg_duration:.2f} hours"), + ("Total Number of Sessions:", total_sessions), + ] + + for label_text, value_text in metrics: + metric_layout = QHBoxLayout() + label = QLabel(f"{label_text}") + value = QLabel(str(value_text)) + metric_layout.addWidget(label) + metric_layout.addWidget(value) + summary_layout.addLayout(metric_layout) + + self.summary_container.addLayout(summary_layout) + + # Fetch session details from API for the current user + api_url_sessions = f"{API_BASE_URL}/sessions" + response_sessions = requests.get(api_url_sessions, params=params) + response_sessions.raise_for_status() + sessions = response_sessions.json() + + # Create table for individual session details + table = QTableWidget() + table.setColumnCount(4) + table.setHorizontalHeaderLabels(["User", "Start Time", "End Time", "Duration"]) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setEditTriggers(QAbstractItemView.NoEditTriggers) + + # Populate table with session data + table.setRowCount(len(sessions)) + for row_index, records in enumerate(sessions): + table.setItem(row_index, 0, QTableWidgetItem(records["user_id"])) + table.setItem(row_index, 1, QTableWidgetItem(records["session_start"])) + table.setItem(row_index, 2, QTableWidgetItem(records["session_end"])) + table.setItem(row_index, 3, QTableWidgetItem(f"{float(records['total_duration'].split(':')[0]) + float(records['total_duration'].split(':')[1]) / 60:.2f} hrs")) + + self.summary_container.addWidget(table) + + except requests.RequestException as e: + print(f"API Request Error: {e}") + + def view_user_activity(self): + """Fetch and display activity data for the logged-in user.""" + activity_window = QDialog(self) + activity_window.setWindowTitle("User Activity") + activity_window.setGeometry(100, 100, 1000, 600) + + layout = QVBoxLayout(activity_window) + + # Dropdown for selecting chart type + self.chart_type = QComboBox() + self.chart_type.addItems(["Bar Chart", "Pie Chart", "Line Chart"]) + self.chart_type.setCurrentText("Bar Chart") + layout.addWidget(self.chart_type) + + # Button to generate chart + generate_btn = QPushButton("Generate Chart") + generate_btn.clicked.connect(lambda: self.generate_chart(activity_window)) + layout.addWidget(generate_btn) + + # Scrollable area for the chart + self.scroll_area = QScrollArea(activity_window) + self.scroll_area.setWidgetResizable(True) + layout.addWidget(self.scroll_area) + + # Chart container + self.chart_container = QWidget() + self.scroll_area.setWidget(self.chart_container) + self.chart_layout = QVBoxLayout(self.chart_container) + + activity_window.setLayout(layout) + activity_window.exec_() + + def generate_chart(self, activity_window): + """Fetch session data for the logged-in user and generate a chart.""" + current_user = self.generate_username() # Get the logged-in user + response = requests.get(f"{API_BASE_URL}/sessions", params={"user": current_user}) + + if response.status_code != 200 or not response.json(): + QMessageBox.information(activity_window, "No Data", "No activity data to display.") + return + def parse_duration(duration_str): + """Convert 'HH:MM:SS.ssssss' to total hours as a float.""" + h, m, s = map(float, duration_str.split(":")) # Convert each part to float + return h + (m / 60) + (s / 3600) # Convert to total hours + # Extract session data + sessions = response.json() + timestamps = [s['session_start'] for s in sessions] + durations = [parse_duration(s['total_duration']) for s in sessions] # Convert durations correctly + + # Clear previous chart + for i in reversed(range(self.chart_layout.count())): + widget = self.chart_layout.itemAt(i).widget() + if widget: + widget.deleteLater() + + # Create a Matplotlib figure + fig, ax = plt.subplots(figsize=(15, 6)) + chart_type = self.chart_type.currentText() + + # Generate the selected chart type + if chart_type == "Bar Chart": + ax.bar(timestamps, durations, color='skyblue') + ax.set_title(f'Activity Log for {current_user} (Bar Chart)', fontsize=14) + ax.set_xlabel('Session Start Time', fontsize=12) + ax.set_ylabel('Duration (hours)', fontsize=12) + elif chart_type == "Pie Chart": + ax.pie(durations, labels=timestamps, autopct='%1.1f%%', startangle=90) + ax.set_title(f'Activity Log for {current_user} (Pie Chart)', fontsize=14) + elif chart_type == "Line Chart": + ax.plot(timestamps, durations, marker='o', color='blue') + ax.set_title(f'Activity Log for {current_user} (Line Chart)', fontsize=14) + ax.set_xlabel('Session Start Time', fontsize=12) + ax.set_ylabel('Duration (hours)', fontsize=12) + + # Embed Matplotlib figure into PyQt5 + canvas = FigureCanvas(fig) + self.chart_layout.addWidget(canvas) + canvas.draw() + + def view_logs(self): + """Fetch and display logs only for the logged-in user.""" + # Create the logs window + logs_window = QDialog(self) + logs_window.setWindowTitle("View Logs") + logs_window.setGeometry(100, 100, 800, 600) + + layout = QVBoxLayout(logs_window) + + # Get the logged-in user's username + current_user = self.generate_username() + + # Fetch logs from the API for the current user + url = f"{API_BASE_URL}/logs" + params = {"user": current_user} + response = requests.get(url, params=params) + + if response.status_code == 200: + logs = response.json() + else: + logs = [] + + if not logs: + no_logs_label = QLabel("No logs available for this user.", logs_window) + no_logs_label.setStyleSheet("font-size: 14px; font-weight: bold;") + layout.addWidget(no_logs_label) + logs_window.setLayout(layout) + logs_window.exec_() + return + + # List widget for displaying logs + log_list_widget = QListWidget(logs_window) + for log in logs: + log_list_widget.addItem(f"ID: {log['log_id']}, Timestamp: {log['log_timestamp']}") + + layout.addWidget(log_list_widget) + + #Function to show selected log details + def show_selected_log(): + + selected_item = log_list_widget.currentItem() + if selected_item: + selected_index = log_list_widget.row(selected_item) + log = logs[selected_index] + log_details = f"User: {log['user_id']}\nTimestamp: {log['log_timestamp']}\n\nLog Content:\n{log['log_content']}" + + # Create a QDialog instead of QMessageBox + log_dialog = QDialog(logs_window) + log_dialog.setWindowTitle("Log Details") + log_dialog.setGeometry(200, 200, 700, 500) # Set initial size + log_dialog.setSizeGripEnabled(True) # Enable window resizing + + # Create a QVBoxLayout + layout = QVBoxLayout(log_dialog) + + # Create a QTextEdit (scrollable) to display the log content + log_text_edit = QTextEdit(log_dialog) + log_text_edit.setText(log_details) + log_text_edit.setReadOnly(True) # Make it read-only + log_text_edit.setMinimumSize(600, 400) # Ensure a good default size + + # Add the text edit widget to the layout + layout.addWidget(log_text_edit) + + # Close button + close_button = QPushButton("Close", log_dialog) + close_button.clicked.connect(log_dialog.close) + layout.addWidget(close_button) + + log_dialog.setLayout(layout) + log_dialog.exec_() + + + # View button to show the selected log's details + view_btn = QPushButton("View Selected Log", logs_window) + view_btn.clicked.connect(show_selected_log) + + layout.addWidget(view_btn) + + logs_window.setLayout(layout) + logs_window.exec_() + + + def quit_app(self): + self.close() + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = TrackerApp() + window.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/src/frontEnd/TrackerTool/tracker.py b/src/frontEnd/TrackerTool/tracker.py new file mode 100644 index 000000000..df884cda9 --- /dev/null +++ b/src/frontEnd/TrackerTool/tracker.py @@ -0,0 +1,142 @@ +import psutil +import time +from datetime import datetime +import os,glob +import requests +import socket + +# def generate_username(): +# pc_name = socket.gethostname() +# mac_address = '_'.join(f'{(uuid.getnode() >> i) & 0xff:02x}' for i in range(40, -1, -8)) +# return f"{pc_name}_{mac_address}" +from getmac import get_mac_address + +def generate_username(): + pc_name = socket.gethostname() + mac_address = get_mac_address() # Get the real MAC address of the active network interface + if mac_address: + return f"{pc_name}_{mac_address.replace(':', '_')}" + else: + raise Exception("Unable to retrieve the MAC address.") +# API base URL for the Flask app hosted locally or on Render +API_BASE_URL = "https://tooltracker-afxj.onrender.com/" +LOG_DIR = os.path.join(os.getcwd(), "logs") # Dynamically set the log directory + +# Function to send session data to the Flask API +def send_session_to_api(user_id, session_start, session_end, total_duration): + data = { + "user_id": user_id, + "session_start": session_start.strftime('%Y-%m-%d %H:%M:%S'), + "session_end": session_end.strftime('%Y-%m-%d %H:%M:%S'), + "total_duration": f"{total_duration} hours" + } + try: + response = requests.post(f"{API_BASE_URL}/add-session", json=data) + print(f"Session API Response: {response.json()}") + except requests.exceptions.RequestException as e: + print(f"Error sending session data to API: {e}") + +# Function to send log data to the Flask API +def send_log_to_api(user_id, log_timestamp, log_content): + data = { + "user_id": user_id, + "log_timestamp": log_timestamp.strftime('%Y-%m-%d %H:%M:%S'), + "log_content": log_content + } + try: + response = requests.post(f"{API_BASE_URL}/add-log", json=data) + print(f"Log API Response: {response.json()}") + except requests.exceptions.RequestException as e: + print(f"Error sending log data to API: {e}") + +# Ensure log directory exists +def ensure_log_directory(): + if not os.path.exists(LOG_DIR): + print(f"Creating log directory: {LOG_DIR}") + os.makedirs(LOG_DIR) + +# Function to log session details and send them to the API +def log_session(user_id, session_start, session_end): + total_duration = (session_end - session_start).total_seconds() / 3600 # Duration in hours + send_session_to_api(user_id, session_start, session_end, total_duration) + +# Function to store logs and send them to the API +# def store_log(user_id): +# log_file_path = os.path.join(LOG_DIR, f"{user_id}_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt") + +# try: +# # Write some dummy content to simulate eSim logs +# with open(log_file_path, 'w') as file: +# file.write(f"Log initialized for user {user_id} at {datetime.now()}\n") + +# # Read and send the log content +# with open(log_file_path, 'r') as file: +# log_content = file.read() +# send_log_to_api(user_id, datetime.now(), log_content) +# except Exception as e: +# print(f"Error handling log file: {e}") +LOG_DIR = "/home/mmn/Downloads/eSim-2.4/src/frontEnd/logs" + +def store_log(user_id): + """Finds the latest log file for the user and sends it to the API.""" + try: + # Find the latest log file for the user + log_files = sorted( + glob.glob(os.path.join(LOG_DIR, f"{user_id}_log_*.txt")), + key=os.path.getmtime, # Sort by modification time (latest last) + reverse=True # Get latest file first + ) + + if not log_files: + print(f"No log file found for user {user_id}.") + return + + latest_log_file = log_files[0] # Get the most recent log file + + # Read and send the log content + with open(latest_log_file, 'r') as file: + log_content = file.read() + + send_log_to_api(user_id, datetime.now(), log_content) + + except Exception as e: + print(f"Error handling log file: {e}") +# Check if eSim is running +def is_esim_running(): + for process in psutil.process_iter(['name']): + if 'esim' in process.info['name'].lower(): + return True + return False + +# Track user activity +def track_activity(user_id): + session_start = None + ensure_log_directory() + + print(f"Tracking started for user: {user_id}") + try: + while True: + if is_esim_running(): + if session_start is None: + session_start = datetime.now() + print(f"Session started at {session_start}") + else: + if session_start: + session_end = datetime.now() + log_session(user_id, session_start, session_end) + store_log(user_id) + print(f"Session ended at {session_end}") + print(f"Duration: {(session_end - session_start)}") + session_start = None + time.sleep(1) # Check every 2 seconds + except KeyboardInterrupt: + print("Tracking stopped.") + +# Main entry point +if __name__ == "__main__": + user_id = generate_username() + # consent = input("Do you consent to activity tracking? (yes/no): ") + # if consent.lower() == 'yes': + track_activity(user_id) + # else: + print("Tracking aborted. Consent not given.") diff --git a/src/frontEnd/Workspace.py b/src/frontEnd/Workspace.py index fca73e399..c0d8e9083 100755 --- a/src/frontEnd/Workspace.py +++ b/src/frontEnd/Workspace.py @@ -33,6 +33,7 @@ class Workspace(QtWidgets.QWidget): - This workspace area contains all the projects made by user. """ + workspace_closed = QtCore.pyqtSignal()#Added Code def __init__(self, parent=None): super(Workspace, self).__init__() @@ -113,6 +114,7 @@ def defaultWorkspace(self): def close(self, *args, **kwargs): self.window_open_close = 1 self.close_var = 1 + self.workspace_closed.emit() # Emit the signal return QtWidgets.QWidget.close(self, *args, **kwargs) def returnWhetherClickedOrNot(self, appView): diff --git a/src/frontEnd/tracker.py b/src/frontEnd/tracker.py new file mode 100644 index 000000000..3aa870256 --- /dev/null +++ b/src/frontEnd/tracker.py @@ -0,0 +1,124 @@ +import psutil +import time +from datetime import datetime +import os,glob +import requests +import socket + + +from getmac import get_mac_address + +def generate_username(): + pc_name = socket.gethostname() + mac_address = get_mac_address() # Get the real MAC address of the active network interface + if mac_address: + return f"{pc_name}_{mac_address.replace(':', '_')}" + else: + raise Exception("Unable to retrieve the MAC address.") +# API base URL for the Flask app hosted locally or on Render +API_BASE_URL = "https://tooltracker-afxj.onrender.com/" +LOG_DIR = os.path.join(os.getcwd(), "logs") # Dynamically set the log directory + +# Function to send session data to the Flask API +def send_session_to_api(user_id, session_start, session_end, total_duration): + data = { + "user_id": user_id, + "session_start": session_start.strftime('%Y-%m-%d %H:%M:%S'), + "session_end": session_end.strftime('%Y-%m-%d %H:%M:%S'), + "total_duration": f"{total_duration} hours" + } + try: + response = requests.post(f"{API_BASE_URL}/add-session", json=data) + print(f"Session API Response: {response.json()}") + except requests.exceptions.RequestException as e: + print(f"Error sending session data to API: {e}") + +# Function to send log data to the Flask API +def send_log_to_api(user_id, log_timestamp, log_content): + data = { + "user_id": user_id, + "log_timestamp": log_timestamp.strftime('%Y-%m-%d %H:%M:%S'), + "log_content": log_content + } + try: + response = requests.post(f"{API_BASE_URL}/add-log", json=data) + print(f"Log API Response: {response.json()}") + except requests.exceptions.RequestException as e: + print(f"Error sending log data to API: {e}") + +# Ensure log directory exists +def ensure_log_directory(): + if not os.path.exists(LOG_DIR): + print(f"Creating log directory: {LOG_DIR}") + os.makedirs(LOG_DIR) + +# Function to log session details and send them to the API +def log_session(user_id, session_start, session_end): + total_duration = (session_end - session_start).total_seconds() / 3600 # Duration in hours + send_session_to_api(user_id, session_start, session_end, total_duration) + +LOG_DIR = "/home/mmn/Downloads/eSim-2.4/src/frontEnd/logs" + +def store_log(user_id): + """Finds the latest log file for the user and sends it to the API.""" + try: + # Find the latest log file for the user + log_files = sorted( + glob.glob(os.path.join(LOG_DIR, f"{user_id}_log_*.txt")), + key=os.path.getmtime, # Sort by modification time (latest last) + reverse=True # Get latest file first + ) + + if not log_files: + print(f"No log file found for user {user_id}.") + return + + latest_log_file = log_files[0] # Get the most recent log file + + # Read and send the log content + with open(latest_log_file, 'r') as file: + log_content = file.read() + + send_log_to_api(user_id, datetime.now(), log_content) + + except Exception as e: + print(f"Error handling log file: {e}") +# Check if eSim is running +def is_esim_running(): + for process in psutil.process_iter(['name']): + if 'esim' in process.info['name'].lower(): + return True + return False + +# Track user activity +def track_activity(user_id): + session_start = None + ensure_log_directory() + + print(f"Tracking started for user: {user_id}") + try: + while True: + if is_esim_running(): + if session_start is None: + session_start = datetime.now() + print(f"Session started at {session_start}") + else: + if session_start: + session_end = datetime.now() + log_session(user_id, session_start, session_end) + store_log(user_id) + print(f"Session ended at {session_end}") + print(f"Duration: {(session_end - session_start)}") + session_start = None + time.sleep(1) # Check every 2 seconds + except KeyboardInterrupt: + print("Tracking stopped.") + +# Main entry point +if __name__ == "__main__": + user_id = generate_username() + # consent = input("Do you consent to activity tracking? (yes/no): ") + # if consent.lower() == 'yes': + track_activity(user_id) + # else: + print("Tracking aborted. Consent not given.") From e4a94eb0caf3f64d1c0ac127607ae481d8b1c743 Mon Sep 17 00:00:00 2001 From: krishnaGoutam Date: Tue, 18 Feb 2025 11:47:35 +0530 Subject: [PATCH 2/5] Tracker Tool Added --- src/{frontEnd => }/TrackerTool/app.py | 0 src/{frontEnd => }/TrackerTool/main.py | 830 +++++++++++----------- src/{frontEnd => }/TrackerTool/tracker.py | 284 ++++---- 3 files changed, 557 insertions(+), 557 deletions(-) rename src/{frontEnd => }/TrackerTool/app.py (100%) rename src/{frontEnd => }/TrackerTool/main.py (97%) rename src/{frontEnd => }/TrackerTool/tracker.py (97%) diff --git a/src/frontEnd/TrackerTool/app.py b/src/TrackerTool/app.py similarity index 100% rename from src/frontEnd/TrackerTool/app.py rename to src/TrackerTool/app.py diff --git a/src/frontEnd/TrackerTool/main.py b/src/TrackerTool/main.py similarity index 97% rename from src/frontEnd/TrackerTool/main.py rename to src/TrackerTool/main.py index 5f60f4b34..8f126c533 100644 --- a/src/frontEnd/TrackerTool/main.py +++ b/src/TrackerTool/main.py @@ -1,416 +1,416 @@ -from PyQt5.QtWidgets import ( - QApplication, QListWidget,QDialog,QWidget,QFileDialog, QVBoxLayout,QScrollArea,QHeaderView, QFrame,QAbstractItemView,QPushButton, QLabel, QComboBox,QMessageBox,QHBoxLayout, QFileDialog,QInputDialog,QTableWidget, QTableWidgetItem -) -from PyQt5.QtCore import Qt -import sys,platform,os -import threading -import subprocess -import csv -#from tracker import TrackerTool -import sqlite3 -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.figure import Figure -import matplotlib.pyplot as plt -from datetime import datetime -import requests -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QComboBox, QPushButton,QTextEdit -import socket -from getmac import get_mac_address - -API_BASE_URL = "https://tooltracker-afxj.onrender.com" -#API_BASE_URL = "http://127.0.0.1:5000" - - -class TrackerApp(QWidget): - def __init__(self): - super().__init__() - self.setWindowTitle("eSim Tool Tracker") - self.setGeometry(100, 100, 500, 400) - - # Layout - layout = QVBoxLayout() - - # View Statistics Button - self.view_stats_button = QPushButton("View Statistics") - self.view_stats_button.clicked.connect(self.view_statistics) - layout.addWidget(self.view_stats_button) - - # View User Activity Button - self.view_user_activity_button = QPushButton("View User Activity") - self.view_user_activity_button.clicked.connect(self.view_user_activity) - layout.addWidget(self.view_user_activity_button) - - # View Logs Button - self.view_logs_button = QPushButton("View Logs") - self.view_logs_button.clicked.connect(self.view_logs) - layout.addWidget(self.view_logs_button) - - # Quit Button - self.quit_button = QPushButton("Quit") - self.quit_button.clicked.connect(self.quit_app) - layout.addWidget(self.quit_button) - - - self.setLayout(layout) - @staticmethod - def generate_username(): - pc_name = socket.gethostname() - mac_address = get_mac_address() # Get the real MAC address of the active network interface - if mac_address: - return f"{pc_name}_{mac_address.replace(':', '_')}" - else: - raise Exception("Unable to retrieve the MAC address.") - - def view_statistics(self): - # Create the Statistics window - stats_window = QDialog(self) - stats_window.setWindowTitle("Statistics") - stats_window.setGeometry(100, 100, 700, 500) - - layout = QVBoxLayout(stats_window) - - # Fetch the current user's username - self.selected_user = self.generate_username() # Ensure this function returns the correct username - - # Add a placeholder for the summary and table layout (Move this before calling `display_summary`) - self.summary_container = QVBoxLayout() - layout.addLayout(self.summary_container) - - # Display summary metrics for the specific user - self.display_summary(stats_window, self.selected_user) - - # Export Data Button - export_button = QPushButton("Export Data") - export_button.clicked.connect(lambda: self.export_data(self.selected_user)) - layout.addWidget(export_button) - - # Delete Data Button - delete_button = QPushButton("Delete Data") - delete_button.clicked.connect(lambda: self.delete_data(self.selected_user, stats_window)) - layout.addWidget(delete_button) - - stats_window.setLayout(layout) - stats_window.exec_() - - def export_data(self, user_filter): - """Exports session data to a CSV file via API.""" - - print(f"Selected User for Export: {user_filter}") # Debugging - - url = f"{API_BASE_URL}/export-data" - params = {"user_filter": user_filter} # Correctly passing user filter - response = requests.get(url, params=params) - - if response.status_code == 200: - records = response.json() - print(f"Received Records: {records}") # Debugging - - if not records: - QMessageBox.warning(self, "Export Failed", "No data available for export.") - return - - # Ask user where to save the file - file_path, _ = QFileDialog.getSaveFileName( - self, "Save File", "", "CSV Files (*.csv);;All Files (*)" - ) - - if file_path: - with open(file_path, mode='w', newline='') as file: - writer = csv.writer(file) - writer.writerow(["User", "Start Time", "End Time", "Duration"]) - - for record in records: - writer.writerow([ - record["user_id"], - record["session_start"], - record["session_end"], - record["total_duration"] - ]) - - QMessageBox.information(self, "Export Successful", "Data exported successfully!") - else: - QMessageBox.warning(self, "Export Failed", "Failed to fetch data from server.") - - def delete_data(self, user_filter, event=None): - """Deletes a session by calling API and allowing user selection.""" - url = f"{API_BASE_URL}/sessions" - params = {"user": user_filter} # Fix: Ensure correct parameter name - response = requests.get(url, params=params) - - if response.status_code == 200: - records = response.json() - - if not records: - QMessageBox.warning(self, "No Data", "No sessions available to delete.") - return - - # Create dropdown with sessions for the selected user only - items = [f"{r['session_start']}" for r in records] # Fix: Only show relevant sessions - item, ok = QInputDialog.getItem(self, "Select Session", "Select a session to delete:", items, 0, False) - - if ok and item: - session_start = item # Only session_start is needed since user_id is fixed - - confirmation = QMessageBox.question( - self, "Confirm Deletion", - f"Are you sure you want to delete the session for '{user_filter}' at '{session_start}'?", - QMessageBox.Yes | QMessageBox.No - ) - - if confirmation == QMessageBox.Yes: - delete_url = f"{API_BASE_URL}/delete-session" - delete_response = requests.delete(delete_url, json={"user": user_filter, "session_start": session_start}) - - if delete_response.status_code == 200: - QMessageBox.information(self, "Deletion Successful", "Session deleted successfully.") - self.display_summary(user_filter) # Refresh UI after deletion - else: - QMessageBox.warning(self, "Deletion Failed", "Failed to delete session.") - else: - QMessageBox.warning(self, "Fetch Failed", "Failed to fetch session data.") - - def clear_layout_recursive(self, layout): - # Loop through all items in the layout - while layout.count(): - item = layout.takeAt(0) - if item.widget(): - item.widget().deleteLater() # Remove the widget - elif item.layout(): - self.clear_layout_recursive(item.layout()) # Recursively clear nested layouts - - def display_summary(self, stats_window, user_filter=None): - # Ensure `user_filter` is always set to the logged-in user - current_user = self.generate_username() # Get the generated username - user_filter = current_user # Override the user filter - - # Clear existing data in the summary layout - self.clear_layout_recursive(self.summary_container) - summary_layout = QVBoxLayout() - - try: - # Make API request to get statistics for the current user only - api_url = f"{API_BASE_URL}/statstics" - params = {"user": user_filter} - - response = requests.get(api_url, params=params) - response.raise_for_status() # Raise an error if the request fails - data = response.json() - - # Extract statistics - total_sessions = data.get("total_sessions", 0) - total_hours = data.get("total_hours", 0.0) - avg_duration = data.get("avg_duration", 0.0) - - # Display summary metrics - metrics = [ - ("Total Hours Logged:", f"{total_hours:.2f} hours"), - ("Average Duration per Session:", f"{avg_duration:.2f} hours"), - ("Total Number of Sessions:", total_sessions), - ] - - for label_text, value_text in metrics: - metric_layout = QHBoxLayout() - label = QLabel(f"{label_text}") - value = QLabel(str(value_text)) - metric_layout.addWidget(label) - metric_layout.addWidget(value) - summary_layout.addLayout(metric_layout) - - self.summary_container.addLayout(summary_layout) - - # Fetch session details from API for the current user - api_url_sessions = f"{API_BASE_URL}/sessions" - response_sessions = requests.get(api_url_sessions, params=params) - response_sessions.raise_for_status() - sessions = response_sessions.json() - - # Create table for individual session details - table = QTableWidget() - table.setColumnCount(4) - table.setHorizontalHeaderLabels(["User", "Start Time", "End Time", "Duration"]) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setEditTriggers(QAbstractItemView.NoEditTriggers) - - # Populate table with session data - table.setRowCount(len(sessions)) - for row_index, records in enumerate(sessions): - table.setItem(row_index, 0, QTableWidgetItem(records["user_id"])) - table.setItem(row_index, 1, QTableWidgetItem(records["session_start"])) - table.setItem(row_index, 2, QTableWidgetItem(records["session_end"])) - table.setItem(row_index, 3, QTableWidgetItem(f"{float(records['total_duration'].split(':')[0]) + float(records['total_duration'].split(':')[1]) / 60:.2f} hrs")) - - self.summary_container.addWidget(table) - - except requests.RequestException as e: - print(f"API Request Error: {e}") - - def view_user_activity(self): - """Fetch and display activity data for the logged-in user.""" - activity_window = QDialog(self) - activity_window.setWindowTitle("User Activity") - activity_window.setGeometry(100, 100, 1000, 600) - - layout = QVBoxLayout(activity_window) - - # Dropdown for selecting chart type - self.chart_type = QComboBox() - self.chart_type.addItems(["Bar Chart", "Pie Chart", "Line Chart"]) - self.chart_type.setCurrentText("Bar Chart") - layout.addWidget(self.chart_type) - - # Button to generate chart - generate_btn = QPushButton("Generate Chart") - generate_btn.clicked.connect(lambda: self.generate_chart(activity_window)) - layout.addWidget(generate_btn) - - # Scrollable area for the chart - self.scroll_area = QScrollArea(activity_window) - self.scroll_area.setWidgetResizable(True) - layout.addWidget(self.scroll_area) - - # Chart container - self.chart_container = QWidget() - self.scroll_area.setWidget(self.chart_container) - self.chart_layout = QVBoxLayout(self.chart_container) - - activity_window.setLayout(layout) - activity_window.exec_() - - def generate_chart(self, activity_window): - """Fetch session data for the logged-in user and generate a chart.""" - current_user = self.generate_username() # Get the logged-in user - response = requests.get(f"{API_BASE_URL}/sessions", params={"user": current_user}) - - if response.status_code != 200 or not response.json(): - QMessageBox.information(activity_window, "No Data", "No activity data to display.") - return - def parse_duration(duration_str): - """Convert 'HH:MM:SS.ssssss' to total hours as a float.""" - h, m, s = map(float, duration_str.split(":")) # Convert each part to float - return h + (m / 60) + (s / 3600) # Convert to total hours - # Extract session data - sessions = response.json() - timestamps = [s['session_start'] for s in sessions] - durations = [parse_duration(s['total_duration']) for s in sessions] # Convert durations correctly - - # Clear previous chart - for i in reversed(range(self.chart_layout.count())): - widget = self.chart_layout.itemAt(i).widget() - if widget: - widget.deleteLater() - - # Create a Matplotlib figure - fig, ax = plt.subplots(figsize=(15, 6)) - chart_type = self.chart_type.currentText() - - # Generate the selected chart type - if chart_type == "Bar Chart": - ax.bar(timestamps, durations, color='skyblue') - ax.set_title(f'Activity Log for {current_user} (Bar Chart)', fontsize=14) - ax.set_xlabel('Session Start Time', fontsize=12) - ax.set_ylabel('Duration (hours)', fontsize=12) - elif chart_type == "Pie Chart": - ax.pie(durations, labels=timestamps, autopct='%1.1f%%', startangle=90) - ax.set_title(f'Activity Log for {current_user} (Pie Chart)', fontsize=14) - elif chart_type == "Line Chart": - ax.plot(timestamps, durations, marker='o', color='blue') - ax.set_title(f'Activity Log for {current_user} (Line Chart)', fontsize=14) - ax.set_xlabel('Session Start Time', fontsize=12) - ax.set_ylabel('Duration (hours)', fontsize=12) - - # Embed Matplotlib figure into PyQt5 - canvas = FigureCanvas(fig) - self.chart_layout.addWidget(canvas) - canvas.draw() - - def view_logs(self): - """Fetch and display logs only for the logged-in user.""" - # Create the logs window - logs_window = QDialog(self) - logs_window.setWindowTitle("View Logs") - logs_window.setGeometry(100, 100, 800, 600) - - layout = QVBoxLayout(logs_window) - - # Get the logged-in user's username - current_user = self.generate_username() - - # Fetch logs from the API for the current user - url = f"{API_BASE_URL}/logs" - params = {"user": current_user} - response = requests.get(url, params=params) - - if response.status_code == 200: - logs = response.json() - else: - logs = [] - - if not logs: - no_logs_label = QLabel("No logs available for this user.", logs_window) - no_logs_label.setStyleSheet("font-size: 14px; font-weight: bold;") - layout.addWidget(no_logs_label) - logs_window.setLayout(layout) - logs_window.exec_() - return - - # List widget for displaying logs - log_list_widget = QListWidget(logs_window) - for log in logs: - log_list_widget.addItem(f"ID: {log['log_id']}, Timestamp: {log['log_timestamp']}") - - layout.addWidget(log_list_widget) - - #Function to show selected log details - def show_selected_log(): - - selected_item = log_list_widget.currentItem() - if selected_item: - selected_index = log_list_widget.row(selected_item) - log = logs[selected_index] - log_details = f"User: {log['user_id']}\nTimestamp: {log['log_timestamp']}\n\nLog Content:\n{log['log_content']}" - - # Create a QDialog instead of QMessageBox - log_dialog = QDialog(logs_window) - log_dialog.setWindowTitle("Log Details") - log_dialog.setGeometry(200, 200, 700, 500) # Set initial size - log_dialog.setSizeGripEnabled(True) # Enable window resizing - - # Create a QVBoxLayout - layout = QVBoxLayout(log_dialog) - - # Create a QTextEdit (scrollable) to display the log content - log_text_edit = QTextEdit(log_dialog) - log_text_edit.setText(log_details) - log_text_edit.setReadOnly(True) # Make it read-only - log_text_edit.setMinimumSize(600, 400) # Ensure a good default size - - # Add the text edit widget to the layout - layout.addWidget(log_text_edit) - - # Close button - close_button = QPushButton("Close", log_dialog) - close_button.clicked.connect(log_dialog.close) - layout.addWidget(close_button) - - log_dialog.setLayout(layout) - log_dialog.exec_() - - - # View button to show the selected log's details - view_btn = QPushButton("View Selected Log", logs_window) - view_btn.clicked.connect(show_selected_log) - - layout.addWidget(view_btn) - - logs_window.setLayout(layout) - logs_window.exec_() - - - def quit_app(self): - self.close() - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = TrackerApp() - window.show() +from PyQt5.QtWidgets import ( + QApplication, QListWidget,QDialog,QWidget,QFileDialog, QVBoxLayout,QScrollArea,QHeaderView, QFrame,QAbstractItemView,QPushButton, QLabel, QComboBox,QMessageBox,QHBoxLayout, QFileDialog,QInputDialog,QTableWidget, QTableWidgetItem +) +from PyQt5.QtCore import Qt +import sys,platform,os +import threading +import subprocess +import csv +#from tracker import TrackerTool +import sqlite3 +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure +import matplotlib.pyplot as plt +from datetime import datetime +import requests +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QComboBox, QPushButton,QTextEdit +import socket +from getmac import get_mac_address + +API_BASE_URL = "https://tooltracker-afxj.onrender.com" +#API_BASE_URL = "http://127.0.0.1:5000" + + +class TrackerApp(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("eSim Tool Tracker") + self.setGeometry(100, 100, 500, 400) + + # Layout + layout = QVBoxLayout() + + # View Statistics Button + self.view_stats_button = QPushButton("View Statistics") + self.view_stats_button.clicked.connect(self.view_statistics) + layout.addWidget(self.view_stats_button) + + # View User Activity Button + self.view_user_activity_button = QPushButton("View User Activity") + self.view_user_activity_button.clicked.connect(self.view_user_activity) + layout.addWidget(self.view_user_activity_button) + + # View Logs Button + self.view_logs_button = QPushButton("View Logs") + self.view_logs_button.clicked.connect(self.view_logs) + layout.addWidget(self.view_logs_button) + + # Quit Button + self.quit_button = QPushButton("Quit") + self.quit_button.clicked.connect(self.quit_app) + layout.addWidget(self.quit_button) + + + self.setLayout(layout) + @staticmethod + def generate_username(): + pc_name = socket.gethostname() + mac_address = get_mac_address() # Get the real MAC address of the active network interface + if mac_address: + return f"{pc_name}_{mac_address.replace(':', '_')}" + else: + raise Exception("Unable to retrieve the MAC address.") + + def view_statistics(self): + # Create the Statistics window + stats_window = QDialog(self) + stats_window.setWindowTitle("Statistics") + stats_window.setGeometry(100, 100, 700, 500) + + layout = QVBoxLayout(stats_window) + + # Fetch the current user's username + self.selected_user = self.generate_username() # Ensure this function returns the correct username + + # Add a placeholder for the summary and table layout (Move this before calling `display_summary`) + self.summary_container = QVBoxLayout() + layout.addLayout(self.summary_container) + + # Display summary metrics for the specific user + self.display_summary(stats_window, self.selected_user) + + # Export Data Button + export_button = QPushButton("Export Data") + export_button.clicked.connect(lambda: self.export_data(self.selected_user)) + layout.addWidget(export_button) + + # Delete Data Button + delete_button = QPushButton("Delete Data") + delete_button.clicked.connect(lambda: self.delete_data(self.selected_user, stats_window)) + layout.addWidget(delete_button) + + stats_window.setLayout(layout) + stats_window.exec_() + + def export_data(self, user_filter): + """Exports session data to a CSV file via API.""" + + print(f"Selected User for Export: {user_filter}") # Debugging + + url = f"{API_BASE_URL}/export-data" + params = {"user_filter": user_filter} # Correctly passing user filter + response = requests.get(url, params=params) + + if response.status_code == 200: + records = response.json() + print(f"Received Records: {records}") # Debugging + + if not records: + QMessageBox.warning(self, "Export Failed", "No data available for export.") + return + + # Ask user where to save the file + file_path, _ = QFileDialog.getSaveFileName( + self, "Save File", "", "CSV Files (*.csv);;All Files (*)" + ) + + if file_path: + with open(file_path, mode='w', newline='') as file: + writer = csv.writer(file) + writer.writerow(["User", "Start Time", "End Time", "Duration"]) + + for record in records: + writer.writerow([ + record["user_id"], + record["session_start"], + record["session_end"], + record["total_duration"] + ]) + + QMessageBox.information(self, "Export Successful", "Data exported successfully!") + else: + QMessageBox.warning(self, "Export Failed", "Failed to fetch data from server.") + + def delete_data(self, user_filter, event=None): + """Deletes a session by calling API and allowing user selection.""" + url = f"{API_BASE_URL}/sessions" + params = {"user": user_filter} # Fix: Ensure correct parameter name + response = requests.get(url, params=params) + + if response.status_code == 200: + records = response.json() + + if not records: + QMessageBox.warning(self, "No Data", "No sessions available to delete.") + return + + # Create dropdown with sessions for the selected user only + items = [f"{r['session_start']}" for r in records] # Fix: Only show relevant sessions + item, ok = QInputDialog.getItem(self, "Select Session", "Select a session to delete:", items, 0, False) + + if ok and item: + session_start = item # Only session_start is needed since user_id is fixed + + confirmation = QMessageBox.question( + self, "Confirm Deletion", + f"Are you sure you want to delete the session for '{user_filter}' at '{session_start}'?", + QMessageBox.Yes | QMessageBox.No + ) + + if confirmation == QMessageBox.Yes: + delete_url = f"{API_BASE_URL}/delete-session" + delete_response = requests.delete(delete_url, json={"user": user_filter, "session_start": session_start}) + + if delete_response.status_code == 200: + QMessageBox.information(self, "Deletion Successful", "Session deleted successfully.") + self.display_summary(user_filter) # Refresh UI after deletion + else: + QMessageBox.warning(self, "Deletion Failed", "Failed to delete session.") + else: + QMessageBox.warning(self, "Fetch Failed", "Failed to fetch session data.") + + def clear_layout_recursive(self, layout): + # Loop through all items in the layout + while layout.count(): + item = layout.takeAt(0) + if item.widget(): + item.widget().deleteLater() # Remove the widget + elif item.layout(): + self.clear_layout_recursive(item.layout()) # Recursively clear nested layouts + + def display_summary(self, stats_window, user_filter=None): + # Ensure `user_filter` is always set to the logged-in user + current_user = self.generate_username() # Get the generated username + user_filter = current_user # Override the user filter + + # Clear existing data in the summary layout + self.clear_layout_recursive(self.summary_container) + summary_layout = QVBoxLayout() + + try: + # Make API request to get statistics for the current user only + api_url = f"{API_BASE_URL}/statstics" + params = {"user": user_filter} + + response = requests.get(api_url, params=params) + response.raise_for_status() # Raise an error if the request fails + data = response.json() + + # Extract statistics + total_sessions = data.get("total_sessions", 0) + total_hours = data.get("total_hours", 0.0) + avg_duration = data.get("avg_duration", 0.0) + + # Display summary metrics + metrics = [ + ("Total Hours Logged:", f"{total_hours:.2f} hours"), + ("Average Duration per Session:", f"{avg_duration:.2f} hours"), + ("Total Number of Sessions:", total_sessions), + ] + + for label_text, value_text in metrics: + metric_layout = QHBoxLayout() + label = QLabel(f"{label_text}") + value = QLabel(str(value_text)) + metric_layout.addWidget(label) + metric_layout.addWidget(value) + summary_layout.addLayout(metric_layout) + + self.summary_container.addLayout(summary_layout) + + # Fetch session details from API for the current user + api_url_sessions = f"{API_BASE_URL}/sessions" + response_sessions = requests.get(api_url_sessions, params=params) + response_sessions.raise_for_status() + sessions = response_sessions.json() + + # Create table for individual session details + table = QTableWidget() + table.setColumnCount(4) + table.setHorizontalHeaderLabels(["User", "Start Time", "End Time", "Duration"]) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setEditTriggers(QAbstractItemView.NoEditTriggers) + + # Populate table with session data + table.setRowCount(len(sessions)) + for row_index, records in enumerate(sessions): + table.setItem(row_index, 0, QTableWidgetItem(records["user_id"])) + table.setItem(row_index, 1, QTableWidgetItem(records["session_start"])) + table.setItem(row_index, 2, QTableWidgetItem(records["session_end"])) + table.setItem(row_index, 3, QTableWidgetItem(f"{float(records['total_duration'].split(':')[0]) + float(records['total_duration'].split(':')[1]) / 60:.2f} hrs")) + + self.summary_container.addWidget(table) + + except requests.RequestException as e: + print(f"API Request Error: {e}") + + def view_user_activity(self): + """Fetch and display activity data for the logged-in user.""" + activity_window = QDialog(self) + activity_window.setWindowTitle("User Activity") + activity_window.setGeometry(100, 100, 1000, 600) + + layout = QVBoxLayout(activity_window) + + # Dropdown for selecting chart type + self.chart_type = QComboBox() + self.chart_type.addItems(["Bar Chart", "Pie Chart", "Line Chart"]) + self.chart_type.setCurrentText("Bar Chart") + layout.addWidget(self.chart_type) + + # Button to generate chart + generate_btn = QPushButton("Generate Chart") + generate_btn.clicked.connect(lambda: self.generate_chart(activity_window)) + layout.addWidget(generate_btn) + + # Scrollable area for the chart + self.scroll_area = QScrollArea(activity_window) + self.scroll_area.setWidgetResizable(True) + layout.addWidget(self.scroll_area) + + # Chart container + self.chart_container = QWidget() + self.scroll_area.setWidget(self.chart_container) + self.chart_layout = QVBoxLayout(self.chart_container) + + activity_window.setLayout(layout) + activity_window.exec_() + + def generate_chart(self, activity_window): + """Fetch session data for the logged-in user and generate a chart.""" + current_user = self.generate_username() # Get the logged-in user + response = requests.get(f"{API_BASE_URL}/sessions", params={"user": current_user}) + + if response.status_code != 200 or not response.json(): + QMessageBox.information(activity_window, "No Data", "No activity data to display.") + return + def parse_duration(duration_str): + """Convert 'HH:MM:SS.ssssss' to total hours as a float.""" + h, m, s = map(float, duration_str.split(":")) # Convert each part to float + return h + (m / 60) + (s / 3600) # Convert to total hours + # Extract session data + sessions = response.json() + timestamps = [s['session_start'] for s in sessions] + durations = [parse_duration(s['total_duration']) for s in sessions] # Convert durations correctly + + # Clear previous chart + for i in reversed(range(self.chart_layout.count())): + widget = self.chart_layout.itemAt(i).widget() + if widget: + widget.deleteLater() + + # Create a Matplotlib figure + fig, ax = plt.subplots(figsize=(15, 6)) + chart_type = self.chart_type.currentText() + + # Generate the selected chart type + if chart_type == "Bar Chart": + ax.bar(timestamps, durations, color='skyblue') + ax.set_title(f'Activity Log for {current_user} (Bar Chart)', fontsize=14) + ax.set_xlabel('Session Start Time', fontsize=12) + ax.set_ylabel('Duration (hours)', fontsize=12) + elif chart_type == "Pie Chart": + ax.pie(durations, labels=timestamps, autopct='%1.1f%%', startangle=90) + ax.set_title(f'Activity Log for {current_user} (Pie Chart)', fontsize=14) + elif chart_type == "Line Chart": + ax.plot(timestamps, durations, marker='o', color='blue') + ax.set_title(f'Activity Log for {current_user} (Line Chart)', fontsize=14) + ax.set_xlabel('Session Start Time', fontsize=12) + ax.set_ylabel('Duration (hours)', fontsize=12) + + # Embed Matplotlib figure into PyQt5 + canvas = FigureCanvas(fig) + self.chart_layout.addWidget(canvas) + canvas.draw() + + def view_logs(self): + """Fetch and display logs only for the logged-in user.""" + # Create the logs window + logs_window = QDialog(self) + logs_window.setWindowTitle("View Logs") + logs_window.setGeometry(100, 100, 800, 600) + + layout = QVBoxLayout(logs_window) + + # Get the logged-in user's username + current_user = self.generate_username() + + # Fetch logs from the API for the current user + url = f"{API_BASE_URL}/logs" + params = {"user": current_user} + response = requests.get(url, params=params) + + if response.status_code == 200: + logs = response.json() + else: + logs = [] + + if not logs: + no_logs_label = QLabel("No logs available for this user.", logs_window) + no_logs_label.setStyleSheet("font-size: 14px; font-weight: bold;") + layout.addWidget(no_logs_label) + logs_window.setLayout(layout) + logs_window.exec_() + return + + # List widget for displaying logs + log_list_widget = QListWidget(logs_window) + for log in logs: + log_list_widget.addItem(f"ID: {log['log_id']}, Timestamp: {log['log_timestamp']}") + + layout.addWidget(log_list_widget) + + #Function to show selected log details + def show_selected_log(): + + selected_item = log_list_widget.currentItem() + if selected_item: + selected_index = log_list_widget.row(selected_item) + log = logs[selected_index] + log_details = f"User: {log['user_id']}\nTimestamp: {log['log_timestamp']}\n\nLog Content:\n{log['log_content']}" + + # Create a QDialog instead of QMessageBox + log_dialog = QDialog(logs_window) + log_dialog.setWindowTitle("Log Details") + log_dialog.setGeometry(200, 200, 700, 500) # Set initial size + log_dialog.setSizeGripEnabled(True) # Enable window resizing + + # Create a QVBoxLayout + layout = QVBoxLayout(log_dialog) + + # Create a QTextEdit (scrollable) to display the log content + log_text_edit = QTextEdit(log_dialog) + log_text_edit.setText(log_details) + log_text_edit.setReadOnly(True) # Make it read-only + log_text_edit.setMinimumSize(600, 400) # Ensure a good default size + + # Add the text edit widget to the layout + layout.addWidget(log_text_edit) + + # Close button + close_button = QPushButton("Close", log_dialog) + close_button.clicked.connect(log_dialog.close) + layout.addWidget(close_button) + + log_dialog.setLayout(layout) + log_dialog.exec_() + + + # View button to show the selected log's details + view_btn = QPushButton("View Selected Log", logs_window) + view_btn.clicked.connect(show_selected_log) + + layout.addWidget(view_btn) + + logs_window.setLayout(layout) + logs_window.exec_() + + + def quit_app(self): + self.close() + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = TrackerApp() + window.show() sys.exit(app.exec_()) \ No newline at end of file diff --git a/src/frontEnd/TrackerTool/tracker.py b/src/TrackerTool/tracker.py similarity index 97% rename from src/frontEnd/TrackerTool/tracker.py rename to src/TrackerTool/tracker.py index df884cda9..c5f94e281 100644 --- a/src/frontEnd/TrackerTool/tracker.py +++ b/src/TrackerTool/tracker.py @@ -1,142 +1,142 @@ -import psutil -import time -from datetime import datetime -import os,glob -import requests -import socket - -# def generate_username(): -# pc_name = socket.gethostname() -# mac_address = '_'.join(f'{(uuid.getnode() >> i) & 0xff:02x}' for i in range(40, -1, -8)) -# return f"{pc_name}_{mac_address}" -from getmac import get_mac_address - -def generate_username(): - pc_name = socket.gethostname() - mac_address = get_mac_address() # Get the real MAC address of the active network interface - if mac_address: - return f"{pc_name}_{mac_address.replace(':', '_')}" - else: - raise Exception("Unable to retrieve the MAC address.") -# API base URL for the Flask app hosted locally or on Render -API_BASE_URL = "https://tooltracker-afxj.onrender.com/" -LOG_DIR = os.path.join(os.getcwd(), "logs") # Dynamically set the log directory - -# Function to send session data to the Flask API -def send_session_to_api(user_id, session_start, session_end, total_duration): - data = { - "user_id": user_id, - "session_start": session_start.strftime('%Y-%m-%d %H:%M:%S'), - "session_end": session_end.strftime('%Y-%m-%d %H:%M:%S'), - "total_duration": f"{total_duration} hours" - } - try: - response = requests.post(f"{API_BASE_URL}/add-session", json=data) - print(f"Session API Response: {response.json()}") - except requests.exceptions.RequestException as e: - print(f"Error sending session data to API: {e}") - -# Function to send log data to the Flask API -def send_log_to_api(user_id, log_timestamp, log_content): - data = { - "user_id": user_id, - "log_timestamp": log_timestamp.strftime('%Y-%m-%d %H:%M:%S'), - "log_content": log_content - } - try: - response = requests.post(f"{API_BASE_URL}/add-log", json=data) - print(f"Log API Response: {response.json()}") - except requests.exceptions.RequestException as e: - print(f"Error sending log data to API: {e}") - -# Ensure log directory exists -def ensure_log_directory(): - if not os.path.exists(LOG_DIR): - print(f"Creating log directory: {LOG_DIR}") - os.makedirs(LOG_DIR) - -# Function to log session details and send them to the API -def log_session(user_id, session_start, session_end): - total_duration = (session_end - session_start).total_seconds() / 3600 # Duration in hours - send_session_to_api(user_id, session_start, session_end, total_duration) - -# Function to store logs and send them to the API -# def store_log(user_id): -# log_file_path = os.path.join(LOG_DIR, f"{user_id}_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt") - -# try: -# # Write some dummy content to simulate eSim logs -# with open(log_file_path, 'w') as file: -# file.write(f"Log initialized for user {user_id} at {datetime.now()}\n") - -# # Read and send the log content -# with open(log_file_path, 'r') as file: -# log_content = file.read() -# send_log_to_api(user_id, datetime.now(), log_content) -# except Exception as e: -# print(f"Error handling log file: {e}") -LOG_DIR = "/home/mmn/Downloads/eSim-2.4/src/frontEnd/logs" - -def store_log(user_id): - """Finds the latest log file for the user and sends it to the API.""" - try: - # Find the latest log file for the user - log_files = sorted( - glob.glob(os.path.join(LOG_DIR, f"{user_id}_log_*.txt")), - key=os.path.getmtime, # Sort by modification time (latest last) - reverse=True # Get latest file first - ) - - if not log_files: - print(f"No log file found for user {user_id}.") - return - - latest_log_file = log_files[0] # Get the most recent log file - - # Read and send the log content - with open(latest_log_file, 'r') as file: - log_content = file.read() - - send_log_to_api(user_id, datetime.now(), log_content) - - except Exception as e: - print(f"Error handling log file: {e}") -# Check if eSim is running -def is_esim_running(): - for process in psutil.process_iter(['name']): - if 'esim' in process.info['name'].lower(): - return True - return False - -# Track user activity -def track_activity(user_id): - session_start = None - ensure_log_directory() - - print(f"Tracking started for user: {user_id}") - try: - while True: - if is_esim_running(): - if session_start is None: - session_start = datetime.now() - print(f"Session started at {session_start}") - else: - if session_start: - session_end = datetime.now() - log_session(user_id, session_start, session_end) - store_log(user_id) - print(f"Session ended at {session_end}") - print(f"Duration: {(session_end - session_start)}") - session_start = None - time.sleep(1) # Check every 2 seconds - except KeyboardInterrupt: - print("Tracking stopped.") - -# Main entry point -if __name__ == "__main__": - user_id = generate_username() - # consent = input("Do you consent to activity tracking? (yes/no): ") - # if consent.lower() == 'yes': - track_activity(user_id) - # else: - print("Tracking aborted. Consent not given.") +import psutil +import time +from datetime import datetime +import os,glob +import requests +import socket + +# def generate_username(): +# pc_name = socket.gethostname() +# mac_address = '_'.join(f'{(uuid.getnode() >> i) & 0xff:02x}' for i in range(40, -1, -8)) +# return f"{pc_name}_{mac_address}" +from getmac import get_mac_address + +def generate_username(): + pc_name = socket.gethostname() + mac_address = get_mac_address() # Get the real MAC address of the active network interface + if mac_address: + return f"{pc_name}_{mac_address.replace(':', '_')}" + else: + raise Exception("Unable to retrieve the MAC address.") +# API base URL for the Flask app hosted locally or on Render +API_BASE_URL = "https://tooltracker-afxj.onrender.com/" +LOG_DIR = os.path.join(os.getcwd(), "logs") # Dynamically set the log directory + +# Function to send session data to the Flask API +def send_session_to_api(user_id, session_start, session_end, total_duration): + data = { + "user_id": user_id, + "session_start": session_start.strftime('%Y-%m-%d %H:%M:%S'), + "session_end": session_end.strftime('%Y-%m-%d %H:%M:%S'), + "total_duration": f"{total_duration} hours" + } + try: + response = requests.post(f"{API_BASE_URL}/add-session", json=data) + print(f"Session API Response: {response.json()}") + except requests.exceptions.RequestException as e: + print(f"Error sending session data to API: {e}") + +# Function to send log data to the Flask API +def send_log_to_api(user_id, log_timestamp, log_content): + data = { + "user_id": user_id, + "log_timestamp": log_timestamp.strftime('%Y-%m-%d %H:%M:%S'), + "log_content": log_content + } + try: + response = requests.post(f"{API_BASE_URL}/add-log", json=data) + print(f"Log API Response: {response.json()}") + except requests.exceptions.RequestException as e: + print(f"Error sending log data to API: {e}") + +# Ensure log directory exists +def ensure_log_directory(): + if not os.path.exists(LOG_DIR): + print(f"Creating log directory: {LOG_DIR}") + os.makedirs(LOG_DIR) + +# Function to log session details and send them to the API +def log_session(user_id, session_start, session_end): + total_duration = (session_end - session_start).total_seconds() / 3600 # Duration in hours + send_session_to_api(user_id, session_start, session_end, total_duration) + +# Function to store logs and send them to the API +# def store_log(user_id): +# log_file_path = os.path.join(LOG_DIR, f"{user_id}_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt") + +# try: +# # Write some dummy content to simulate eSim logs +# with open(log_file_path, 'w') as file: +# file.write(f"Log initialized for user {user_id} at {datetime.now()}\n") + +# # Read and send the log content +# with open(log_file_path, 'r') as file: +# log_content = file.read() +# send_log_to_api(user_id, datetime.now(), log_content) +# except Exception as e: +# print(f"Error handling log file: {e}") +LOG_DIR = "/home/mmn/Downloads/eSim-2.4/src/frontEnd/logs" + +def store_log(user_id): + """Finds the latest log file for the user and sends it to the API.""" + try: + # Find the latest log file for the user + log_files = sorted( + glob.glob(os.path.join(LOG_DIR, f"{user_id}_log_*.txt")), + key=os.path.getmtime, # Sort by modification time (latest last) + reverse=True # Get latest file first + ) + + if not log_files: + print(f"No log file found for user {user_id}.") + return + + latest_log_file = log_files[0] # Get the most recent log file + + # Read and send the log content + with open(latest_log_file, 'r') as file: + log_content = file.read() + + send_log_to_api(user_id, datetime.now(), log_content) + + except Exception as e: + print(f"Error handling log file: {e}") +# Check if eSim is running +def is_esim_running(): + for process in psutil.process_iter(['name']): + if 'esim' in process.info['name'].lower(): + return True + return False + +# Track user activity +def track_activity(user_id): + session_start = None + ensure_log_directory() + + print(f"Tracking started for user: {user_id}") + try: + while True: + if is_esim_running(): + if session_start is None: + session_start = datetime.now() + print(f"Session started at {session_start}") + else: + if session_start: + session_end = datetime.now() + log_session(user_id, session_start, session_end) + store_log(user_id) + print(f"Session ended at {session_end}") + print(f"Duration: {(session_end - session_start)}") + session_start = None + time.sleep(1) # Check every 2 seconds + except KeyboardInterrupt: + print("Tracking stopped.") + +# Main entry point +if __name__ == "__main__": + user_id = generate_username() + # consent = input("Do you consent to activity tracking? (yes/no): ") + # if consent.lower() == 'yes': + track_activity(user_id) + # else: + print("Tracking aborted. Consent not given.") From 195a6bf53d8c3e81e8f4b74a73b0080a01d8908b Mon Sep 17 00:00:00 2001 From: krishnaGoutam Date: Tue, 18 Feb 2025 12:59:33 +0530 Subject: [PATCH 3/5] Modified Tracker Path --- src/frontEnd/Application.py | 130 +++++++++++++++++++----------------- 1 file changed, 70 insertions(+), 60 deletions(-) diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 1dc10dc35..a78253085 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -43,7 +43,7 @@ import subprocess,json,sys,logging from datetime import datetime from multiprocessing import Process - +from TrackerTool.main import TrackerApp LOG_DIR = "logs" @@ -479,21 +479,12 @@ def help_project(self): def tracker_tool(self, event): """ - Opens the Tracker Tool application. + Opens the Tracker Tool application without restarting the event loop. """ - # Dynamically locate the tracker tool script - # base_dir = os.path.dirname(os.path.abspath(__file__)) # Get the current directory of this script - # tracker_script_path = os.path.join(base_dir, "TrackerTool", "main.py") - home_dir = os.path.expanduser("~") # Get the user's home directory - tracker_script_path = os.path.join(home_dir, "Downloads", "eSim-2.4", "src", "TrackerTool", "main.py") try: - # Check if the tracker tool script exists - if os.path.exists(tracker_script_path): - # Use subprocess to run the tracker tool - subprocess.Popen(["python3", tracker_script_path]) - print("Tracker Tool launched successfully.") - else: - print("Tracker Tool script not found:", tracker_script_path) + self.tracker_window = TrackerApp() # Create a new window instance + self.tracker_window.show() # Show the Tracker Tool window + print("Tracker Tool launched successfully.") except Exception as e: print("Error launching Tracker Tool:", e) @@ -858,61 +849,80 @@ def main(args): work = 0 def show_preferences(): - global tracker_thread + + global tracker_thread + if os.name == "nt": # Windows + preferences_file = os.path.join(os.environ["APPDATA"], "eSim", "preferences.json") + else: # Linux/macOS preferences_file = os.path.expanduser("~/.esim/preferences.json") user_preferences = {} - - # Load existing preferences if available - if os.path.exists(preferences_file): - with open(preferences_file, "r") as file: - user_preferences = json.load(file) - - # Show dialog if the user has not chosen to remember preferences - if not user_preferences.get("remember_choice", False): - dialog = UserPreferenceDialog() - if dialog.exec_() == QtWidgets.QDialog.Accepted: - preferences = dialog.getPreferences() - - # Save preferences if the user chose to remember - if preferences["remember_choice"]: - os.makedirs(os.path.dirname(preferences_file), exist_ok=True) - with open(preferences_file, "w") as file: - json.dump(preferences, file) - - # Start session tracking if the user chose to track - if preferences["track_session"]: - print("Session tracking enabled.") - start_tracking(preferences["username"]) # Start tracking in a separate process - else: - print("Session tracking disabled.") + # Load existing preferences if available + if os.path.exists(preferences_file): + with open(preferences_file, "r") as file: + user_preferences = json.load(file) + + # Show dialog if the user has not chosen to remember preferences + if not user_preferences.get("remember_choice", False): + dialog = UserPreferenceDialog() + if dialog.exec_() == QtWidgets.QDialog.Accepted: + preferences = dialog.getPreferences() + + # Save preferences if the user chose to remember + if preferences["remember_choice"]: + os.makedirs(os.path.dirname(preferences_file), exist_ok=True) + with open(preferences_file, "w") as file: + json.dump(preferences, file) + + # Start session tracking if the user chose to track + if preferences["track_session"]: + print("Session tracking enabled.") + start_tracking(preferences["username"]) # Start tracking in a separate process else: - print("User cancelled. Exiting application.") - sys.exit(0) + print("Session tracking disabled.") else: - # Act based on remembered preferences - if user_preferences.get("track_session", False): - print("Session tracking enabled (from remembered preferences).") - start_tracking(user_preferences.get("username", "Unknown")) - else: - print("Session tracking disabled (from remembered preferences).") + print("User cancelled. Exiting application.") + sys.exit(0) + else: + # Act based on remembered preferences + if user_preferences.get("track_session", False): + print("Session tracking enabled (from remembered preferences).") + start_tracking(user_preferences.get("username", "Unknown")) + else: + print("Session tracking disabled (from remembered preferences).") + + def start_tracking(username): """ - Start tracking the session by launching tracker.py as a completely independent process. + Start tracking the session by launching tracker.py as a separate process. """ - home_dir = os.path.expanduser("~") - tracker_script = os.path.join(home_dir, "Downloads", "eSim-2.4", "src", "TrackerTool", "tracker.py") - #command = [sys.executable, "tracker.py", username] - command = [sys.executable, tracker_script, username] - - if sys.platform.startswith("win"): - # Windows: DETACHED_PROCESS ensures process runs independently - DETACHED_PROCESS = 0x00000008 - subprocess.Popen(command, creationflags=DETACHED_PROCESS, close_fds=True) - else: - # Linux/macOS: Use setsid to detach process - subprocess.Popen(command, preexec_fn=os.setsid, close_fds=True) + try: + # Get the current working directory (from where the script is executed) + current_dir = os.getcwd() + + # Move up one directory to the parent directory and then navigate to TrackerTool + parent_dir = os.path.dirname(current_dir) # Go one directory up + tracker_script = os.path.join(parent_dir,"TrackerTool", "tracker.py") + + if not os.path.exists(tracker_script): + raise FileNotFoundError(f"Tracker script not found at: {tracker_script}") + + # The command to run tracker.py as an independent process + command = [sys.executable, tracker_script, username] + + if sys.platform.startswith("win"): + # Windows: DETACHED_PROCESS ensures process runs independently + DETACHED_PROCESS = 0x00000008 + subprocess.Popen(command, creationflags=DETACHED_PROCESS, close_fds=True) + else: + # Linux/macOS: Use setsid to detach process + subprocess.Popen(command, preexec_fn=os.setsid, close_fds=True) + + print(f"Tracker started successfully for user: {username}") + + except Exception as e: + print("Error starting tracker:", e) def after_workspace_selection(): splash.close() From e90ebc91562fa9ce67aa175e8fddb0c5584c8e24 Mon Sep 17 00:00:00 2001 From: krishnaGoutam Date: Tue, 18 Feb 2025 14:42:27 +0530 Subject: [PATCH 4/5] update --- src/TrackerTool/tracker.py | 3 ++- src/frontEnd/tracker.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/TrackerTool/tracker.py b/src/TrackerTool/tracker.py index c5f94e281..d66339cca 100644 --- a/src/TrackerTool/tracker.py +++ b/src/TrackerTool/tracker.py @@ -75,7 +75,8 @@ def log_session(user_id, session_start, session_end): # send_log_to_api(user_id, datetime.now(), log_content) # except Exception as e: # print(f"Error handling log file: {e}") -LOG_DIR = "/home/mmn/Downloads/eSim-2.4/src/frontEnd/logs" +# LOG_DIR = "/home/mmn/Downloads/eSim-2.4/src/frontEnd/logs" +LOG_DIR = os.path.join(os.getcwd(), "logs") def store_log(user_id): """Finds the latest log file for the user and sends it to the API.""" diff --git a/src/frontEnd/tracker.py b/src/frontEnd/tracker.py index 3aa870256..93ab42890 100644 --- a/src/frontEnd/tracker.py +++ b/src/frontEnd/tracker.py @@ -57,7 +57,9 @@ def log_session(user_id, session_start, session_end): total_duration = (session_end - session_start).total_seconds() / 3600 # Duration in hours send_session_to_api(user_id, session_start, session_end, total_duration) -LOG_DIR = "/home/mmn/Downloads/eSim-2.4/src/frontEnd/logs" +# LOG_DIR = "/home/mmn/Downloads/eSim-2.4/src/frontEnd/logs" +LOG_DIR = os.path.join(os.getcwd(), "logs") + def store_log(user_id): """Finds the latest log file for the user and sends it to the API.""" From e768ea22f670ead916da1683f1a097aafdfb0faf Mon Sep 17 00:00:00 2001 From: krishnaGoutam Date: Mon, 24 Mar 2025 17:53:27 +0530 Subject: [PATCH 5/5] Implemented a Tracker Tool for eSim This Tracker tool will monitor user activity for eSim. --- src/frontEnd/Application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index a78253085..6aa94937d 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -814,7 +814,7 @@ def __init__(self, *args): def main(args): user_id = tracker.generate_username() log_capture(user_id) - """ + """"" The splash screen opened at the starting of screen is performed by this function. """