improve structure; move stuff to relevant folders

This commit is contained in:
2025-12-31 14:53:07 +02:00
parent e4296a3251
commit e405894cc2
34 changed files with 1452 additions and 80 deletions

View File

@@ -0,0 +1,235 @@
#!/bin/bash
# Enhanced Autoclicker script for Sway with improved antiban features
# Uses ydotool for Wayland/Sway environments
# Features human-like timing patterns and CSV logging for visualization
# Press Ctrl+C to stop (standard shell interrupt)
# Requires ydotool and ydotoold daemon running
# Configuration
MIN_DELAY=50 # Minimum delay in milliseconds
MAX_DELAY=700 # Maximum delay in milliseconds
MIN_CLICKS_BEFORE_BREAK=30 # Minimum clicks before break
MAX_CLICKS_BEFORE_BREAK=200 # Maximum clicks before break
MIN_BREAK=3 # Minimum break duration in seconds
MAX_BREAK=15 # Maximum break duration in seconds
# Human-like timing parameters
FASTIGUE_FACTOR=0.95 # Gradually slow down over time (0.95 = 5% slower)
REACTION_VARIATION=0.3 # 30% variation in reaction times
PATTERN_BREAK_CHANCE=0.05 # 5% chance to break pattern
# File paths
STATE_FILE="/tmp/autoclicker_running"
LOCK_FILE="/tmp/autoclicker_lock"
LOG_DIR="/tmp/autoclicker_logs"
LOG_FILE="$LOG_DIR/click_timings_$(date +%Y%m%d_%H%M%S).csv"
# Create log directory if it doesn't exist
mkdir -p "$LOG_DIR"
# Initialize CSV log file with header
echo "click_number,delay_ms,timestamp" > "$LOG_FILE"
# Check if ydotool is available
if ! command -v ydotool &> /dev/null; then
echo "Error: ydotool is not installed. Install it with: sudo pacman -S ydotool"
exit 1
fi
# Check if bc is available (needed for calculations)
if ! command -v bc &> /dev/null; then
echo "Error: bc is not installed. Install it with: sudo pacman -S bc"
exit 1
fi
# Check if script is already running (prevent multiple instances)
if [ -f "$LOCK_FILE" ]; then
echo "Autoclicker is already running!"
echo "Press Ctrl+C in the terminal where it's running to stop it."
# Clean up the incomplete log file that was just created
rm -f "$LOG_FILE"
exit 0
fi
# Create lock file
echo $$ > "$LOCK_FILE"
# Global exit flag
EXIT_REQUESTED=0
# Clean up on exit
cleanup() {
EXIT_REQUESTED=1
rm -f "$LOCK_FILE"
rm -f "$STATE_FILE"
echo ""
echo "Autoclicker stopped."
exit 0
}
# Set up traps for cleanup
trap cleanup INT TERM
# Check if ydotoold daemon is running
if ! pgrep -x "ydotoold" > /dev/null; then
echo "Starting ydotoold daemon..."
sudo ydotoold &
sleep 1
fi
# Create state file to indicate running
echo "running" > "$STATE_FILE"
# Function to log click timing to CSV
log_click_timing() {
local click_number=$1
local delay_ms=$2
local timestamp=$(date +%s%3N) # Milliseconds since epoch
echo "$click_number,$delay_ms,$timestamp" >> "$LOG_FILE"
}
# Function to generate random delay with human-like distribution
get_human_delay() {
local min=$1
local max=$2
local base_delay=$((min + RANDOM % (max - min + 1)))
# Apply reaction time variation (30% up or down)
local variation=$(echo "scale=3; $base_delay * $REACTION_VARIATION * (2 * $RANDOM / 32767 - 1)" | bc)
local varied_delay=$(echo "$base_delay + $variation" | bc)
# Ensure we stay within reasonable bounds
if (( $(echo "$varied_delay < $min" | bc -l) )); then
varied_delay=$min
elif (( $(echo "$varied_delay > $max" | bc -l) )); then
varied_delay=$max
fi
# Convert to seconds for sleep function
echo "scale=3; $varied_delay / 1000" | bc
}
# Function to generate random number of clicks before break
get_random_clicks() {
local min=$1
local max=$2
echo $((min + RANDOM % (max - min + 1)))
}
# Function to sleep with interrupt checking
interruptible_sleep() {
local duration=$1
local start_time=$(date +%s.%N)
local end_time=$(echo "$start_time + $duration" | bc)
while true; do
if [ $EXIT_REQUESTED -eq 1 ]; then
return 1 # Exit requested
fi
local current_time=$(date +%s.%N)
if (( $(echo "$current_time >= $end_time" | bc -l) )); then
return 0 # Sleep completed
fi
sleep 0.1
done
}
# Function to apply fatigue effect (gradually slow down)
apply_fatigue() {
local current_delay=$1
local click_count=$2
local fatigue_effect=$(echo "scale=3; $current_delay * (1 + $FASTIGUE_FACTOR * $click_count / 1000)" | bc)
echo "$fatigue_effect"
}
# Function to randomly break pattern (5% chance)
should_break_pattern() {
local threshold=$(echo "$PATTERN_BREAK_CHANCE * 32767" | bc | cut -d'.' -f1)
[ $RANDOM -lt $threshold ]
}
echo "=========================================="
echo "Enhanced Autoclicker started!"
echo "=========================================="
echo "Configuration:"
echo " - Click delay: ${MIN_DELAY}-${MAX_DELAY}ms (human-like variation)"
echo " - Break every: ${MIN_CLICKS_BEFORE_BREAK}-${MAX_CLICKS_BEFORE_BREAK} clicks (random)"
echo " - Break duration: ${MIN_BREAK}-${MAX_BREAK}s"
echo " - Fatigue factor: ${FASTIGUE_FACTOR} (gradual slowdown)"
echo " - Pattern break chance: ${PATTERN_BREAK_CHANCE} (5%)"
echo " - Logging to: $LOG_FILE"
echo ""
echo "Press Ctrl+C to stop"
echo "=========================================="
echo ""
click_count=0
total_clicks=0
clicks_before_break=$(get_random_clicks $MIN_CLICKS_BEFORE_BREAK $MAX_CLICKS_BEFORE_BREAK)
echo "Next break will be after $clicks_before_break clicks"
echo ""
while true; do
if [ $EXIT_REQUESTED -eq 1 ]; then
echo "Exit signal received!"
break
fi
# Generate base random delay
base_delay=$(get_human_delay $MIN_DELAY $MAX_DELAY)
# Apply fatigue effect (gradually slow down)
final_delay=$(apply_fatigue "$base_delay" "$click_count")
# Randomly break pattern (5% chance)
if should_break_pattern; then
# Add a longer, unpredictable delay
pattern_break_delay=$(echo "scale=3; ($MAX_DELAY * 2 + $RANDOM % 1000) / 1000" | bc)
echo "[Pattern Break] Adding unpredictable delay of $(echo "$pattern_break_delay * 1000" | bc | cut -d'.' -f1)ms"
if ! interruptible_sleep "$pattern_break_delay"; then
break
fi
fi
# Perform click (suppress ydotool output)
ydotool click 0xC0 2>/dev/null
click_count=$((click_count + 1))
total_clicks=$((total_clicks + 1))
# Convert delay to milliseconds for display
delay_ms=$(echo "$final_delay * 1000" | bc | cut -d'.' -f1)
# Log click timing to CSV
log_click_timing "$total_clicks" "$delay_ms"
# Show each click with its delay
echo "Click #$total_clicks (delay: ${delay_ms}ms, next break in $((clicks_before_break - click_count)) clicks)"
# Check if it's time for a break
if [ $click_count -ge $clicks_before_break ]; then
break_duration=$(get_human_delay $((MIN_BREAK * 1000)) $((MAX_BREAK * 1000)))
echo ""
echo "[Break] Completed $clicks_before_break clicks (total: $total_clicks). Taking a break for ${break_duration}s..."
if ! interruptible_sleep "$break_duration"; then
break
fi
click_count=0
# Generate new random click count for next cycle
clicks_before_break=$(get_random_clicks $MIN_CLICKS_BEFORE_BREAK $MAX_CLICKS_BEFORE_BREAK)
echo "[Resumed] Next break will be after $clicks_before_break clicks"
echo ""
else
# Wait the calculated delay before next click
if ! interruptible_sleep "$final_delay"; then
break
fi
fi
done
echo "Click timings saved to: $LOG_FILE"

View File

@@ -0,0 +1,619 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Autoclicker Timing Visualizer</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
.controls {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.control-group {
flex: 1;
min-width: 250px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #2c3e50;
}
input, select, button {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
}
button {
background-color: #3498db;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
font-weight: 600;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 200px;
background-color: #ecf0f1;
padding: 15px;
border-radius: 6px;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
margin: 5px 0;
}
.stat-label {
font-size: 12px;
color: #7f8c8d;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.chart-container {
background-color: white;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: relative;
height: 400px;
}
.chart-title {
font-weight: 600;
margin-bottom: 10px;
color: #2c3e50;
}
.chart-wrapper {
position: relative;
height: 300px;
width: 100%;
}
.file-list {
margin-top: 20px;
}
.file-item {
padding: 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #f8f9fa;
}
.file-item.active {
background-color: #e3f2fd;
font-weight: 600;
}
.file-name {
color: #3498db;
margin-bottom: 3px;
}
.file-info {
font-size: 12px;
color: #7f8c8d;
}
.loading {
text-align: center;
padding: 20px;
color: #7f8c8d;
font-style: italic;
}
.error {
color: #e74c3c;
padding: 10px;
background-color: #fadbd8;
border-radius: 4px;
margin: 10px 0;
}
.server-info {
background-color: #e8f5e9;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
border-left: 4px solid #4caf50;
}
.api-status {
background-color: #fff3cd;
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
display: none;
}
.api-status.success {
background-color: #d4edda;
color: #155724;
display: block;
}
.api-status.error {
background-color: #f8d7da;
color: #721c24;
display: block;
}
@media (max-width: 768px) {
.controls, .stats {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🖱️ Autoclicker Timing Visualizer</h1>
<div class="server-info">
<strong>📊 Real-time Data Server</strong>
<p>This visualizer connects to a local Python server that serves real log files from <code>/tmp/autoclicker_logs/</code></p>
<p>Make sure to run: <code>python3 click_server.py</code> in a separate terminal</p>
</div>
<div id="apiStatus" class="api-status">
Testing API connection...
</div>
<div class="controls">
<div class="control-group">
<label for="logFile">Select Log File:</label>
<select id="logFile">
<option value="">-- Loading log files... --</option>
</select>
</div>
<div class="control-group">
<label for="chartType">Chart Type:</label>
<select id="chartType">
<option value="scatter">Scatter Plot</option>
<option value="line">Line Chart</option>
<option value="histogram">Histogram</option>
</select>
</div>
<div class="control-group">
<label>&nbsp;</label>
<button id="loadBtn" disabled>Load Data</button>
</div>
</div>
<div class="stats" id="statsContainer" style="display: none;">
<div class="stat-card">
<div class="stat-label">Total Clicks</div>
<div class="stat-value" id="totalClicks">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Average Delay</div>
<div class="stat-value" id="avgDelay">0 ms</div>
</div>
<div class="stat-card">
<div class="stat-label">Min Delay</div>
<div class="stat-value" id="minDelay">0 ms</div>
</div>
<div class="stat-card">
<div class="stat-label">Max Delay</div>
<div class="stat-value" id="maxDelay">0 ms</div>
</div>
<div class="stat-card">
<div class="stat-label">Std Deviation</div>
<div class="stat-value" id="stdDev">0 ms</div>
</div>
</div>
<div class="chart-container">
<div class="chart-title">Click Timing Distribution</div>
<div class="chart-wrapper">
<canvas id="timingChart"></canvas>
</div>
</div>
<div class="chart-container">
<div class="chart-title">Delay Over Time</div>
<div class="chart-wrapper">
<canvas id="timeSeriesChart"></canvas>
</div>
</div>
<div class="file-list">
<h3>Available Log Files</h3>
<div id="fileList">
<div class="loading">Loading log files from server...</div>
</div>
</div>
</div>
<!-- Load Chart.js with date adapter -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0"></script>
<script>
// Global variables
let clickData = [];
let timingChart, timeSeriesChart;
let currentFile = '';
// API base URL
const API_BASE = '/api';
// DOM elements
const logFileSelect = document.getElementById('logFile');
const chartTypeSelect = document.getElementById('chartType');
const loadBtn = document.getElementById('loadBtn');
const fileList = document.getElementById('fileList');
const statsContainer = document.getElementById('statsContainer');
const apiStatus = document.getElementById('apiStatus');
// Test API connection
async function testAPIConnection() {
try {
const response = await fetch(`${API_BASE}/logs`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (response.ok) {
apiStatus.textContent = "✅ API connected successfully!";
apiStatus.className = "api-status success";
return true;
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
apiStatus.textContent = `❌ API connection failed: ${error.message}. Is the Python server running?`;
apiStatus.className = "api-status error";
console.error('API connection error:', error);
return false;
}
}
// Initialize charts with proper configuration
function initCharts() {
const timingCtx = document.getElementById('timingChart').getContext('2d');
const timeSeriesCtx = document.getElementById('timeSeriesChart').getContext('2d');
// Destroy existing charts if they exist
if (timingChart) timingChart.destroy();
if (timeSeriesChart) timeSeriesChart.destroy();
timingChart = new Chart(timingCtx, {
type: 'scatter',
data: { datasets: [] },
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
title: { display: true, text: 'Click Number' }
},
y: {
type: 'linear',
title: { display: true, text: 'Delay (ms)' }
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `Click ${context.raw.x}: ${context.raw.y}ms`;
}
}
}
}
}
});
timeSeriesChart = new Chart(timeSeriesCtx, {
type: 'line',
data: { datasets: [] },
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
title: { display: true, text: 'Click Number' }
},
y: {
type: 'linear',
title: { display: true, text: 'Delay (ms)' }
}
}
}
});
}
// Load available log files from server
async function loadLogFiles() {
try {
fileList.innerHTML = '<div class="loading">Loading log files...</div>';
logFileSelect.innerHTML = '<option value="">-- Loading... --</option>';
const response = await fetch(`${API_BASE}/logs`);
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
const files = data.logs;
if (files.length === 0) {
fileList.innerHTML = '<div class="error">No log files found. Run the enhanced autoclicker first.</div>';
logFileSelect.innerHTML = '<option value="">-- No log files available --</option>';
return;
}
// Populate select dropdown
logFileSelect.innerHTML = '<option value="">-- Select a log file --</option>';
files.forEach(file => {
const option = document.createElement('option');
option.value = file.name;
option.textContent = `${file.name} (${file.date})`;
logFileSelect.appendChild(option);
});
// Populate file list
fileList.innerHTML = files.map(file => `
<div class="file-item" data-file="${file.name}">
<div class="file-name">${file.name}</div>
<div class="file-info">Size: ${file.size} | Created: ${file.date}</div>
</div>
`).join('');
// Add click handlers to file items
document.querySelectorAll('.file-item').forEach(item => {
item.addEventListener('click', function() {
document.querySelectorAll('.file-item').forEach(i => i.classList.remove('active'));
this.classList.add('active');
logFileSelect.value = this.dataset.file;
loadBtn.disabled = false;
});
});
} catch (error) {
console.error('Error loading log files:', error);
fileList.innerHTML = `<div class="error">Error loading log files: ${error.message}</div>`;
logFileSelect.innerHTML = '<option value="">-- Error loading files --</option>';
}
}
// Load and parse JSON data from server
async function loadData() {
const fileName = logFileSelect.value;
if (!fileName) return;
currentFile = fileName;
loadBtn.disabled = true;
loadBtn.textContent = 'Loading...';
try {
const response = await fetch(`${API_BASE}/log/${encodeURIComponent(fileName)}`);
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const data = await response.json();
if (data.error) {
if (data.content_preview) {
fileList.innerHTML = `<div class="error">Invalid JSON in ${fileName}: ${data.error}<br><br>File content preview:<pre>${data.content_preview}</pre></div>`;
} else {
fileList.innerHTML = `<div class="error">Error loading ${fileName}: ${data.error}</div>`;
}
return;
}
if (!data.valid) {
fileList.innerHTML = `<div class="error">File ${fileName} is not valid JSON: ${data.error || 'Unknown error'}</div>`;
return;
}
clickData = data.data;
updateCharts();
updateStats();
statsContainer.style.display = 'flex';
} catch (error) {
console.error('Error loading data:', error);
fileList.innerHTML = `<div class="error">Error loading ${fileName}: ${error.message}</div>`;
} finally {
loadBtn.disabled = false;
loadBtn.textContent = 'Load Data';
}
}
// Update charts with current data
function updateCharts() {
if (clickData.length === 0) return;
// Update timing chart based on selected type
const chartType = chartTypeSelect.value;
if (chartType === 'scatter' || chartType === 'line') {
const dataPoints = clickData.map(item => ({
x: item.click,
y: item.delay_ms
}));
timingChart.data.datasets = [{
label: 'Click Delays',
data: dataPoints,
type: chartType,
backgroundColor: 'rgba(52, 152, 219, 0.5)',
borderColor: 'rgba(52, 152, 219, 1)',
pointRadius: chartType === 'scatter' ? 4 : 2,
tension: chartType === 'line' ? 0.3 : 0,
pointStyle: 'circle'
}];
timingChart.update();
} else if (chartType === 'histogram') {
// Create histogram data
const delays = clickData.map(item => item.delay_ms);
const bins = createHistogram(delays, 20);
const histogramData = bins.map((count, i) => ({
x: i * (700 / 20) + 50,
y: count
}));
timingChart.data.datasets = [{
label: 'Delay Distribution',
data: histogramData,
type: 'bar',
backgroundColor: 'rgba(52, 152, 219, 0.7)',
borderColor: 'rgba(52, 152, 219, 1)',
barPercentage: 0.9
}];
timingChart.options.scales.x.title.text = 'Delay Range (ms)';
timingChart.options.scales.y.title.text = 'Frequency';
timingChart.update();
}
// Update time series chart (using click number instead of time for simplicity)
const timeSeriesData = clickData.map(item => ({
x: item.click,
y: item.delay_ms
}));
timeSeriesChart.data.datasets = [{
label: 'Delay Over Time',
data: timeSeriesData,
borderColor: 'rgba(46, 204, 113, 1)',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
tension: 0.3,
pointRadius: 2,
pointStyle: 'circle'
}];
timeSeriesChart.update();
}
// Create histogram from data
function createHistogram(data, binCount) {
if (data.length === 0) return [];
const min = Math.min(...data);
const max = Math.max(...data);
const binSize = (max - min) / binCount;
const bins = new Array(binCount).fill(0);
data.forEach(value => {
const binIndex = Math.min(Math.floor((value - min) / binSize), binCount - 1);
bins[binIndex]++;
});
return bins;
}
// Update statistics
function updateStats() {
if (clickData.length === 0) return;
const delays = clickData.map(item => item.delay_ms);
const total = delays.length;
const sum = delays.reduce((a, b) => a + b, 0);
const avg = sum / total;
const min = Math.min(...delays);
const max = Math.max(...delays);
// Calculate standard deviation
const variance = delays.reduce((sq, n) => sq + Math.pow(n - avg, 2), 0) / total;
const stdDev = Math.sqrt(variance);
document.getElementById('totalClicks').textContent = total;
document.getElementById('avgDelay').textContent = `${avg.toFixed(1)} ms`;
document.getElementById('minDelay').textContent = `${min} ms`;
document.getElementById('maxDelay').textContent = `${max} ms`;
document.getElementById('stdDev').textContent = `${stdDev.toFixed(1)} ms`;
}
// Event listeners
logFileSelect.addEventListener('change', function() {
loadBtn.disabled = !this.value;
});
chartTypeSelect.addEventListener('change', updateCharts);
loadBtn.addEventListener('click', loadData);
// Initialize
initCharts();
// Test API connection first
testAPIConnection().then(isConnected => {
if (isConnected) {
loadLogFiles();
// Auto-refresh log list every 10 seconds
setInterval(loadLogFiles, 10000);
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""
Simple web server for autoclicker visualization
Serves the HTML visualizer and provides API endpoints for CSV log files
Converts CSV to JSON for the frontend
"""
import os
import json
import csv
import http.server
import socketserver
from urllib.parse import urlparse, parse_qs
from datetime import datetime
PORT = 8661
LOG_DIR = "/tmp/autoclicker_logs"
class ClickServerHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=".", **kwargs)
def do_GET(self):
# Handle API requests
if self.path.startswith('/api/'):
self.handle_api_request()
return
# Serve the visualizer HTML for root path
if self.path == '/' or self.path == '/index.html':
self.path = '/click-visualizer.html'
# Serve static files normally
return super().do_GET()
def handle_api_request(self):
"""Handle API endpoints for log file operations"""
try:
if self.path.startswith('/api/logs'):
self.handle_logs_request()
elif self.path.startswith('/api/log/'):
self.handle_log_file_request()
else:
self.send_error(404, "API endpoint not found")
except Exception as e:
self.send_error(500, f"Server error: {str(e)}")
def handle_logs_request(self):
"""Return list of available log files"""
if not os.path.exists(LOG_DIR):
self.send_json_response({"error": "Log directory not found", "logs": []})
return
try:
files = []
for filename in sorted(os.listdir(LOG_DIR), reverse=True):
if filename.endswith('.csv'):
filepath = os.path.join(LOG_DIR, filename)
stat = os.stat(filepath)
# Check if CSV is valid
if self.is_valid_csv(filepath):
files.append({
"name": filename,
"size": self.format_file_size(stat.st_size),
"date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
"path": filename,
"valid": True
})
else:
files.append({
"name": filename,
"size": self.format_file_size(stat.st_size),
"date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
"path": filename,
"valid": False,
"error": "Invalid CSV format"
})
self.send_json_response({"logs": files})
except Exception as e:
self.send_json_response({"error": str(e), "logs": []})
def handle_log_file_request(self):
"""Return contents of a specific log file as JSON"""
# Extract filename from path
parts = self.path.split('/')
if len(parts) < 4:
self.send_error(400, "Invalid log file request")
return
filename = parts[3]
filepath = os.path.join(LOG_DIR, filename)
if not os.path.exists(filepath):
self.send_error(404, "Log file not found")
return
try:
if filename.endswith('.csv'):
data = self.csv_to_json(filepath)
if data is None:
self.send_json_response({
"error": "Failed to parse CSV file",
"filename": filename,
"valid": False
})
return
self.send_json_response({
"data": data,
"filename": filename,
"valid": True
})
else:
self.send_error(400, "Unsupported file format")
except Exception as e:
self.send_error(500, f"Error reading log file: {str(e)}")
def csv_to_json(self, filepath):
"""Convert CSV file to JSON array"""
try:
data = []
with open(filepath, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
data.append({
"click": int(row["click_number"]),
"delay_ms": int(row["delay_ms"]),
"timestamp": int(row["timestamp"])
})
return data
except Exception as e:
print(f"Error parsing CSV {filepath}: {str(e)}")
return None
def is_valid_csv(self, filepath):
"""Check if a file contains valid CSV"""
try:
with open(filepath, 'r') as f:
reader = csv.DictReader(f)
# Try to read first row
next(reader)
return True
except Exception:
return False
def send_json_response(self, data):
"""Send JSON response with proper headers"""
response = json.dumps(data, indent=2)
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Content-Length', str(len(response)))
self.end_headers()
self.wfile.write(response.encode())
def format_file_size(self, size_bytes):
"""Format file size in human-readable format"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f} TB"
def run_server():
"""Start the web server"""
print(f"Starting autoclicker visualization server on port {PORT}")
print(f"Log directory: {LOG_DIR}")
print("Open your browser to: http://localhost:8661")
print("Press Ctrl+C to stop the server")
try:
with socketserver.TCPServer(("", PORT), ClickServerHandler) as httpd:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nServer stopped")
except Exception as e:
print(f"Server error: {str(e)}")
if __name__ == "__main__":
run_server()

View File

@@ -0,0 +1,71 @@
#!/bin/bash
# Script to visualize autoclicker log files
# Starts Python server and opens the HTML visualization tool
VISUALIZER_FILE="click-visualizer.html"
LOG_DIR="/tmp/autoclicker_logs"
SERVER_SCRIPT="click_server.py"
SERVER_PORT=8661
# Check if visualization file exists
if [ ! -f "$VISUALIZER_FILE" ]; then
echo "Error: Visualization tool not found at $VISUALIZER_FILE"
exit 1
fi
# Check if server script exists
if [ ! -f "$SERVER_SCRIPT" ]; then
echo "Error: Server script not found at $SERVER_SCRIPT"
exit 1
fi
# Check if Python is available
if ! command -v python3 &> /dev/null; then
echo "Error: Python 3 is not installed. Install it with: sudo pacman -S python"
exit 1
fi
# Check if we have any log files
if [ ! -d "$LOG_DIR" ] || [ -z "$(ls -A "$LOG_DIR")" ]; then
echo "No autoclicker log files found in $LOG_DIR"
echo "Run the enhanced autoclicker first to generate data."
echo ""
echo "Starting server anyway (you can generate logs while it's running)..."
fi
echo "Starting autoclicker visualization server..."
echo "Server will be available at: http://localhost:$SERVER_PORT"
echo "Log directory: $LOG_DIR"
echo ""
# Start the Python server in the background
python3 "$SERVER_SCRIPT" &
SERVER_PID=$!
# Give the server a moment to start
sleep 2
# Check if server started successfully
if ! ps -p $SERVER_PID > /dev/null; then
echo "Error: Failed to start server"
exit 1
fi
# Open browser
if command -v xdg-open &> /dev/null; then
xdg-open "http://localhost:$SERVER_PORT"
elif command -v open &> /dev/null; then
open "http://localhost:$SERVER_PORT"
else
echo "Server started successfully!"
echo "Open your browser and navigate to: http://localhost:$SERVER_PORT"
fi
echo ""
echo "Server is running in the background (PID: $SERVER_PID)"
echo "Press Ctrl+C to stop the server when you're done"
echo ""
# Wait for server to finish (it will run until manually stopped)
wait $SERVER_PID