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,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>