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.")