a cache for slack profile pictures and emojis

feat: add better uptime tracking

dunkirk.sh 29b74ffb 9775926e

verified
Changed files
+126 -5
src
+106 -5
src/cache.ts
··· 346 346 private userUpdateQueue: Set<string> = new Set(); 347 347 private isProcessingQueue = false; 348 348 private slackWrapper?: SlackUserProvider; // Will be injected after construction 349 + private currentSessionId?: number; 349 350 350 351 /** 351 352 * Creates a new Cache instance ··· 453 454 ) WITHOUT ROWID 454 455 `); 455 456 457 + // Create uptime tracking table 458 + this.db.run(` 459 + CREATE TABLE IF NOT EXISTS uptime_sessions ( 460 + id INTEGER PRIMARY KEY AUTOINCREMENT, 461 + start_time INTEGER NOT NULL, 462 + end_time INTEGER, 463 + duration INTEGER 464 + ) 465 + `); 466 + 456 467 // Create indexes for time-range queries 457 468 this.db.run( 458 469 "CREATE INDEX IF NOT EXISTS idx_traffic_10min_bucket ON traffic_10min(bucket)", ··· 487 498 this.onEmojiExpired(); 488 499 } 489 500 } 501 + 502 + // Start uptime session tracking 503 + this.startUptimeSession(); 504 + } 505 + 506 + /** 507 + * Starts a new uptime session and closes any orphaned sessions from crashes 508 + * @private 509 + */ 510 + private startUptimeSession() { 511 + const now = Date.now(); 512 + 513 + // Find and close any orphaned sessions (from crashes) 514 + const orphanedSessions = this.db 515 + .query("SELECT id, start_time FROM uptime_sessions WHERE end_time IS NULL") 516 + .all() as Array<{ id: number; start_time: number }>; 517 + 518 + for (const session of orphanedSessions) { 519 + // Estimate end time from last traffic activity or use start time + 1 minute as fallback 520 + const lastActivity = this.db 521 + .query("SELECT MAX(bucket) * 1000 as last_bucket FROM traffic_10min") 522 + .get() as { last_bucket: number | null }; 523 + 524 + const estimatedEnd = lastActivity?.last_bucket && lastActivity.last_bucket > session.start_time 525 + ? lastActivity.last_bucket 526 + : session.start_time + 60000; // Assume at least 1 minute if no activity 527 + 528 + const duration = estimatedEnd - session.start_time; 529 + this.db.run( 530 + "UPDATE uptime_sessions SET end_time = ?, duration = ? WHERE id = ?", 531 + [estimatedEnd, duration, session.id], 532 + ); 533 + console.log(`Closed orphaned session ${session.id} (likely crash), estimated duration: ${Math.round(duration / 1000)}s`); 534 + } 535 + 536 + // Start new session 537 + const result = this.db 538 + .query("INSERT INTO uptime_sessions (start_time) VALUES (?) RETURNING id") 539 + .get(now) as { id: number }; 540 + this.currentSessionId = result.id; 541 + } 542 + 543 + /** 544 + * Ends the current uptime session (call on graceful shutdown) 545 + */ 546 + endUptimeSession() { 547 + if (!this.currentSessionId) return; 548 + const now = Date.now(); 549 + const session = this.db 550 + .query("SELECT start_time FROM uptime_sessions WHERE id = ?") 551 + .get(this.currentSessionId) as { start_time: number } | null; 552 + if (session) { 553 + const duration = now - session.start_time; 554 + this.db.run( 555 + "UPDATE uptime_sessions SET end_time = ?, duration = ? WHERE id = ?", 556 + [now, duration, this.currentSessionId], 557 + ); 558 + } 559 + } 560 + 561 + /** 562 + * Gets lifetime uptime percentage 563 + * @returns Uptime percentage (0-100) 564 + */ 565 + getLifetimeUptime(): number { 566 + const now = Date.now(); 567 + 568 + // Get first session start time 569 + const firstSession = this.db 570 + .query("SELECT MIN(start_time) as first_start FROM uptime_sessions") 571 + .get() as { first_start: number | null }; 572 + 573 + if (!firstSession?.first_start) { 574 + return 100; // No sessions yet, assume 100% 575 + } 576 + 577 + const totalLifetime = now - firstSession.first_start; 578 + if (totalLifetime <= 0) return 100; 579 + 580 + // Sum all completed session durations 581 + const completedResult = this.db 582 + .query("SELECT COALESCE(SUM(duration), 0) as total FROM uptime_sessions WHERE duration IS NOT NULL") 583 + .get() as { total: number }; 584 + 585 + // Add current session duration (still running) 586 + const currentSession = this.db 587 + .query("SELECT start_time FROM uptime_sessions WHERE id = ?") 588 + .get(this.currentSessionId) as { start_time: number } | null; 589 + 590 + const currentDuration = currentSession ? now - currentSession.start_time : 0; 591 + const totalUptime = completedResult.total + currentDuration; 592 + 593 + return Math.min(100, (totalUptime / totalLifetime) * 100); 490 594 } 491 595 492 596 /** ··· 1506 1610 ? (redirectRequests / (redirectRequests + dataRequests)) * 100 1507 1611 : 0; 1508 1612 1509 - const uptime = Math.max(0, 100 - errorRate * 2); 1613 + const uptime = this.getLifetimeUptime(); 1510 1614 1511 1615 // Peak traffic analysis from bucket table 1512 1616 const peakHourData = this.db ··· 1664 1768 .get(alignedCutoff) as { count: number | null }; 1665 1769 1666 1770 const totalCount = totalResult.count ?? 0; 1667 - const errorRate = totalCount > 0 ? ((errorResult.count ?? 0) / totalCount) * 100 : 0; 1668 - const uptime = Math.max(0, 100 - errorRate * 2); 1669 - 1670 1771 const result = { 1671 1772 totalRequests: totalCount, 1672 1773 averageResponseTime: 1673 1774 avgResponseResult.totalHits && avgResponseResult.totalHits > 0 1674 1775 ? (avgResponseResult.totalTime ?? 0) / avgResponseResult.totalHits 1675 1776 : null, 1676 - uptime: uptime, 1777 + uptime: this.getLifetimeUptime(), 1677 1778 }; 1678 1779 1679 1780 this.typedAnalyticsCache.setEssentialStatsData(cacheKey, result);
+10
src/dashboard.html
··· 466 466 return `${(ms / 1000).toFixed(2)}s`; 467 467 } 468 468 469 + function formatUptime(seconds) { 470 + if (seconds === null || seconds === undefined) return '-'; 471 + const days = Math.floor(seconds / 86400); 472 + const hours = Math.floor((seconds % 86400) / 3600); 473 + const minutes = Math.floor((seconds % 3600) / 60); 474 + if (days > 0) return `${days}d ${hours}h`; 475 + if (hours > 0) return `${hours}h ${minutes}m`; 476 + return `${minutes}m`; 477 + } 478 + 469 479 function classifyUserAgent(ua) { 470 480 if (!ua) return { browser: 'Unknown', os: 'Unknown', type: 'unknown' }; 471 481
+10
src/index.ts
··· 122 122 123 123 console.log(`🚀 Server running on http://localhost:${server.port}`); 124 124 125 + // Graceful shutdown handling 126 + const shutdown = () => { 127 + console.log("Shutting down gracefully..."); 128 + cache.endUptimeSession(); 129 + process.exit(0); 130 + }; 131 + 132 + process.on("SIGINT", shutdown); 133 + process.on("SIGTERM", shutdown); 134 + 125 135 export { cache, slackApp };