are-we-there-yet-estimator.user.js
edited
1// ==UserScript==
2// @name Are We There Yet - Time Estimator
3// @namespace http://tampermonkey.net/
4// @version 2.3
5// @description Estimates completion time based on progress rate
6// @author You
7// @match https://are-we-there-yet.hackclub.com/*
8// @grant GM_setValue
9// @grant GM_getValue
10// @run-at document-end
11// ==/UserScript==
12
13(function() {
14 'use strict';
15
16 const STORAGE_KEY = 'awtyt_progress_history';
17 const CHECK_INTERVAL = 5000; // Check every 5 seconds
18 const MIN_DATA_POINTS = 3; // Minimum data points needed for estimation
19 const RELOAD_INTERVAL = 60000; // Reload page every 60 seconds (1 minute)
20 const RELOAD_KEY = 'awtyt_auto_reload';
21
22 // Get current progress percentage from the page
23 function getCurrentProgress() {
24 // Try the progress element first
25 const progressEl = document.getElementById('progress');
26 if (progressEl && progressEl.value) {
27 return parseFloat(progressEl.value);
28 }
29
30 // Fallback to parsing the label text
31 const labelEl = document.querySelector('label[for="progress"]');
32 if (labelEl) {
33 const match = labelEl.textContent.match(/(\d+\.?\d*)%/);
34 if (match) {
35 return parseFloat(match[1]);
36 }
37 }
38
39 return null;
40 }
41
42 // Load progress history from storage
43 function loadHistory() {
44 const stored = GM_getValue(STORAGE_KEY, '[]');
45 return JSON.parse(stored);
46 }
47
48 // Save progress history to storage
49 function saveHistory(history) {
50 GM_setValue(STORAGE_KEY, JSON.stringify(history));
51 }
52
53 // Calculate estimated completion time
54 function calculateEstimation(history) {
55 if (history.length < MIN_DATA_POINTS) {
56 return null;
57 }
58
59 // Use linear regression to estimate rate of change
60 const n = history.length;
61 let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
62
63 // Use only recent data points (last 20) for better accuracy
64 const recentHistory = history.slice(-20);
65 const baseTime = recentHistory[0].timestamp;
66
67 recentHistory.forEach((point) => {
68 const x = (point.timestamp - baseTime) / 1000; // seconds since first point
69 const y = point.progress;
70 sumX += x;
71 sumY += y;
72 sumXY += x * y;
73 sumX2 += x * x;
74 });
75
76 const slope = (recentHistory.length * sumXY - sumX * sumY) /
77 (recentHistory.length * sumX2 - sumX * sumX);
78 const intercept = (sumY - slope * sumX) / recentHistory.length;
79
80 // If slope is zero or negative, we can't estimate
81 if (slope <= 0) {
82 return null;
83 }
84
85 const currentProgress = recentHistory[recentHistory.length - 1].progress;
86 const currentTime = recentHistory[recentHistory.length - 1].timestamp;
87 const timeElapsed = (currentTime - baseTime) / 1000;
88
89 // Calculate when we'll reach 100%
90 const timeToCompletion = (100 - currentProgress) / slope;
91 const estimatedCompletionTime = new Date(currentTime + timeToCompletion * 1000);
92
93 return {
94 slope: slope,
95 currentProgress: currentProgress,
96 estimatedCompletionTime: estimatedCompletionTime,
97 timeRemaining: timeToCompletion
98 };
99 }
100
101 // Format time remaining
102 function formatTimeRemaining(seconds) {
103 if (seconds < 0) return "Completed!";
104
105 const days = Math.floor(seconds / 86400);
106 const hours = Math.floor((seconds % 86400) / 3600);
107 const minutes = Math.floor((seconds % 3600) / 60);
108 const secs = Math.floor(seconds % 60);
109
110 const parts = [];
111 if (days > 0) parts.push(`${days}d`);
112 if (hours > 0) parts.push(`${hours}h`);
113 if (minutes > 0) parts.push(`${minutes}m`);
114 if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
115
116 return parts.join(' ');
117 }
118
119 // Create and inject the full history graph into the page
120 function createPageGraph() {
121 // Check if already exists
122 if (document.getElementById('awtyt-page-graph-container')) {
123 return;
124 }
125
126 const progressBar = document.getElementById('progress');
127 if (!progressBar) {
128 return; // Progress bar not found yet
129 }
130
131 const container = document.createElement('div');
132 container.id = 'awtyt-page-graph-container';
133 container.style.cssText = `
134 margin: 3rem 0 2rem 0;
135 `;
136
137 container.innerHTML = `
138 <div style="
139 font-weight: 700;
140 font-size: 0.9em;
141 color: #e0e0e0;
142 letter-spacing: 0.05em;
143 margin-bottom: 1rem;
144 ">Progress History</div>
145 <div style="
146 background: #222;
147 border-left: 4px solid #60a5fa;
148 padding: 1.5em;
149 ">
150 <canvas id="awtyt-graph-full" width="750" height="300" style="
151 background: #1a1a1a;
152 border: 1px solid #333;
153 border-radius: 4px;
154 max-width: 100%;
155 display: block;
156 "></canvas>
157 <div style="
158 margin-top: 1em;
159 font-size: 0.8em;
160 color: #888;
161 letter-spacing: 0.05em;
162 " id="awtyt-page-graph-info">Collecting data...</div>
163 </div>
164 `;
165
166 // Insert after the progress bar
167 progressBar.parentNode.insertBefore(container, progressBar.nextSibling);
168 }
169
170 // Create and inject the UI
171 function createUI() {
172 const container = document.createElement('div');
173 container.id = 'awtyt-estimator';
174 container.style.cssText = `
175 position: fixed;
176 top: 10px;
177 right: 10px;
178 background: rgba(0, 0, 0, 0.9);
179 color: #00ff00;
180 padding: 15px;
181 border-radius: 8px;
182 font-family: 'Courier New', monospace;
183 font-size: 14px;
184 z-index: 10000;
185 min-width: 350px;
186 max-width: 90vw;
187 box-shadow: 0 0 10px rgba(0, 255, 0, 0.3);
188 `;
189
190 container.innerHTML = `
191 <div style="margin-bottom: 10px; font-weight: bold; border-bottom: 1px solid #00ff00; padding-bottom: 5px;">
192 ⏱️ Progress Tracker
193 </div>
194 <div id="awtyt-status">Collecting data...</div>
195 <div id="awtyt-progress" style="margin-top: 5px;"></div>
196 <div id="awtyt-rate" style="margin-top: 5px;"></div>
197 <div id="awtyt-eta" style="margin-top: 5px;"></div>
198 <div id="awtyt-remaining" style="margin-top: 5px; font-weight: bold;"></div>
199 <div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #00ff00;">
200 <div style="font-size: 11px; margin-bottom: 5px; opacity: 0.8;">Recent (Last 4 Hours)</div>
201 <canvas id="awtyt-graph-recent" width="320" height="100" style="background: #000; border: 1px solid #00ff00; border-radius: 4px; max-width: 100%;"></canvas>
202 </div>
203 <div style="margin-top: 10px; font-size: 11px; opacity: 0.7; border-top: 1px solid #00ff00; padding-top: 5px;">
204 <span id="awtyt-datapoints">0</span> data points collected
205 <button id="awtyt-reset" style="margin-left: 10px; background: #00ff00; color: black; border: none; padding: 2px 8px; cursor: pointer; border-radius: 3px;">Reset</button>
206 </div>
207 <div style="margin-top: 8px; font-size: 11px;">
208 <label style="display: flex; align-items: center; cursor: pointer;">
209 <input type="checkbox" id="awtyt-autoreload" style="margin-right: 5px; cursor: pointer;">
210 Auto-reload every <input type="number" id="awtyt-reload-interval" min="30" max="600" value="60" style="width: 50px; margin: 0 3px; background: #001100; color: #00ff00; border: 1px solid #00ff00; padding: 2px; text-align: center;"> sec
211 </label>
212 <div id="awtyt-next-reload" style="margin-top: 3px; font-size: 10px; opacity: 0.6;"></div>
213 </div>
214 `;
215
216 document.body.appendChild(container);
217
218 // Add reset button handler
219 document.getElementById('awtyt-reset').addEventListener('click', () => {
220 saveHistory([]);
221 updateUI();
222 });
223
224 // Load auto-reload preference
225 const autoReloadEnabled = GM_getValue(RELOAD_KEY + '_enabled', false);
226 const reloadInterval = GM_getValue(RELOAD_KEY + '_interval', 60);
227 document.getElementById('awtyt-autoreload').checked = autoReloadEnabled;
228 document.getElementById('awtyt-reload-interval').value = reloadInterval;
229
230 // Auto-reload checkbox handler
231 document.getElementById('awtyt-autoreload').addEventListener('change', (e) => {
232 GM_setValue(RELOAD_KEY + '_enabled', e.target.checked);
233 if (e.target.checked) {
234 setupAutoReload();
235 } else {
236 clearAutoReload();
237 }
238 });
239
240 // Reload interval input handler
241 document.getElementById('awtyt-reload-interval').addEventListener('change', (e) => {
242 const interval = parseInt(e.target.value);
243 if (interval >= 30 && interval <= 600) {
244 GM_setValue(RELOAD_KEY + '_interval', interval);
245 if (document.getElementById('awtyt-autoreload').checked) {
246 clearAutoReload();
247 setupAutoReload();
248 }
249 }
250 });
251 }
252
253 // Thin out data points for display based on time window
254 function thinData(history, maxPoints, timeWindow = null) {
255 if (history.length <= maxPoints) {
256 return history;
257 }
258
259 const now = Date.now();
260 let filtered = history;
261
262 // Filter by time window if specified
263 if (timeWindow) {
264 filtered = history.filter(p => (now - p.timestamp) <= timeWindow);
265 }
266
267 if (filtered.length <= maxPoints) {
268 return filtered;
269 }
270
271 // Thin data by taking every Nth point, but always keep first and last
272 const step = Math.ceil(filtered.length / maxPoints);
273 const thinned = [];
274
275 for (let i = 0; i < filtered.length; i += step) {
276 thinned.push(filtered[i]);
277 }
278
279 // Ensure we include the last point
280 if (thinned[thinned.length - 1] !== filtered[filtered.length - 1]) {
281 thinned.push(filtered[filtered.length - 1]);
282 }
283
284 return thinned;
285 }
286
287 // Draw a progress graph on a specific canvas
288 function drawGraph(canvasId, dataPoints, title = '') {
289 const canvas = document.getElementById(canvasId);
290 if (!canvas) {
291 console.log('[AWTYT] Canvas not found:', canvasId);
292 return;
293 }
294
295 const ctx = canvas.getContext('2d');
296 const width = canvas.width;
297 const height = canvas.height;
298
299 const padding = { top: 15, right: 10, bottom: 25, left: 45 };
300 const graphWidth = width - padding.left - padding.right;
301 const graphHeight = height - padding.top - padding.bottom;
302
303 // Determine if this is the page graph or popup graph
304 const isPageGraph = canvasId === 'awtyt-graph-full';
305 const bgColor = isPageGraph ? '#1a1a1a' : '#000';
306 const lineColor = isPageGraph ? '#60a5fa' : '#00ff00';
307 const gridColor = isPageGraph ? '#2a2a2a' : '#003300';
308 const textColor = isPageGraph ? '#e0e0e0' : '#00ff00';
309 const trendColor = isPageGraph ? '#60a5fa66' : '#00ff0066';
310
311 // Clear canvas
312 ctx.fillStyle = bgColor;
313 ctx.fillRect(0, 0, width, height);
314
315 if (dataPoints.length < 2) {
316 // Not enough data to draw
317 ctx.fillStyle = textColor;
318 ctx.font = '12px "Courier New", monospace';
319 ctx.textAlign = 'center';
320 ctx.fillText('Collecting data...', width / 2, height / 2);
321 console.log('[AWTYT] Not enough data points:', dataPoints.length);
322 return;
323 }
324
325 // Find min/max values for scaling
326 const minTime = dataPoints[0].timestamp;
327 const maxTime = dataPoints[dataPoints.length - 1].timestamp;
328 const timeRange = maxTime - minTime || 1;
329
330 const progressValues = dataPoints.map(h => h.progress);
331 const minProgress = Math.max(0, Math.min(...progressValues) - 0.5);
332 const maxProgress = Math.min(100, Math.max(...progressValues) + 0.5);
333 const progressRange = maxProgress - minProgress || 1;
334
335 // Draw grid lines
336 ctx.strokeStyle = gridColor;
337 ctx.lineWidth = 1;
338
339 // Horizontal grid lines (progress)
340 for (let i = 0; i <= 5; i++) {
341 const y = padding.top + (graphHeight * i / 5);
342 ctx.beginPath();
343 ctx.moveTo(padding.left, y);
344 ctx.lineTo(padding.left + graphWidth, y);
345 ctx.stroke();
346 }
347
348 // Vertical grid lines (time)
349 for (let i = 0; i <= 5; i++) {
350 const x = padding.left + (graphWidth * i / 5);
351 ctx.beginPath();
352 ctx.moveTo(x, padding.top);
353 ctx.lineTo(x, padding.top + graphHeight);
354 ctx.stroke();
355 }
356
357 // Draw axes
358 ctx.strokeStyle = lineColor;
359 ctx.lineWidth = 2;
360 ctx.beginPath();
361 ctx.moveTo(padding.left, padding.top);
362 ctx.lineTo(padding.left, padding.top + graphHeight);
363 ctx.lineTo(padding.left + graphWidth, padding.top + graphHeight);
364 ctx.stroke();
365
366 // Draw Y-axis labels (progress %)
367 ctx.fillStyle = textColor;
368 ctx.font = '10px "Courier New", monospace';
369 ctx.textAlign = 'right';
370 ctx.textBaseline = 'middle';
371 for (let i = 0; i <= 5; i++) {
372 const progress = maxProgress - (progressRange * i / 5);
373 const y = padding.top + (graphHeight * i / 5);
374 ctx.fillText(progress.toFixed(1) + '%', padding.left - 5, y);
375 }
376
377 // Draw X-axis labels (time)
378 ctx.textAlign = 'center';
379 ctx.textBaseline = 'top';
380 const now = Date.now();
381 const displayPoints = [0, 0.5, 1];
382 displayPoints.forEach(fraction => {
383 const time = minTime + (timeRange * fraction);
384 const x = padding.left + (graphWidth * fraction);
385 const secondsAgo = Math.round((now - time) / 1000);
386
387 let label;
388 if (secondsAgo < 60) {
389 label = secondsAgo === 0 ? 'now' : `-${secondsAgo}s`;
390 } else if (secondsAgo < 3600) {
391 const minutesAgo = Math.round(secondsAgo / 60);
392 label = `-${minutesAgo}m`;
393 } else if (secondsAgo < 86400) {
394 const hoursAgo = Math.round(secondsAgo / 3600);
395 label = `-${hoursAgo}h`;
396 } else {
397 const daysAgo = Math.round(secondsAgo / 86400);
398 label = `-${daysAgo}d`;
399 }
400
401 ctx.fillText(label, x, padding.top + graphHeight + 5);
402 });
403
404 // Draw the data line
405 ctx.strokeStyle = lineColor;
406 ctx.lineWidth = 2;
407 ctx.beginPath();
408
409 dataPoints.forEach((point, index) => {
410 const x = padding.left + ((point.timestamp - minTime) / timeRange) * graphWidth;
411 const y = padding.top + graphHeight - ((point.progress - minProgress) / progressRange) * graphHeight;
412
413 if (index === 0) {
414 ctx.moveTo(x, y);
415 } else {
416 ctx.lineTo(x, y);
417 }
418 });
419
420 ctx.stroke();
421
422 // Draw data points
423 ctx.fillStyle = lineColor;
424 dataPoints.forEach((point) => {
425 const x = padding.left + ((point.timestamp - minTime) / timeRange) * graphWidth;
426 const y = padding.top + graphHeight - ((point.progress - minProgress) / progressRange) * graphHeight;
427
428 ctx.beginPath();
429 ctx.arc(x, y, 2, 0, 2 * Math.PI);
430 ctx.fill();
431 });
432
433 // Draw trend line if we have estimation
434 const history = loadHistory();
435 if (history.length >= MIN_DATA_POINTS) {
436 const estimation = calculateEstimation(history);
437 if (estimation && estimation.slope > 0) {
438 ctx.strokeStyle = trendColor;
439 ctx.lineWidth = 1;
440 ctx.setLineDash([5, 5]);
441 ctx.beginPath();
442
443 // Draw from first point to projected end
444 const firstPoint = dataPoints[0];
445 const lastPoint = dataPoints[dataPoints.length - 1];
446
447 const x1 = padding.left;
448 const y1 = padding.top + graphHeight - ((firstPoint.progress - minProgress) / progressRange) * graphHeight;
449
450 // Project to 100% or edge of graph, whichever comes first
451 const timeToMax = (maxProgress - lastPoint.progress) / estimation.slope;
452 const projectedTime = Math.min(lastPoint.timestamp + timeToMax * 1000, maxTime + timeRange * 0.2);
453 const projectedProgress = Math.min(lastPoint.progress + estimation.slope * (projectedTime - lastPoint.timestamp) / 1000, maxProgress);
454
455 const x2 = padding.left + ((projectedTime - minTime) / timeRange) * graphWidth;
456 const y2 = padding.top + graphHeight - ((projectedProgress - minProgress) / progressRange) * graphHeight;
457
458 ctx.moveTo(x1, y1);
459 ctx.lineTo(x2, y2);
460 ctx.stroke();
461 ctx.setLineDash([]);
462 }
463 }
464 }
465
466 // Update the UI with current estimation
467 function updateUI() {
468 const history = loadHistory();
469 const statusEl = document.getElementById('awtyt-status');
470 const progressEl = document.getElementById('awtyt-progress');
471 const rateEl = document.getElementById('awtyt-rate');
472 const etaEl = document.getElementById('awtyt-eta');
473 const remainingEl = document.getElementById('awtyt-remaining');
474 const datapointsEl = document.getElementById('awtyt-datapoints');
475
476 datapointsEl.textContent = history.length;
477
478 // Create page graph if it doesn't exist
479 createPageGraph();
480
481 // Draw both graphs
482 const fourHours = 4 * 60 * 60 * 1000;
483 const recentData = thinData(history, 100, fourHours);
484 const fullData = thinData(history, 150);
485
486 drawGraph('awtyt-graph-recent', recentData);
487 drawGraph('awtyt-graph-full', fullData);
488
489 // Update page graph info
490 const pageInfoEl = document.getElementById('awtyt-page-graph-info');
491 if (pageInfoEl) {
492 if (history.length < 2) {
493 pageInfoEl.textContent = 'Collecting data...';
494 } else {
495 const timeSpan = history[history.length - 1].timestamp - history[0].timestamp;
496 const hours = Math.floor(timeSpan / 3600000);
497 const minutes = Math.floor((timeSpan % 3600000) / 60000);
498 let timeText = '';
499 if (hours > 0) timeText += `${hours}h `;
500 if (minutes > 0 || hours === 0) timeText += `${minutes}m`;
501 pageInfoEl.textContent = `${history.length} data points over ${timeText.trim()} • Showing ${fullData.length} points`;
502 }
503 }
504
505 if (history.length < MIN_DATA_POINTS) {
506 statusEl.textContent = `Need ${MIN_DATA_POINTS - history.length} more data points...`;
507 progressEl.textContent = history.length > 0 ? `Progress: ${history[history.length - 1].progress.toFixed(2)}%` : '';
508 rateEl.textContent = '';
509 etaEl.textContent = '';
510 remainingEl.textContent = '';
511 return;
512 }
513
514 const estimation = calculateEstimation(history);
515 if (!estimation) {
516 statusEl.textContent = 'Unable to estimate (no progress detected)';
517 return;
518 }
519
520 statusEl.textContent = '📊 Estimation Active';
521 progressEl.textContent = `Progress: ${estimation.currentProgress.toFixed(2)}%`;
522 rateEl.textContent = `Rate: ${(estimation.slope * 60).toFixed(4)}%/min`;
523 etaEl.textContent = `ETA: ${estimation.estimatedCompletionTime.toLocaleString()}`;
524 remainingEl.textContent = `Time Left: ${formatTimeRemaining(estimation.timeRemaining)}`;
525 }
526
527 let reloadTimer = null;
528 let reloadCountdown = null;
529
530 // Setup auto-reload
531 function setupAutoReload() {
532 clearAutoReload();
533 const interval = parseInt(document.getElementById('awtyt-reload-interval').value) * 1000;
534 const reloadTime = Date.now() + interval;
535 GM_setValue(RELOAD_KEY + '_time', reloadTime);
536
537 reloadTimer = setTimeout(() => {
538 location.reload();
539 }, interval);
540
541 // Update countdown every second
542 reloadCountdown = setInterval(() => {
543 updateReloadCountdown();
544 }, 1000);
545
546 updateReloadCountdown();
547 }
548
549 // Clear auto-reload timers
550 function clearAutoReload() {
551 if (reloadTimer) {
552 clearTimeout(reloadTimer);
553 reloadTimer = null;
554 }
555 if (reloadCountdown) {
556 clearInterval(reloadCountdown);
557 reloadCountdown = null;
558 }
559 document.getElementById('awtyt-next-reload').textContent = '';
560 }
561
562 // Update reload countdown display
563 function updateReloadCountdown() {
564 const reloadTime = GM_getValue(RELOAD_KEY + '_time', 0);
565 const remaining = Math.max(0, Math.floor((reloadTime - Date.now()) / 1000));
566 document.getElementById('awtyt-next-reload').textContent =
567 remaining > 0 ? `Next reload in ${remaining}s` : 'Reloading...';
568 }
569
570 // Main tracking loop
571 function trackProgress() {
572 const progress = getCurrentProgress();
573 if (progress !== null) {
574 const history = loadHistory();
575 const now = Date.now();
576
577 // Only add if progress has changed or it's been more than 1 minute
578 const shouldAdd = history.length === 0 ||
579 history[history.length - 1].progress !== progress ||
580 (now - history[history.length - 1].timestamp) > 60000;
581
582 if (shouldAdd) {
583 history.push({
584 timestamp: now,
585 progress: progress
586 });
587
588 // Keep all historical data - no limit!
589 saveHistory(history);
590 }
591
592 updateUI();
593 }
594 }
595
596 // Initialize
597 createUI();
598 trackProgress();
599 setInterval(trackProgress, CHECK_INTERVAL);
600
601 // Start auto-reload if enabled
602 if (GM_getValue(RELOAD_KEY + '_enabled', false)) {
603 setupAutoReload();
604 }
605
606})();