reaper-arcade-hub 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/app.js CHANGED
@@ -1,4 +1,6 @@
1
- // REAPER HUB - Core Engine (3 Roles: Member/Chat Mod/Owner, Key Limits, Cyber Font, Mod Tab)
1
+ // REAPER HUB - Core Engine (v2.4.0 Next-Gen UI & Instant Auto-Redirect)
2
+ window.REAPER_VERSION = "2.4.0";
3
+ window.REAPER_LATEST_URL = "https://unpkg.com/reaper-arcade-hub@2.4.0/main/index.html";
2
4
 
3
5
  class ReaperHubApp {
4
6
  constructor() {
@@ -28,6 +30,7 @@ class ReaperHubApp {
28
30
  this.lastActivityTime = Date.now();
29
31
  this.isPausedForInactivity = false;
30
32
  this.leaderboardData = [];
33
+ this.onlineUsers = [];
31
34
 
32
35
  // Turbo Anti-Lag Engine & Superpower State
33
36
  this.turboMode = localStorage.getItem('reaper_turbo_mode') !== 'false'; // Default TRUE for high FPS
@@ -218,6 +221,25 @@ class ReaperHubApp {
218
221
  const select = document.getElementById('admin-slowmode-interval');
219
222
  if (select) select.value = this.slowModeInterval.toString();
220
223
  });
224
+ window.reaperFirebase.purgeGhostUsers();
225
+ window.reaperFirebase.registerOnlinePresence({
226
+ username: this.currentUser.username,
227
+ rank: this.currentUser.rank,
228
+ version: window.REAPER_VERSION,
229
+ playtime: this.playtimeSeconds
230
+ });
231
+ window.reaperFirebase.listenOnlineUsers((users) => {
232
+ this.onlineUsers = users || [];
233
+ this.renderAdminUsersDirectory();
234
+ this.renderModUsersDirectory();
235
+ const onlineCountEl = document.getElementById('admin-stat-online');
236
+ if (onlineCountEl) onlineCountEl.textContent = this.onlineUsers.length;
237
+ });
238
+ window.reaperFirebase.listenLatestVersion((info) => {
239
+ if (info && info.version && info.version !== window.REAPER_VERSION && info.forceUpgrade !== false) {
240
+ this.handleOldVersionDetected(info);
241
+ }
242
+ });
221
243
  }
222
244
 
223
245
  // 9. Render UI, Games, Leaderboard, Turbo Engine
@@ -508,6 +530,21 @@ class ReaperHubApp {
508
530
  alert("[ACCESS DENIED]\nInvalid, exhausted, or expired Key/Token. Please obtain a valid key from the Owner.");
509
531
  }
510
532
 
533
+ async pasteKeyFromClipboard() {
534
+ const input = document.getElementById('gate-token-input');
535
+ if (!input) return;
536
+ try {
537
+ const text = await navigator.clipboard.readText();
538
+ if (text) {
539
+ input.value = text.trim();
540
+ this.submitGateToken();
541
+ }
542
+ } catch (e) {
543
+ input.focus();
544
+ this.showToast("Paste manually or press Enter.");
545
+ }
546
+ }
547
+
511
548
  // ==========================================
512
549
  // CUSTOM KEY CREATION WITH MAX USES & DELETION
513
550
  // ==========================================
@@ -795,40 +832,189 @@ class ReaperHubApp {
795
832
  const container = document.getElementById('admin-users-directory');
796
833
  if (!container) return;
797
834
 
798
- if (this.leaderboardData.length === 0) {
799
- this.leaderboardData = [
800
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
801
- ];
835
+ // Collect REAL active users only (Zero fake bots/users)
836
+ const userMap = new Map();
837
+ if (this.currentUser && this.currentUser.username && this.currentUser.username !== 'User') {
838
+ userMap.set(this.currentUser.username, {
839
+ username: this.currentUser.username,
840
+ rank: this.currentUser.rank || 'member',
841
+ version: window.REAPER_VERSION || '2.3.0',
842
+ playtime: this.playtimeSeconds || 0
843
+ });
844
+ }
845
+
846
+ if (this.onlineUsers && this.onlineUsers.length > 0) {
847
+ this.onlineUsers.forEach(u => {
848
+ if (u && u.username && u.username !== 'User' && u.username !== 'Member' && u.username !== 'Anon') {
849
+ userMap.set(u.username, u);
850
+ }
851
+ });
852
+ }
853
+
854
+ const realUsers = Array.from(userMap.values());
855
+
856
+ if (realUsers.length <= 1) {
857
+ container.innerHTML = `
858
+ <div style="color: #88aacc; padding: 1.15rem; text-align: center; font-size: 0.82rem; font-family: var(--font-mono); background: #040912; border: 1px dashed var(--blue-border); border-radius: 6px;">
859
+ 🟢 Only you are currently online. (0 fake users / 0 ghost bots)
860
+ </div>
861
+ `;
862
+ return;
802
863
  }
803
864
 
804
- container.innerHTML = this.leaderboardData.map(u => {
865
+ container.innerHTML = realUsers.map(u => {
805
866
  const isSelf = u.username === this.currentUser.username;
806
867
  const currentRank = (u.rank || 'member').toLowerCase();
807
- const timeStr = this.formatPlaytime(u.seconds || 0);
868
+ const timeStr = this.formatPlaytime(u.playtime || u.seconds || 0);
869
+ const userVer = u.version || '2.3.0';
870
+ const isLatest = userVer === window.REAPER_VERSION;
871
+
872
+ const verBadge = isLatest
873
+ ? `<span style="color: #00ffaa; font-size: 0.68rem; font-weight: 800; border: 1px solid #00ffaa; padding: 1px 5px; border-radius: 4px;">v${this.escapeHtml(userVer)} (LATEST)</span>`
874
+ : `<span style="color: #ff3377; font-size: 0.68rem; font-weight: 800; border: 1px solid #ff3377; background: rgba(255, 51, 119, 0.15); padding: 1px 5px; border-radius: 4px;">⚠️ OLD VER (v${this.escapeHtml(userVer)})</span>`;
808
875
 
809
876
  return `
810
- <div style="display: flex; justify-content: space-between; align-items: center; background: #041022; padding: 0.45rem 0.75rem; border-radius: 6px; border: 1px solid var(--blue-border); font-size: 0.8rem;">
811
- <div>
812
- <strong style="color: #fff;" class="${currentRank === 'owner' ? 'owner-animated-name' : ''}">${this.escapeHtml(u.username || 'User')}</strong>
877
+ <div style="display: flex; justify-content: space-between; align-items: center; background: #041022; padding: 0.5rem 0.75rem; border-radius: 6px; border: 1px solid var(--blue-border); font-size: 0.8rem; margin-bottom: 0.35rem;">
878
+ <div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
879
+ <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #00ffaa; box-shadow: 0 0 8px #00ffaa;"></span>
880
+ <strong style="color: #fff;" class="${currentRank === 'owner' ? 'owner-animated-name' : ''}">${this.escapeHtml(u.username)}</strong>
813
881
  <span class="rank-pill rank-${currentRank}">${currentRank.toUpperCase()}</span>
814
- <span style="color: #88ccff; margin-left: 0.5rem;">Playtime: ${timeStr}</span>
882
+ ${verBadge}
883
+ <span style="color: #88ccff; font-size: 0.75rem;">Playtime: ${timeStr}</span>
815
884
  </div>
816
885
  <div style="display: flex; gap: 0.35rem;">
817
886
  ${!isSelf ? `
818
887
  ${currentRank === 'member' ? `
819
- <button onclick="app.setUserRank('${u.username}', 'chat_mod')" class="btn-stealth" style="color: #00ffaa; padding: 0.2rem 0.5rem; font-size: 0.72rem;">+ PROMOTE MOD</button>
888
+ <button onclick="app.setUserRank('${this.escapeHtml(u.username)}', 'chat_mod')" class="btn-stealth" style="color: #00ffaa; padding: 0.2rem 0.5rem; font-size: 0.72rem;">+ PROMOTE MOD</button>
820
889
  ` : currentRank === 'chat_mod' || currentRank === 'mod' ? `
821
- <button onclick="app.setUserRank('${u.username}', 'member')" class="btn-stealth" style="color: #ffaa00; padding: 0.2rem 0.5rem; font-size: 0.72rem;">- DE-RANK MEMBER</button>
890
+ <button onclick="app.setUserRank('${this.escapeHtml(u.username)}', 'member')" class="btn-stealth" style="color: #ffaa00; padding: 0.2rem 0.5rem; font-size: 0.72rem;">- DE-RANK</button>
822
891
  ` : ''}
823
- <button onclick="app.kickUser('${u.username}')" class="btn-stealth" style="color: #ffaa00; padding: 0.2rem 0.5rem; font-size: 0.72rem;">KICK</button>
824
- <button onclick="app.banUser('${u.username}')" class="btn-stealth" style="color: #ff5555; padding: 0.2rem 0.5rem; font-size: 0.72rem;">BAN</button>
825
- ` : '<span style="color: #ffd700; font-weight: 800;">YOU</span>'}
892
+ <button onclick="app.kickUser('${this.escapeHtml(u.username)}')" class="btn-stealth" style="color: #ffaa00; padding: 0.2rem 0.5rem; font-size: 0.72rem;">KICK</button>
893
+ <button onclick="app.banUser('${this.escapeHtml(u.username)}')" class="btn-stealth" style="color: #ff5555; padding: 0.2rem 0.5rem; font-size: 0.72rem;">BAN</button>
894
+ ` : '<span style="color: #ffd700; font-weight: 800; font-size: 0.75rem;">YOU</span>'}
826
895
  </div>
827
896
  </div>
828
897
  `;
829
898
  }).join('');
830
899
  }
831
900
 
901
+ renderModUsersDirectory() {
902
+ const container = document.getElementById('mod-users-directory');
903
+ if (!container) return;
904
+
905
+ const userMap = new Map();
906
+ if (this.currentUser && this.currentUser.username && this.currentUser.username !== 'User') {
907
+ userMap.set(this.currentUser.username, {
908
+ username: this.currentUser.username,
909
+ rank: this.currentUser.rank || 'member',
910
+ version: window.REAPER_VERSION || '2.3.0'
911
+ });
912
+ }
913
+
914
+ if (this.onlineUsers && this.onlineUsers.length > 0) {
915
+ this.onlineUsers.forEach(u => {
916
+ if (u && u.username && u.username !== 'User' && u.username !== 'Member' && u.username !== 'Anon') {
917
+ userMap.set(u.username, u);
918
+ }
919
+ });
920
+ }
921
+
922
+ const realUsers = Array.from(userMap.values());
923
+
924
+ if (realUsers.length <= 1) {
925
+ container.innerHTML = `
926
+ <div style="color: #88aacc; padding: 0.85rem; text-align: center; font-size: 0.8rem; font-family: var(--font-mono); background: #040912; border: 1px dashed var(--blue-border); border-radius: 6px;">
927
+ 🟢 Only you are currently online. (0 fake users)
928
+ </div>
929
+ `;
930
+ return;
931
+ }
932
+
933
+ container.innerHTML = realUsers.map(u => {
934
+ const isSelf = u.username === this.currentUser.username;
935
+ const currentRank = (u.rank || 'member').toLowerCase();
936
+ const userVer = u.version || '2.3.0';
937
+ const isLatest = userVer === window.REAPER_VERSION;
938
+
939
+ const verBadge = isLatest
940
+ ? `<span style="color: #00ffaa; font-size: 0.65rem; font-weight: 800; border: 1px solid #00ffaa; padding: 1px 4px; border-radius: 4px;">v${this.escapeHtml(userVer)} (LATEST)</span>`
941
+ : `<span style="color: #ff3377; font-size: 0.65rem; font-weight: 800; border: 1px solid #ff3377; background: rgba(255, 51, 119, 0.15); padding: 1px 4px; border-radius: 4px;">⚠️ OLD VER (v${this.escapeHtml(userVer)})</span>`;
942
+
943
+ return `
944
+ <div style="display: flex; justify-content: space-between; align-items: center; background: #041022; padding: 0.4rem 0.65rem; border-radius: 6px; border: 1px solid var(--blue-border); font-size: 0.78rem; margin-bottom: 0.3rem;">
945
+ <div style="display: flex; align-items: center; gap: 0.45rem;">
946
+ <span style="display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #00ffaa;"></span>
947
+ <strong style="color: #fff;">${this.escapeHtml(u.username)}</strong>
948
+ <span class="rank-pill rank-${currentRank}">${currentRank.toUpperCase()}</span>
949
+ ${verBadge}
950
+ </div>
951
+ <div>
952
+ ${!isSelf ? `
953
+ <button onclick="app.modApplyTimeoutToUser('${this.escapeHtml(u.username)}')" class="btn-stealth" style="color: #ffaa00; padding: 0.2rem 0.4rem; font-size: 0.7rem;">MUTE</button>
954
+ ` : '<span style="color: #00ffaa; font-size: 0.7rem;">YOU</span>'}
955
+ </div>
956
+ </div>
957
+ `;
958
+ }).join('');
959
+ }
960
+
961
+ // Cross-Site Old Version Interceptor & Forwarder
962
+ handleOldVersionDetected(info) {
963
+ if (!info || !info.version || info.version === window.REAPER_VERSION) return;
964
+ if (info.forceUpgrade === false) return;
965
+
966
+ const currentVer = window.REAPER_VERSION;
967
+ const latestVer = info.version;
968
+ const latestUrl = info.url || window.REAPER_LATEST_URL;
969
+
970
+ let modal = document.getElementById('version-upgrade-modal');
971
+ if (!modal) {
972
+ modal = document.createElement('div');
973
+ modal.id = 'version-upgrade-modal';
974
+ modal.style.cssText = 'position: fixed; inset: 0; background: rgba(2, 5, 10, 0.98); z-index: 9999999999; display: flex; align-items: center; justify-content: center; padding: 1.5rem; text-align: center; font-family: var(--font-heading); backdrop-filter: blur(20px);';
975
+ document.body.appendChild(modal);
976
+ }
977
+
978
+ modal.innerHTML = `
979
+ <div style="max-width: 480px; background: #040e1c; border: 2px solid #00ffaa; border-radius: 16px; padding: 2.5rem 2rem; box-shadow: 0 0 60px rgba(0, 255, 170, 0.5);">
980
+ <div style="font-size: 3.5rem; margin-bottom: 0.5rem;">⚡</div>
981
+ <div style="font-size: 1.5rem; font-weight: 900; color: #00ffaa; letter-spacing: 0.08em; margin-bottom: 0.5rem;">UPGRADING TO NEW VERSION</div>
982
+ <p style="color: #adc4e2; font-size: 0.9rem; line-height: 1.6; margin-bottom: 1.25rem;">
983
+ You were on an older site build (<strong style="color: #ff5555;">v${currentVer}</strong>).<br>
984
+ Bringing you to the new high-speed version <strong style="color: #00ffaa;">v${latestVer}</strong> right now...
985
+ </p>
986
+ <div style="color: #00d2ff; font-size: 0.88rem; font-weight: 800; font-family: var(--font-mono);">
987
+ 🚀 REDIRECTING NOW...
988
+ </div>
989
+ </div>
990
+ `;
991
+
992
+ setTimeout(() => {
993
+ window.location.replace(latestUrl);
994
+ }, 400);
995
+ }
996
+
997
+ // Root Owner Version Control Broadcast
998
+ async publishLatestVersionToFirebase() {
999
+ const verInput = document.getElementById('admin-latest-version-input');
1000
+ const urlInput = document.getElementById('admin-latest-url-input');
1001
+ const version = verInput && verInput.value.trim() ? verInput.value.trim() : window.REAPER_VERSION;
1002
+ const url = urlInput && urlInput.value.trim() ? urlInput.value.trim() : window.REAPER_LATEST_URL;
1003
+
1004
+ if (window.reaperFirebase) {
1005
+ await window.reaperFirebase.setLatestVersionInfo({
1006
+ version: version,
1007
+ url: url,
1008
+ forceUpgrade: true,
1009
+ updatedAt: Date.now()
1010
+ });
1011
+ // Also broadcast an emergency alert to all legacy versions (like 1.0.9) that listen to reaperGlobalAlert
1012
+ await window.reaperFirebase.setGlobalAlert(`NOTICE: Please update to the latest Reaper site: ${url}`);
1013
+ }
1014
+
1015
+ this.showToast(`Version ${version} broadcasted! All older sites will forward here.`);
1016
+ }
1017
+
832
1018
  async sendGlobalAlert() {
833
1019
  if (this.currentUser.rank !== 'owner') return;
834
1020
  const input = document.getElementById('admin-broadcast-msg');
@@ -1053,31 +1239,38 @@ class ReaperHubApp {
1053
1239
  }
1054
1240
  }, 1100);
1055
1241
 
1242
+ const terminalText = document.getElementById('splash-terminal-text');
1243
+
1056
1244
  let progress = 0;
1057
- const interval = 100;
1245
+ const interval = 18; // Super snappy 1.8-second cyber boot sequence
1058
1246
 
1059
1247
  const timer = setInterval(() => {
1060
1248
  progress += 1;
1061
1249
  bar.style.width = `${progress}%`;
1062
1250
 
1063
- if (progress < 20) {
1064
- if (status) status.textContent = `[${progress}%] INITIALIZING REAPER CORE...`;
1065
- } else if (progress < 45) {
1251
+ if (progress < 25) {
1252
+ if (status) status.textContent = `[${progress}%] INITIALIZING REAPER KERNEL...`;
1253
+ if (terminalText) terminalText.textContent = "[KERNEL] WebGL 2.0 & Canvas Accelerator Active";
1254
+ } else if (progress < 50) {
1066
1255
  if (status) status.textContent = `[${progress}%] CONNECTING REALTIME FIREBASE DB...`;
1067
- } else if (progress < 70) {
1068
- if (status) status.textContent = `[${progress}%] UNBLOCKING FASTLY CDN ASSETS...`;
1069
- } else if (progress < 90) {
1070
- if (status) status.textContent = `[${progress}%] DECRYPTING 788 GAME MANIFESTS...`;
1071
- } else if (progress < 100) {
1072
- if (status) status.textContent = `[${progress}%] PREPARING STARFIELD ENGINE...`;
1256
+ if (terminalText) terminalText.textContent = "[DATABASE] Firebase Realtime Sync Established";
1257
+ } else if (progress < 75) {
1258
+ if (status) status.textContent = `[${progress}%] UNBLOCKING GITHUB PAGES CDN...`;
1259
+ if (terminalText) terminalText.textContent = "[CDN] High-Speed GitHub Pages Stream Ready";
1260
+ } else if (progress < 95) {
1261
+ if (status) status.textContent = `[${progress}%] TURBO PRE-CACHING 788 GAMES...`;
1262
+ if (terminalText) terminalText.textContent = "[TURBO ENGINE] 788 Games Cached & Ready (60 FPS)";
1073
1263
  } else {
1074
1264
  if (status) status.textContent = `[100%] ALL SYSTEMS ONLINE`;
1265
+ if (terminalText) terminalText.textContent = "[BOOT] Core Online. Welcome to Reaper Hub!";
1075
1266
  clearInterval(timer);
1076
1267
  clearInterval(quoteInterval);
1077
1268
  setTimeout(() => {
1269
+ splash.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
1078
1270
  splash.style.opacity = '0';
1079
- setTimeout(() => { splash.style.display = 'none'; }, 600);
1080
- }, 400);
1271
+ splash.style.transform = 'scale(1.03)';
1272
+ setTimeout(() => { splash.style.display = 'none'; }, 400);
1273
+ }, 200);
1081
1274
  }
1082
1275
  }, interval);
1083
1276
  }
@@ -1167,24 +1360,18 @@ class ReaperHubApp {
1167
1360
  window.reaperFirebase.db.ref("reaperPlaytime").on("value", (snap) => {
1168
1361
  const val = snap.val();
1169
1362
  if (val) {
1170
- this.leaderboardData = Object.values(val);
1363
+ this.leaderboardData = Object.values(val).filter(u => {
1364
+ return u && u.username && u.username !== 'User' && u.username !== 'Member' && u.username !== 'Anon' && (u.seconds || 0) > 0;
1365
+ });
1171
1366
  this.leaderboardData.sort((a, b) => (b.seconds || 0) - (a.seconds || 0));
1172
1367
  } else {
1173
- this.leaderboardData = [
1174
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
1175
- ];
1368
+ this.leaderboardData = [];
1176
1369
  }
1177
1370
  this.renderLeaderboardUI();
1178
- this.renderAdminUsersDirectory();
1179
1371
  });
1180
1372
  } catch (e) {
1181
1373
  this.renderLeaderboardUI();
1182
1374
  }
1183
- } else {
1184
- this.leaderboardData = [
1185
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
1186
- ];
1187
- this.renderLeaderboardUI();
1188
1375
  }
1189
1376
  }
1190
1377
 
@@ -1200,13 +1387,21 @@ class ReaperHubApp {
1200
1387
  const container = document.getElementById('leaderboard-rows-container');
1201
1388
  if (!container) return;
1202
1389
 
1203
- if (this.leaderboardData.length === 0) {
1204
- this.leaderboardData = [
1205
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
1206
- ];
1390
+ // Filter strictly to real players with recorded playtime
1391
+ const realPlayers = (this.leaderboardData || []).filter(p => {
1392
+ return p && p.username && p.username !== 'User' && p.username !== 'Member' && p.username !== 'Anon' && (p.seconds || 0) > 0;
1393
+ });
1394
+
1395
+ if (realPlayers.length === 0) {
1396
+ container.innerHTML = `
1397
+ <div style="text-align: center; padding: 2rem; color: #88aacc; font-family: var(--font-mono); font-size: 0.85rem;">
1398
+ Play any game to register your playtime on the global leaderboard!
1399
+ </div>
1400
+ `;
1401
+ return;
1207
1402
  }
1208
1403
 
1209
- container.innerHTML = this.leaderboardData.slice(0, 20).map((player, idx) => {
1404
+ container.innerHTML = realPlayers.slice(0, 20).map((player, idx) => {
1210
1405
  const rankNum = idx + 1;
1211
1406
  const timeStr = this.formatPlaytime(player.seconds || 0);
1212
1407
  const isSelf = player.username === this.currentUser.username;
@@ -742,6 +742,102 @@ class FirebaseService {
742
742
  } catch (e) {}
743
743
  }
744
744
  }
745
+
746
+ // Real-time Online Users Presence System (Zero Fake Users)
747
+ registerOnlinePresence(userObj) {
748
+ if (!this.db || !userObj || !userObj.username || userObj.username === 'User' || userObj.username === 'guest') return;
749
+
750
+ try {
751
+ const cleanUsername = userObj.username.replace(/[.#$[\]]/g, '_');
752
+ const userPresenceRef = this.db.ref(`reaperOnlineUsers/${cleanUsername}`);
753
+ const connectedRef = this.db.ref('.info/connected');
754
+
755
+ connectedRef.on('value', (snap) => {
756
+ if (snap.val() === true) {
757
+ userPresenceRef.onDisconnect().remove();
758
+ userPresenceRef.set({
759
+ username: userObj.username,
760
+ rank: userObj.rank || 'member',
761
+ version: userObj.version || window.REAPER_VERSION || '2.3.0',
762
+ playtime: userObj.playtime || 0,
763
+ connectedAt: Date.now(),
764
+ lastSeen: Date.now()
765
+ });
766
+ }
767
+ });
768
+
769
+ // Keepalive heartbeat every 20 seconds
770
+ if (this.presenceHeartbeat) clearInterval(this.presenceHeartbeat);
771
+ this.presenceHeartbeat = setInterval(() => {
772
+ userPresenceRef.update({
773
+ lastSeen: Date.now(),
774
+ playtime: userObj.playtime || 0
775
+ }).catch(() => {});
776
+ }, 20000);
777
+ } catch (e) {
778
+ console.warn("Online presence error:", e);
779
+ }
780
+ }
781
+
782
+ listenOnlineUsers(callback) {
783
+ if (this.db) {
784
+ try {
785
+ this.db.ref("reaperOnlineUsers").on("value", (snap) => {
786
+ const val = snap.val();
787
+ let list = [];
788
+ if (val) {
789
+ const now = Date.now();
790
+ list = Object.values(val).filter(u => {
791
+ return u && u.username && u.username !== 'User' && u.username !== 'Member' && (now - (u.lastSeen || 0) < 60000);
792
+ });
793
+ }
794
+ callback(list);
795
+ });
796
+ } catch (e) {
797
+ callback([]);
798
+ }
799
+ } else {
800
+ callback([]);
801
+ }
802
+ }
803
+
804
+ // Cross-Version Global Upgrade & Redirect Detection
805
+ listenLatestVersion(callback) {
806
+ if (this.db) {
807
+ try {
808
+ this.db.ref("reaperLatestVersionInfo").on("value", (snap) => {
809
+ const val = snap.val();
810
+ if (val) {
811
+ callback(val);
812
+ }
813
+ });
814
+ } catch (e) {}
815
+ }
816
+ }
817
+
818
+ async setLatestVersionInfo(versionInfo) {
819
+ if (this.db) {
820
+ try {
821
+ await this.db.ref("reaperLatestVersionInfo").set(versionInfo);
822
+ } catch (e) {}
823
+ }
824
+ }
825
+
826
+ // Auto-clean ghost dummy accounts from Firebase DB
827
+ async purgeGhostUsers() {
828
+ if (!this.db) return;
829
+ try {
830
+ const snap = await this.db.ref("reaperPlaytime").once("value");
831
+ const val = snap.val();
832
+ if (val) {
833
+ for (const k of Object.keys(val)) {
834
+ if (!k || k === 'User' || k === 'Member' || k === 'null' || k === 'undefined' || !val[k].username || val[k].username === 'User') {
835
+ await this.db.ref(`reaperPlaytime/${k}`).remove();
836
+ }
837
+ }
838
+ }
839
+ } catch (e) {}
840
+ }
745
841
  }
746
842
 
747
843
  window.reaperFirebase = new FirebaseService();