diff --git a/images/tracker.png b/images/tracker.png new file mode 100644 index 000000000..3d772fa4c Binary files /dev/null and b/images/tracker.png differ diff --git a/src/TrackerTool/app.py b/src/TrackerTool/app.py new file mode 100644 index 000000000..7bca9a07c --- /dev/null +++ b/src/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/TrackerTool/main.py b/src/TrackerTool/main.py new file mode 100644 index 000000000..8f126c533 --- /dev/null +++ b/src/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/TrackerTool/tracker.py b/src/TrackerTool/tracker.py new file mode 100644 index 000000000..d66339cca --- /dev/null +++ b/src/TrackerTool/tracker.py @@ -0,0 +1,143 @@ +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" +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.""" + 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/Application.py b/src/frontEnd/Application.py index 5d76bf9d7..6aa94937d 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 +from TrackerTool.main import TrackerApp + + +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,17 @@ 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 without restarting the event loop. + """ + try: + 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) @QtCore.pyqtSlot(QtCore.QProcess.ExitStatus, int) def plotSimulationData(self, exitCode, exitStatus): @@ -715,7 +812,9 @@ 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 +848,97 @@ def main(args): except IOError: work = 0 + def show_preferences(): + + 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.") + 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 separate process. + """ + 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() + 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/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..93ab42890 --- /dev/null +++ b/src/frontEnd/tracker.py @@ -0,0 +1,126 @@ +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" +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.""" + 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.")