this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add live counters

+171 -9
+171 -9
public/app.js
··· 8 8 const noGraph = document.getElementById("no-graph"); 9 9 const rankChart = document.getElementById("rank-chart"); 10 10 const verifiedOnlyToggle = document.getElementById("verified-only-toggle"); 11 - 11 + 12 12 // Auto-refresh timer 13 13 let autoRefreshTimer = null; 14 14 const AUTO_REFRESH_INTERVAL = 2 * 60 * 1000; // 2 minutes in milliseconds 15 + 16 + // Live counter timer 17 + const LIVE_COUNTER_INTERVAL = 1000; // Update every second 18 + let liveCounterTimer = null; 15 19 16 20 // Track verified user stats 17 21 let verifiedUserStats = { ··· 33 37 // Initialize stats 34 38 updatePerformanceMetrics([]); 35 39 36 - // For calculating durations 37 - const now = Date.now(); 40 + // For calculating durations (updated live) 41 + let now = Date.now(); 38 42 39 43 // Utility function to format time duration 40 44 function formatDuration(ms) { ··· 99 103 100 104 // Fetch stories data 101 105 function fetchStories() { 106 + // Update last refresh time 107 + window.lastRefreshTime = Date.now(); 108 + 102 109 storyList.innerHTML = '<div class="loading">Loading stories...</div>'; 110 + 111 + // Ensure live counters are running 112 + if (!liveCounterTimer) { 113 + startLiveCounters(); 114 + } 103 115 104 116 // Fetch total stories count first 105 117 fetch(`/api/stats/total-stories?_=${Date.now()}`) ··· 178 190 topRankRecord = bestRank; 179 191 } 180 192 193 + // Store stories globally for duration updates 194 + window.displayedStories = stories; 195 + now = Date.now(); // Update current time for accurate initial durations 196 + 181 197 let html = ""; 182 198 for (let i = 0; i < stories.length; i++) { 183 199 const story = stories[i]; ··· 216 232 217 233 // Add verified badge if story is from a monitored user 218 234 html += ` 219 - <div class="story-item${isCurrentTop ? " top-story" : ""}${isCurrentRankOne ? " top-ranked" : ""}${isBestRankOne && !isCurrentRankOne ? " previously-top-ranked" : ""}" data-id="${story.id}" data-url="${story.url}"> 235 + <div class="story-item${isCurrentTop ? " top-story" : ""}${isCurrentRankOne ? " top-ranked" : ""}${isBestRankOne && !isCurrentRankOne ? " previously-top-ranked" : ""}" data-id="${story.id}" data-url="${story.url}" data-timestamp="${timestampMs}"> 220 236 <h3>${story.title}</h3> 221 237 ${rankDisplay} 222 238 <div class="story-meta"> ··· 227 243 </div> 228 244 <div class="story-meta"> 229 245 <span>Detected: ${date}</span> 230 - <span class="duration ${durationClass}" title="Time since first detection">${durationEmoji} ${durationText}</span> 246 + <span class="duration ${durationClass}" title="Time since first detection" data-timestamp="${timestampMs}" data-story-id="${story.id}">${durationEmoji} ${durationText}</span> 231 247 <span><a href="${story.url}" target="_blank" class="external-link">View Story ↗</a></span> 232 248 </div> 233 249 </div> ··· 240 256 const storyItems = document.querySelectorAll(".story-item"); 241 257 242 258 for (const item of storyItems) { 259 + // Ensure timestamps are available for live updates 260 + if (!item.hasAttribute("data-timestamp")) { 261 + const storyId = item.getAttribute("data-id"); 262 + const story = stories.find((s) => s.id.toString() === storyId); 263 + if (story?.timestamp) { 264 + const timestamp = new Date(story.timestamp).getTime(); 265 + item.setAttribute("data-timestamp", timestamp); 266 + } 267 + } 268 + 243 269 item.addEventListener("click", (e) => { 244 270 // Prevent triggering when clicking links 245 271 if ( ··· 402 428 return; 403 429 } 404 430 431 + // Update current time for accurate calculations 432 + now = Date.now(); 433 + 405 434 // Get total stories from the API 406 435 fetch(`/api/stats/total-stories?_=${Date.now()}`) 407 436 .then((response) => response.json()) ··· 468 497 applyFiltersAndUpdateUI(); 469 498 }); 470 499 500 + // Apply updateLiveCounters() periodically for time-based UI elements 501 + document.addEventListener("visibilitychange", () => { 502 + if (!document.hidden) { 503 + console.log("Tab is visible again, updating time elements"); 504 + // Update the current time immediately 505 + now = Date.now(); 506 + // Force update of all time-based elements 507 + updateLiveCounters(); 508 + // Restart live counters if they're not running 509 + if (!liveCounterTimer) { 510 + startLiveCounters(); 511 + } 512 + // Refresh data if it's been more than 30 seconds since last refresh 513 + const lastRefreshTime = window.lastRefreshTime || 0; 514 + if (now - lastRefreshTime > 30000) { 515 + console.log("Data may be stale, triggering refresh"); 516 + fetchStories(); 517 + } 518 + } else { 519 + // Tab is hidden, pause live counters to save resources 520 + if (liveCounterTimer) { 521 + clearInterval(liveCounterTimer); 522 + liveCounterTimer = null; 523 + console.log("Live counters paused while tab is inactive"); 524 + } 525 + } 526 + }); 527 + 471 528 // Add CSS for duration indicators 472 529 const style = document.createElement("style"); 473 530 style.textContent = ` ··· 517 574 if (autoRefreshTimer) { 518 575 clearTimeout(autoRefreshTimer); 519 576 } 520 - 577 + 521 578 // Set new timer 522 579 autoRefreshTimer = setTimeout(() => { 523 580 console.log("Auto-refreshing data..."); ··· 526 583 startAutoRefreshTimer(); 527 584 }, AUTO_REFRESH_INTERVAL); 528 585 } 529 - 586 + 530 587 // Reset the auto-refresh timer 531 588 function resetAutoRefreshTimer() { 532 589 if (autoRefreshTimer) { 533 590 clearTimeout(autoRefreshTimer); 534 591 } 535 592 startAutoRefreshTimer(); 593 + 594 + // Also reset the live counter to ensure synchronized updates 595 + if (liveCounterTimer) { 596 + clearInterval(liveCounterTimer); 597 + } 598 + startLiveCounters(); 536 599 } 537 - 600 + 601 + // Live counter function to update time-based elements 602 + function updateLiveCounters() { 603 + // Update current time 604 + now = Date.now(); 605 + 606 + // Update story durations if we have stories displayed 607 + if (window.displayedStories && window.displayedStories.length > 0) { 608 + // Update all duration spans 609 + // Get all story items with timestamps 610 + const storyItems = document.querySelectorAll( 611 + ".story-item[data-timestamp]", 612 + ); 613 + for (const item of storyItems) { 614 + const timestamp = Number.parseInt( 615 + item.getAttribute("data-timestamp"), 616 + 10, 617 + ); 618 + if (Number.isNaN(timestamp)) continue; 619 + 620 + const durationMs = now - timestamp; 621 + const durationEl = item.querySelector(".duration"); 622 + 623 + if (durationEl) { 624 + // Get new duration values 625 + const durationText = formatDuration(durationMs); 626 + const durationClass = getDurationClass(durationMs); 627 + const durationEmoji = getDurationEmoji(durationMs); 628 + 629 + // Update the duration element 630 + durationEl.innerHTML = `${durationEmoji} ${durationText}`; 631 + 632 + // Update the class if needed 633 + const durationClasses = [ 634 + "duration-short", 635 + "duration-normal", 636 + "duration-medium", 637 + "duration-long", 638 + ]; 639 + for (const cls of durationClasses) { 640 + durationEl.classList.remove(cls); 641 + } 642 + durationEl.classList.add(durationClass); 643 + } 644 + } 645 + 646 + // Update average duration in performance metrics if we have the displayed stories 647 + if (document.getElementById("avg-frontpage-time")) { 648 + let totalMs = 0; 649 + let storiesWithDuration = 0; 650 + 651 + for (const story of window.displayedStories) { 652 + if (story.timestamp) { 653 + const enteredTimestamp = new Date(story.timestamp).getTime(); 654 + 655 + // If story is still on front page, calculate duration until now 656 + if (story.rank <= 30) { 657 + const durationMs = now - enteredTimestamp; 658 + totalMs += durationMs; 659 + storiesWithDuration++; 660 + } 661 + } 662 + } 663 + 664 + if (storiesWithDuration > 0) { 665 + const avgDurationMs = Math.round(totalMs / storiesWithDuration); 666 + avgFrontpageTimeEl.textContent = formatDuration(avgDurationMs); 667 + } 668 + } 669 + } 670 + } 671 + 672 + // Start live counter updates 673 + function startLiveCounters() { 674 + // Clear any existing timers 675 + if (liveCounterTimer) { 676 + clearInterval(liveCounterTimer); 677 + } 678 + 679 + // Update immediately first 680 + updateLiveCounters(); 681 + 682 + // Then set interval for regular updates 683 + liveCounterTimer = setInterval(() => { 684 + requestAnimationFrame(updateLiveCounters); // Use requestAnimationFrame for smoother updates 685 + }, LIVE_COUNTER_INTERVAL); 686 + 687 + console.log("Live counters started - durations will update every second"); 688 + } 689 + 690 + // Track last refresh time 691 + window.lastRefreshTime = Date.now(); 692 + 693 + // No need to replace the function 694 + 538 695 // Initial data fetch 539 696 fetchStories(); 540 - 697 + 541 698 // Set up auto-refresh 542 699 startAutoRefreshTimer(); 700 + 701 + // Start live counters 702 + startLiveCounters(); 703 + 704 + 543 705 544 706 // We'll initialize the chart on demand rather than empty 545 707 });