+106
-5
src/cache.ts
+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
+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
+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 };