feat(stats): voice-time and message leaderboards per space
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s

Voice stays get their own table rather than joining the audit log: that table
records points in time, a call is an interval, and pairing join/leave point
events would leave every query guessing at joins whose leave never arrived.

Sessions are opened and closed inside joinRoom/leaveCurrentRoom rather than at
the seven call sites that reach them, so no path can be missed, and
destroyRoom closes them too — it bypasses leaveCurrentRoom and would otherwise
leak open rows.

A restart leaves sessions open with an unknowable end time. They are closed at
startedAt, discarding that time rather than inventing it: crediting the gap
would hand someone hours they never spent, and the numbers are the point.
Mirrors the existing users.status sweep on boot.

Only closed sessions count, so a figure does not move on every refresh. Bars
scale to the leader, not the total — with five people every share of a total
looks identical. Statistics are readable by any member, since they are the
group's own numbers; the audit log, which names who did what, stays on
MANAGE_SPACE.
This commit is contained in:
2026-08-31 13:29:59 -03:00
parent bbb190cbda
commit 1830051732
13 changed files with 4642 additions and 1 deletions
+7
View File
@@ -17,6 +17,8 @@ import { dmRoutes } from './routes/dm.js';
import { livekitRoutes } from './routes/livekit.js';
import { spotifyRoutes } from './routes/spotify.js';
import { auditRoutes } from './routes/audit.js';
import { statsRoutes } from './routes/stats.js';
import { closeOrphanedVoiceSessions } from './utils/voiceSessions.js';
import { socialRoutes } from './routes/social.js';
import { settingsRoutes } from './routes/settings.js';
import { utilRoutes } from './routes/utils.js';
@@ -111,6 +113,10 @@ async function main(): Promise<void> {
// Initialize database
getDb();
// A restart leaves voice sessions open with no way to know when they really
// ended. Sweep them before anything can read the statistics.
closeOrphanedVoiceSessions();
// Reset orphaned `users.status` rows for locally-homed users. The previous
// process's in-memory disconnect timers are gone, so any non-offline row
// is stale by construction. Replicated (federated) rows are skipped — their
@@ -130,6 +136,7 @@ async function main(): Promise<void> {
await app.register(livekitRoutes);
await app.register(spotifyRoutes);
await app.register(auditRoutes);
await app.register(statsRoutes);
await app.register(socialRoutes);
await app.register(settingsRoutes);
await app.register(utilRoutes);