// ==UserScript==
// @name Are We There Yet - Time Estimator
// @namespace http://tampermonkey.net/
// @version 2.3
// @description Estimates completion time based on progress rate
// @author You
// @match https://are-we-there-yet.hackclub.com/*
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'awtyt_progress_history';
const CHECK_INTERVAL = 5000; // Check every 5 seconds
const MIN_DATA_POINTS = 3; // Minimum data points needed for estimation
const RELOAD_INTERVAL = 60000; // Reload page every 60 seconds (1 minute)
const RELOAD_KEY = 'awtyt_auto_reload';
// Get current progress percentage from the page
function getCurrentProgress() {
// Try the progress element first
const progressEl = document.getElementById('progress');
if (progressEl && progressEl.value) {
return parseFloat(progressEl.value);
}
// Fallback to parsing the label text
const labelEl = document.querySelector('label[for="progress"]');
if (labelEl) {
const match = labelEl.textContent.match(/(\d+\.?\d*)%/);
if (match) {
return parseFloat(match[1]);
}
}
return null;
}
// Load progress history from storage
function loadHistory() {
const stored = GM_getValue(STORAGE_KEY, '[]');
return JSON.parse(stored);
}
// Save progress history to storage
function saveHistory(history) {
GM_setValue(STORAGE_KEY, JSON.stringify(history));
}
// Calculate estimated completion time
function calculateEstimation(history) {
if (history.length < MIN_DATA_POINTS) {
return null;
}
// Use linear regression to estimate rate of change
const n = history.length;
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
// Use only recent data points (last 20) for better accuracy
const recentHistory = history.slice(-20);
const baseTime = recentHistory[0].timestamp;
recentHistory.forEach((point) => {
const x = (point.timestamp - baseTime) / 1000; // seconds since first point
const y = point.progress;
sumX += x;
sumY += y;
sumXY += x * y;
sumX2 += x * x;
});
const slope = (recentHistory.length * sumXY - sumX * sumY) /
(recentHistory.length * sumX2 - sumX * sumX);
const intercept = (sumY - slope * sumX) / recentHistory.length;
// If slope is zero or negative, we can't estimate
if (slope <= 0) {
return null;
}
const currentProgress = recentHistory[recentHistory.length - 1].progress;
const currentTime = recentHistory[recentHistory.length - 1].timestamp;
const timeElapsed = (currentTime - baseTime) / 1000;
// Calculate when we'll reach 100%
const timeToCompletion = (100 - currentProgress) / slope;
const estimatedCompletionTime = new Date(currentTime + timeToCompletion * 1000);
return {
slope: slope,
currentProgress: currentProgress,
estimatedCompletionTime: estimatedCompletionTime,
timeRemaining: timeToCompletion
};
}
// Format time remaining
function formatTimeRemaining(seconds) {
if (seconds < 0) return "Completed!";
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
const parts = [];
if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
return parts.join(' ');
}
// Create and inject the full history graph into the page
function createPageGraph() {
// Check if already exists
if (document.getElementById('awtyt-page-graph-container')) {
return;
}
const progressBar = document.getElementById('progress');
if (!progressBar) {
return; // Progress bar not found yet
}
const container = document.createElement('div');
container.id = 'awtyt-page-graph-container';
container.style.cssText = `
margin: 3rem 0 2rem 0;
`;
container.innerHTML = `
Progress History
`;
// Insert after the progress bar
progressBar.parentNode.insertBefore(container, progressBar.nextSibling);
}
// Create and inject the UI
function createUI() {
const container = document.createElement('div');
container.id = 'awtyt-estimator';
container.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.9);
color: #00ff00;
padding: 15px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 14px;
z-index: 10000;
min-width: 350px;
max-width: 90vw;
box-shadow: 0 0 10px rgba(0, 255, 0, 0.3);
`;
container.innerHTML = `
⏱️ Progress Tracker
Collecting data...
0 data points collected
`;
document.body.appendChild(container);
// Add reset button handler
document.getElementById('awtyt-reset').addEventListener('click', () => {
saveHistory([]);
updateUI();
});
// Load auto-reload preference
const autoReloadEnabled = GM_getValue(RELOAD_KEY + '_enabled', false);
const reloadInterval = GM_getValue(RELOAD_KEY + '_interval', 60);
document.getElementById('awtyt-autoreload').checked = autoReloadEnabled;
document.getElementById('awtyt-reload-interval').value = reloadInterval;
// Auto-reload checkbox handler
document.getElementById('awtyt-autoreload').addEventListener('change', (e) => {
GM_setValue(RELOAD_KEY + '_enabled', e.target.checked);
if (e.target.checked) {
setupAutoReload();
} else {
clearAutoReload();
}
});
// Reload interval input handler
document.getElementById('awtyt-reload-interval').addEventListener('change', (e) => {
const interval = parseInt(e.target.value);
if (interval >= 30 && interval <= 600) {
GM_setValue(RELOAD_KEY + '_interval', interval);
if (document.getElementById('awtyt-autoreload').checked) {
clearAutoReload();
setupAutoReload();
}
}
});
}
// Thin out data points for display based on time window
function thinData(history, maxPoints, timeWindow = null) {
if (history.length <= maxPoints) {
return history;
}
const now = Date.now();
let filtered = history;
// Filter by time window if specified
if (timeWindow) {
filtered = history.filter(p => (now - p.timestamp) <= timeWindow);
}
if (filtered.length <= maxPoints) {
return filtered;
}
// Thin data by taking every Nth point, but always keep first and last
const step = Math.ceil(filtered.length / maxPoints);
const thinned = [];
for (let i = 0; i < filtered.length; i += step) {
thinned.push(filtered[i]);
}
// Ensure we include the last point
if (thinned[thinned.length - 1] !== filtered[filtered.length - 1]) {
thinned.push(filtered[filtered.length - 1]);
}
return thinned;
}
// Draw a progress graph on a specific canvas
function drawGraph(canvasId, dataPoints, title = '') {
const canvas = document.getElementById(canvasId);
if (!canvas) {
console.log('[AWTYT] Canvas not found:', canvasId);
return;
}
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
const padding = { top: 15, right: 10, bottom: 25, left: 45 };
const graphWidth = width - padding.left - padding.right;
const graphHeight = height - padding.top - padding.bottom;
// Determine if this is the page graph or popup graph
const isPageGraph = canvasId === 'awtyt-graph-full';
const bgColor = isPageGraph ? '#1a1a1a' : '#000';
const lineColor = isPageGraph ? '#60a5fa' : '#00ff00';
const gridColor = isPageGraph ? '#2a2a2a' : '#003300';
const textColor = isPageGraph ? '#e0e0e0' : '#00ff00';
const trendColor = isPageGraph ? '#60a5fa66' : '#00ff0066';
// Clear canvas
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
if (dataPoints.length < 2) {
// Not enough data to draw
ctx.fillStyle = textColor;
ctx.font = '12px "Courier New", monospace';
ctx.textAlign = 'center';
ctx.fillText('Collecting data...', width / 2, height / 2);
console.log('[AWTYT] Not enough data points:', dataPoints.length);
return;
}
// Find min/max values for scaling
const minTime = dataPoints[0].timestamp;
const maxTime = dataPoints[dataPoints.length - 1].timestamp;
const timeRange = maxTime - minTime || 1;
const progressValues = dataPoints.map(h => h.progress);
const minProgress = Math.max(0, Math.min(...progressValues) - 0.5);
const maxProgress = Math.min(100, Math.max(...progressValues) + 0.5);
const progressRange = maxProgress - minProgress || 1;
// Draw grid lines
ctx.strokeStyle = gridColor;
ctx.lineWidth = 1;
// Horizontal grid lines (progress)
for (let i = 0; i <= 5; i++) {
const y = padding.top + (graphHeight * i / 5);
ctx.beginPath();
ctx.moveTo(padding.left, y);
ctx.lineTo(padding.left + graphWidth, y);
ctx.stroke();
}
// Vertical grid lines (time)
for (let i = 0; i <= 5; i++) {
const x = padding.left + (graphWidth * i / 5);
ctx.beginPath();
ctx.moveTo(x, padding.top);
ctx.lineTo(x, padding.top + graphHeight);
ctx.stroke();
}
// Draw axes
ctx.strokeStyle = lineColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding.left, padding.top);
ctx.lineTo(padding.left, padding.top + graphHeight);
ctx.lineTo(padding.left + graphWidth, padding.top + graphHeight);
ctx.stroke();
// Draw Y-axis labels (progress %)
ctx.fillStyle = textColor;
ctx.font = '10px "Courier New", monospace';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let i = 0; i <= 5; i++) {
const progress = maxProgress - (progressRange * i / 5);
const y = padding.top + (graphHeight * i / 5);
ctx.fillText(progress.toFixed(1) + '%', padding.left - 5, y);
}
// Draw X-axis labels (time)
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
const now = Date.now();
const displayPoints = [0, 0.5, 1];
displayPoints.forEach(fraction => {
const time = minTime + (timeRange * fraction);
const x = padding.left + (graphWidth * fraction);
const secondsAgo = Math.round((now - time) / 1000);
let label;
if (secondsAgo < 60) {
label = secondsAgo === 0 ? 'now' : `-${secondsAgo}s`;
} else if (secondsAgo < 3600) {
const minutesAgo = Math.round(secondsAgo / 60);
label = `-${minutesAgo}m`;
} else if (secondsAgo < 86400) {
const hoursAgo = Math.round(secondsAgo / 3600);
label = `-${hoursAgo}h`;
} else {
const daysAgo = Math.round(secondsAgo / 86400);
label = `-${daysAgo}d`;
}
ctx.fillText(label, x, padding.top + graphHeight + 5);
});
// Draw the data line
ctx.strokeStyle = lineColor;
ctx.lineWidth = 2;
ctx.beginPath();
dataPoints.forEach((point, index) => {
const x = padding.left + ((point.timestamp - minTime) / timeRange) * graphWidth;
const y = padding.top + graphHeight - ((point.progress - minProgress) / progressRange) * graphHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// Draw data points
ctx.fillStyle = lineColor;
dataPoints.forEach((point) => {
const x = padding.left + ((point.timestamp - minTime) / timeRange) * graphWidth;
const y = padding.top + graphHeight - ((point.progress - minProgress) / progressRange) * graphHeight;
ctx.beginPath();
ctx.arc(x, y, 2, 0, 2 * Math.PI);
ctx.fill();
});
// Draw trend line if we have estimation
const history = loadHistory();
if (history.length >= MIN_DATA_POINTS) {
const estimation = calculateEstimation(history);
if (estimation && estimation.slope > 0) {
ctx.strokeStyle = trendColor;
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.beginPath();
// Draw from first point to projected end
const firstPoint = dataPoints[0];
const lastPoint = dataPoints[dataPoints.length - 1];
const x1 = padding.left;
const y1 = padding.top + graphHeight - ((firstPoint.progress - minProgress) / progressRange) * graphHeight;
// Project to 100% or edge of graph, whichever comes first
const timeToMax = (maxProgress - lastPoint.progress) / estimation.slope;
const projectedTime = Math.min(lastPoint.timestamp + timeToMax * 1000, maxTime + timeRange * 0.2);
const projectedProgress = Math.min(lastPoint.progress + estimation.slope * (projectedTime - lastPoint.timestamp) / 1000, maxProgress);
const x2 = padding.left + ((projectedTime - minTime) / timeRange) * graphWidth;
const y2 = padding.top + graphHeight - ((projectedProgress - minProgress) / progressRange) * graphHeight;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.setLineDash([]);
}
}
}
// Update the UI with current estimation
function updateUI() {
const history = loadHistory();
const statusEl = document.getElementById('awtyt-status');
const progressEl = document.getElementById('awtyt-progress');
const rateEl = document.getElementById('awtyt-rate');
const etaEl = document.getElementById('awtyt-eta');
const remainingEl = document.getElementById('awtyt-remaining');
const datapointsEl = document.getElementById('awtyt-datapoints');
datapointsEl.textContent = history.length;
// Create page graph if it doesn't exist
createPageGraph();
// Draw both graphs
const fourHours = 4 * 60 * 60 * 1000;
const recentData = thinData(history, 100, fourHours);
const fullData = thinData(history, 150);
drawGraph('awtyt-graph-recent', recentData);
drawGraph('awtyt-graph-full', fullData);
// Update page graph info
const pageInfoEl = document.getElementById('awtyt-page-graph-info');
if (pageInfoEl) {
if (history.length < 2) {
pageInfoEl.textContent = 'Collecting data...';
} else {
const timeSpan = history[history.length - 1].timestamp - history[0].timestamp;
const hours = Math.floor(timeSpan / 3600000);
const minutes = Math.floor((timeSpan % 3600000) / 60000);
let timeText = '';
if (hours > 0) timeText += `${hours}h `;
if (minutes > 0 || hours === 0) timeText += `${minutes}m`;
pageInfoEl.textContent = `${history.length} data points over ${timeText.trim()} • Showing ${fullData.length} points`;
}
}
if (history.length < MIN_DATA_POINTS) {
statusEl.textContent = `Need ${MIN_DATA_POINTS - history.length} more data points...`;
progressEl.textContent = history.length > 0 ? `Progress: ${history[history.length - 1].progress.toFixed(2)}%` : '';
rateEl.textContent = '';
etaEl.textContent = '';
remainingEl.textContent = '';
return;
}
const estimation = calculateEstimation(history);
if (!estimation) {
statusEl.textContent = 'Unable to estimate (no progress detected)';
return;
}
statusEl.textContent = '📊 Estimation Active';
progressEl.textContent = `Progress: ${estimation.currentProgress.toFixed(2)}%`;
rateEl.textContent = `Rate: ${(estimation.slope * 60).toFixed(4)}%/min`;
etaEl.textContent = `ETA: ${estimation.estimatedCompletionTime.toLocaleString()}`;
remainingEl.textContent = `Time Left: ${formatTimeRemaining(estimation.timeRemaining)}`;
}
let reloadTimer = null;
let reloadCountdown = null;
// Setup auto-reload
function setupAutoReload() {
clearAutoReload();
const interval = parseInt(document.getElementById('awtyt-reload-interval').value) * 1000;
const reloadTime = Date.now() + interval;
GM_setValue(RELOAD_KEY + '_time', reloadTime);
reloadTimer = setTimeout(() => {
location.reload();
}, interval);
// Update countdown every second
reloadCountdown = setInterval(() => {
updateReloadCountdown();
}, 1000);
updateReloadCountdown();
}
// Clear auto-reload timers
function clearAutoReload() {
if (reloadTimer) {
clearTimeout(reloadTimer);
reloadTimer = null;
}
if (reloadCountdown) {
clearInterval(reloadCountdown);
reloadCountdown = null;
}
document.getElementById('awtyt-next-reload').textContent = '';
}
// Update reload countdown display
function updateReloadCountdown() {
const reloadTime = GM_getValue(RELOAD_KEY + '_time', 0);
const remaining = Math.max(0, Math.floor((reloadTime - Date.now()) / 1000));
document.getElementById('awtyt-next-reload').textContent =
remaining > 0 ? `Next reload in ${remaining}s` : 'Reloading...';
}
// Main tracking loop
function trackProgress() {
const progress = getCurrentProgress();
if (progress !== null) {
const history = loadHistory();
const now = Date.now();
// Only add if progress has changed or it's been more than 1 minute
const shouldAdd = history.length === 0 ||
history[history.length - 1].progress !== progress ||
(now - history[history.length - 1].timestamp) > 60000;
if (shouldAdd) {
history.push({
timestamp: now,
progress: progress
});
// Keep all historical data - no limit!
saveHistory(history);
}
updateUI();
}
}
// Initialize
createUI();
trackProgress();
setInterval(trackProgress, CHECK_INTERVAL);
// Start auto-reload if enabled
if (GM_getValue(RELOAD_KEY + '_enabled', false)) {
setupAutoReload();
}
})();