reaper-arcade-hub 2.2.0 → 2.3.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/index.html CHANGED
@@ -280,6 +280,16 @@
280
280
  <input type="checkbox" id="mod-slowmode-toggle" style="width: 22px; height: 22px; cursor: pointer; accent-color: #00ffaa;" onchange="app.toggleSlowMode(this.checked)">
281
281
  </div>
282
282
 
283
+ <!-- Live Online Users for Mods -->
284
+ <div class="mod-section">
285
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
286
+ <h3 style="font-size: 0.95rem; color: #00e676;">🟢 ACTIVE PLAYERS ONLINE (REALTIME)</h3>
287
+ <button onclick="app.renderModUsersDirectory()" class="btn-stealth" style="font-size: 0.75rem;">REFRESH</button>
288
+ </div>
289
+ <p style="color: #88aacc; font-size: 0.8rem; margin-bottom: 0.65rem;">See connected users, their rank, and their active site version.</p>
290
+ <div id="mod-users-directory" style="max-height: 200px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.4rem;"></div>
291
+ </div>
292
+
283
293
  <!-- Timeout User Box -->
284
294
  <div class="mod-section">
285
295
  <h3 style="font-size: 0.95rem; color: #fff; margin-bottom: 0.35rem;">TIMEOUT / MUTE USER</h3>
@@ -320,11 +330,15 @@
320
330
  <!-- Superpower 1: Live Real-Time Analytics Dashboard -->
321
331
  <div class="admin-section" style="border-color: #00d2ff; background: rgba(0, 210, 255, 0.05);">
322
332
  <h3 style="font-size: 0.95rem; color: #00d2ff; margin-bottom: 0.6rem;">📊 LIVE HUB SYSTEM ANALYTICS</h3>
323
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.65rem;">
333
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 0.65rem;">
324
334
  <div style="background: #040912; border: 1px solid var(--blue-border); padding: 0.65rem; border-radius: 6px; text-align: center;">
325
335
  <div style="font-size: 0.7rem; color: #88aacc;">TOTAL GAMES</div>
326
336
  <div style="font-size: 1.3rem; font-weight: 800; color: #00ffaa;" id="admin-stat-games">788</div>
327
337
  </div>
338
+ <div style="background: #040912; border: 1px solid var(--blue-border); padding: 0.65rem; border-radius: 6px; text-align: center;">
339
+ <div style="font-size: 0.7rem; color: #88aacc;">ONLINE USERS</div>
340
+ <div style="font-size: 1.3rem; font-weight: 800; color: #00e676;" id="admin-stat-online">1</div>
341
+ </div>
328
342
  <div style="background: #040912; border: 1px solid var(--blue-border); padding: 0.65rem; border-radius: 6px; text-align: center;">
329
343
  <div style="font-size: 0.7rem; color: #88aacc;">TURBO CACHED</div>
330
344
  <div style="font-size: 1.3rem; font-weight: 800; color: #00d2ff;" id="admin-stat-cached">0</div>
@@ -340,6 +354,21 @@
340
354
  </div>
341
355
  </div>
342
356
 
357
+ <!-- Superpower 6: Cross-Site Version Control & Auto-Forwarder -->
358
+ <div class="admin-section" style="border-color: #00ffaa; background: #02140a;">
359
+ <h3 style="font-size: 0.95rem; color: #00ffaa; margin-bottom: 0.4rem;">🌐 GLOBAL VERSION CONTROLLER &amp; AUTO-REDIRECT</h3>
360
+ <p style="color: #88ffcc; font-size: 0.8rem; margin-bottom: 0.75rem;">
361
+ When you publish a new version, update this once. Any user visiting ANY old version (1.0.9, 2.0.x, etc.) will be notified and instantly brought to the new site!
362
+ </p>
363
+ <div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.65rem;">
364
+ <input type="text" id="admin-latest-version-input" class="red-chat-input" placeholder="Latest Version (e.g. 2.3.0)" value="2.3.0" style="max-width: 140px;">
365
+ <input type="text" id="admin-latest-url-input" class="red-chat-input" placeholder="New UNPKG URL" value="https://unpkg.com/reaper-arcade-hub@2.3.0/main/index.html" style="flex: 1; min-width: 250px;">
366
+ <button class="btn-primary" style="background: #00e676; color: #02050a; font-weight: 900; border-color: #00ff88;" onclick="app.publishLatestVersionToFirebase()">
367
+ BROADCAST VERSION &amp; FORWARD ALL SITES
368
+ </button>
369
+ </div>
370
+ </div>
371
+
343
372
  <!-- Superpower 2: Global Enforce Turbo Mode (Anti-Lag Broadcast) -->
344
373
  <div class="admin-section" style="border-color: #00ffaa; background: #03140c;">
345
374
  <div style="display: flex; justify-content: space-between; align-items: center;">
@@ -412,14 +441,14 @@
412
441
  </div>
413
442
  </div>
414
443
 
415
- <!-- Connected Players Directory (With Promote/Demote/Kick/Ban) -->
444
+ <!-- Connected Real Players Directory (With Version & Controls) -->
416
445
  <div class="admin-section">
417
446
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
418
- <h3 style="font-size: 0.95rem; color: #fff;">LIVE PLAYERS DIRECTORY &amp; ROLES</h3>
447
+ <h3 style="font-size: 0.95rem; color: #00e676;">🟢 REALTIME ONLINE PLAYERS (ZERO BOTS)</h3>
419
448
  <button onclick="app.renderAdminUsersDirectory()" class="btn-stealth" style="font-size: 0.75rem;">REFRESH LIST</button>
420
449
  </div>
421
- <p style="color: #88aacc; font-size: 0.8rem; margin-bottom: 0.65rem;">Manage connected users, promote to Chat Mod, de-rank, kick, or permanently ban.</p>
422
- <div id="admin-users-directory" style="max-height: 200px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.4rem;"></div>
450
+ <p style="color: #88aacc; font-size: 0.8rem; margin-bottom: 0.65rem;">Active real players currently connected to the hub. View their current site version, rank, and moderation actions.</p>
451
+ <div id="admin-users-directory" style="max-height: 220px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.4rem;"></div>
423
452
  </div>
424
453
 
425
454
  <!-- Custom Access Key Generator -->
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.3.0 Turbo Edition)
2
+ window.REAPER_VERSION = "2.3.0";
3
+ window.REAPER_LATEST_URL = "https://unpkg.com/reaper-arcade-hub@2.3.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
@@ -795,40 +817,192 @@ class ReaperHubApp {
795
817
  const container = document.getElementById('admin-users-directory');
796
818
  if (!container) return;
797
819
 
798
- if (this.leaderboardData.length === 0) {
799
- this.leaderboardData = [
800
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
801
- ];
820
+ // Collect REAL active users only (Zero fake bots/users)
821
+ const userMap = new Map();
822
+ if (this.currentUser && this.currentUser.username && this.currentUser.username !== 'User') {
823
+ userMap.set(this.currentUser.username, {
824
+ username: this.currentUser.username,
825
+ rank: this.currentUser.rank || 'member',
826
+ version: window.REAPER_VERSION || '2.3.0',
827
+ playtime: this.playtimeSeconds || 0
828
+ });
829
+ }
830
+
831
+ if (this.onlineUsers && this.onlineUsers.length > 0) {
832
+ this.onlineUsers.forEach(u => {
833
+ if (u && u.username && u.username !== 'User' && u.username !== 'Member' && u.username !== 'Anon') {
834
+ userMap.set(u.username, u);
835
+ }
836
+ });
837
+ }
838
+
839
+ const realUsers = Array.from(userMap.values());
840
+
841
+ if (realUsers.length <= 1) {
842
+ container.innerHTML = `
843
+ <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;">
844
+ 🟢 Only you are currently online. (0 fake users / 0 ghost bots)
845
+ </div>
846
+ `;
847
+ return;
802
848
  }
803
849
 
804
- container.innerHTML = this.leaderboardData.map(u => {
850
+ container.innerHTML = realUsers.map(u => {
805
851
  const isSelf = u.username === this.currentUser.username;
806
852
  const currentRank = (u.rank || 'member').toLowerCase();
807
- const timeStr = this.formatPlaytime(u.seconds || 0);
853
+ const timeStr = this.formatPlaytime(u.playtime || u.seconds || 0);
854
+ const userVer = u.version || '2.3.0';
855
+ const isLatest = userVer === window.REAPER_VERSION;
856
+
857
+ const verBadge = isLatest
858
+ ? `<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>`
859
+ : `<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
860
 
809
861
  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>
862
+ <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;">
863
+ <div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
864
+ <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #00ffaa; box-shadow: 0 0 8px #00ffaa;"></span>
865
+ <strong style="color: #fff;" class="${currentRank === 'owner' ? 'owner-animated-name' : ''}">${this.escapeHtml(u.username)}</strong>
813
866
  <span class="rank-pill rank-${currentRank}">${currentRank.toUpperCase()}</span>
814
- <span style="color: #88ccff; margin-left: 0.5rem;">Playtime: ${timeStr}</span>
867
+ ${verBadge}
868
+ <span style="color: #88ccff; font-size: 0.75rem;">Playtime: ${timeStr}</span>
815
869
  </div>
816
870
  <div style="display: flex; gap: 0.35rem;">
817
871
  ${!isSelf ? `
818
872
  ${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>
873
+ <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
874
  ` : 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>
875
+ <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
876
  ` : ''}
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>'}
877
+ <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>
878
+ <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>
879
+ ` : '<span style="color: #ffd700; font-weight: 800; font-size: 0.75rem;">YOU</span>'}
880
+ </div>
881
+ </div>
882
+ `;
883
+ }).join('');
884
+ }
885
+
886
+ renderModUsersDirectory() {
887
+ const container = document.getElementById('mod-users-directory');
888
+ if (!container) return;
889
+
890
+ const userMap = new Map();
891
+ if (this.currentUser && this.currentUser.username && this.currentUser.username !== 'User') {
892
+ userMap.set(this.currentUser.username, {
893
+ username: this.currentUser.username,
894
+ rank: this.currentUser.rank || 'member',
895
+ version: window.REAPER_VERSION || '2.3.0'
896
+ });
897
+ }
898
+
899
+ if (this.onlineUsers && this.onlineUsers.length > 0) {
900
+ this.onlineUsers.forEach(u => {
901
+ if (u && u.username && u.username !== 'User' && u.username !== 'Member' && u.username !== 'Anon') {
902
+ userMap.set(u.username, u);
903
+ }
904
+ });
905
+ }
906
+
907
+ const realUsers = Array.from(userMap.values());
908
+
909
+ if (realUsers.length <= 1) {
910
+ container.innerHTML = `
911
+ <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;">
912
+ 🟢 Only you are currently online. (0 fake users)
913
+ </div>
914
+ `;
915
+ return;
916
+ }
917
+
918
+ container.innerHTML = realUsers.map(u => {
919
+ const isSelf = u.username === this.currentUser.username;
920
+ const currentRank = (u.rank || 'member').toLowerCase();
921
+ const userVer = u.version || '2.3.0';
922
+ const isLatest = userVer === window.REAPER_VERSION;
923
+
924
+ const verBadge = isLatest
925
+ ? `<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>`
926
+ : `<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>`;
927
+
928
+ return `
929
+ <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;">
930
+ <div style="display: flex; align-items: center; gap: 0.45rem;">
931
+ <span style="display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #00ffaa;"></span>
932
+ <strong style="color: #fff;">${this.escapeHtml(u.username)}</strong>
933
+ <span class="rank-pill rank-${currentRank}">${currentRank.toUpperCase()}</span>
934
+ ${verBadge}
935
+ </div>
936
+ <div>
937
+ ${!isSelf ? `
938
+ <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>
939
+ ` : '<span style="color: #00ffaa; font-size: 0.7rem;">YOU</span>'}
826
940
  </div>
827
941
  </div>
828
942
  `;
829
943
  }).join('');
830
944
  }
831
945
 
946
+ // Cross-Site Old Version Interceptor & Forwarder
947
+ handleOldVersionDetected(info) {
948
+ if (!info || !info.version || info.version === window.REAPER_VERSION) return;
949
+ if (info.forceUpgrade === false) return;
950
+
951
+ const currentVer = window.REAPER_VERSION;
952
+ const latestVer = info.version;
953
+ const latestUrl = info.url || window.REAPER_LATEST_URL;
954
+
955
+ let modal = document.getElementById('version-upgrade-modal');
956
+ if (!modal) {
957
+ modal = document.createElement('div');
958
+ modal.id = 'version-upgrade-modal';
959
+ modal.style.cssText = 'position: fixed; inset: 0; background: rgba(2, 5, 10, 0.97); 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(12px);';
960
+ document.body.appendChild(modal);
961
+ }
962
+
963
+ modal.innerHTML = `
964
+ <div style="max-width: 500px; background: #040e1c; border: 2px solid #00d2ff; border-radius: 12px; padding: 2.5rem 2rem; box-shadow: 0 0 50px rgba(0, 210, 255, 0.4);">
965
+ <div style="font-size: 3rem; margin-bottom: 0.5rem;">⚡</div>
966
+ <div style="font-size: 1.4rem; font-weight: 900; color: #00d2ff; letter-spacing: 0.08em; margin-bottom: 0.5rem;">NEW VERSION ACTIVE</div>
967
+ <p style="color: #adc4e2; font-size: 0.9rem; line-height: 1.6; margin-bottom: 1.25rem;">
968
+ You are currently visiting an older version of Reaper Hub (<strong style="color: #ff5555;">v${currentVer}</strong>).<br>
969
+ The new upgraded site is live at <strong style="color: #00ffaa;">v${latestVer}</strong> with zero lag and fixes.
970
+ </p>
971
+ <div style="color: #00ffaa; font-size: 0.85rem; font-weight: 800; margin-bottom: 1.5rem; font-family: var(--font-mono);">
972
+ 🚀 BRINGING YOU TO THE NEW SITE IN 2 SECONDS...
973
+ </div>
974
+ <button class="btn-primary" style="width: 100%; justify-content: center; padding: 0.85rem; font-size: 0.9rem; background: #00d2ff; color: #000; font-weight: 900;" onclick="window.location.replace('${latestUrl}')">
975
+ OPEN NEW SITE NOW ➔
976
+ </button>
977
+ </div>
978
+ `;
979
+
980
+ setTimeout(() => {
981
+ window.location.replace(latestUrl);
982
+ }, 2000);
983
+ }
984
+
985
+ // Root Owner Version Control Broadcast
986
+ async publishLatestVersionToFirebase() {
987
+ const verInput = document.getElementById('admin-latest-version-input');
988
+ const urlInput = document.getElementById('admin-latest-url-input');
989
+ const version = verInput && verInput.value.trim() ? verInput.value.trim() : window.REAPER_VERSION;
990
+ const url = urlInput && urlInput.value.trim() ? urlInput.value.trim() : window.REAPER_LATEST_URL;
991
+
992
+ if (window.reaperFirebase) {
993
+ await window.reaperFirebase.setLatestVersionInfo({
994
+ version: version,
995
+ url: url,
996
+ forceUpgrade: true,
997
+ updatedAt: Date.now()
998
+ });
999
+ // Also broadcast an emergency alert to all legacy versions (like 1.0.9) that listen to reaperGlobalAlert
1000
+ await window.reaperFirebase.setGlobalAlert(`NOTICE: Please update to the latest Reaper site: ${url}`);
1001
+ }
1002
+
1003
+ this.showToast(`Version ${version} broadcasted! All older sites will forward here.`);
1004
+ }
1005
+
832
1006
  async sendGlobalAlert() {
833
1007
  if (this.currentUser.rank !== 'owner') return;
834
1008
  const input = document.getElementById('admin-broadcast-msg');
@@ -1167,24 +1341,18 @@ class ReaperHubApp {
1167
1341
  window.reaperFirebase.db.ref("reaperPlaytime").on("value", (snap) => {
1168
1342
  const val = snap.val();
1169
1343
  if (val) {
1170
- this.leaderboardData = Object.values(val);
1344
+ this.leaderboardData = Object.values(val).filter(u => {
1345
+ return u && u.username && u.username !== 'User' && u.username !== 'Member' && u.username !== 'Anon' && (u.seconds || 0) > 0;
1346
+ });
1171
1347
  this.leaderboardData.sort((a, b) => (b.seconds || 0) - (a.seconds || 0));
1172
1348
  } else {
1173
- this.leaderboardData = [
1174
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
1175
- ];
1349
+ this.leaderboardData = [];
1176
1350
  }
1177
1351
  this.renderLeaderboardUI();
1178
- this.renderAdminUsersDirectory();
1179
1352
  });
1180
1353
  } catch (e) {
1181
1354
  this.renderLeaderboardUI();
1182
1355
  }
1183
- } else {
1184
- this.leaderboardData = [
1185
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
1186
- ];
1187
- this.renderLeaderboardUI();
1188
1356
  }
1189
1357
  }
1190
1358
 
@@ -1200,13 +1368,21 @@ class ReaperHubApp {
1200
1368
  const container = document.getElementById('leaderboard-rows-container');
1201
1369
  if (!container) return;
1202
1370
 
1203
- if (this.leaderboardData.length === 0) {
1204
- this.leaderboardData = [
1205
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
1206
- ];
1371
+ // Filter strictly to real players with recorded playtime
1372
+ const realPlayers = (this.leaderboardData || []).filter(p => {
1373
+ return p && p.username && p.username !== 'User' && p.username !== 'Member' && p.username !== 'Anon' && (p.seconds || 0) > 0;
1374
+ });
1375
+
1376
+ if (realPlayers.length === 0) {
1377
+ container.innerHTML = `
1378
+ <div style="text-align: center; padding: 2rem; color: #88aacc; font-family: var(--font-mono); font-size: 0.85rem;">
1379
+ Play any game to register your playtime on the global leaderboard!
1380
+ </div>
1381
+ `;
1382
+ return;
1207
1383
  }
1208
1384
 
1209
- container.innerHTML = this.leaderboardData.slice(0, 20).map((player, idx) => {
1385
+ container.innerHTML = realPlayers.slice(0, 20).map((player, idx) => {
1210
1386
  const rankNum = idx + 1;
1211
1387
  const timeStr = this.formatPlaytime(player.seconds || 0);
1212
1388
  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();
package/main/index.html CHANGED
@@ -280,6 +280,16 @@
280
280
  <input type="checkbox" id="mod-slowmode-toggle" style="width: 22px; height: 22px; cursor: pointer; accent-color: #00ffaa;" onchange="app.toggleSlowMode(this.checked)">
281
281
  </div>
282
282
 
283
+ <!-- Live Online Users for Mods -->
284
+ <div class="mod-section">
285
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
286
+ <h3 style="font-size: 0.95rem; color: #00e676;">🟢 ACTIVE PLAYERS ONLINE (REALTIME)</h3>
287
+ <button onclick="app.renderModUsersDirectory()" class="btn-stealth" style="font-size: 0.75rem;">REFRESH</button>
288
+ </div>
289
+ <p style="color: #88aacc; font-size: 0.8rem; margin-bottom: 0.65rem;">See connected users, their rank, and their active site version.</p>
290
+ <div id="mod-users-directory" style="max-height: 200px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.4rem;"></div>
291
+ </div>
292
+
283
293
  <!-- Timeout User Box -->
284
294
  <div class="mod-section">
285
295
  <h3 style="font-size: 0.95rem; color: #fff; margin-bottom: 0.35rem;">TIMEOUT / MUTE USER</h3>
@@ -320,11 +330,15 @@
320
330
  <!-- Superpower 1: Live Real-Time Analytics Dashboard -->
321
331
  <div class="admin-section" style="border-color: #00d2ff; background: rgba(0, 210, 255, 0.05);">
322
332
  <h3 style="font-size: 0.95rem; color: #00d2ff; margin-bottom: 0.6rem;">📊 LIVE HUB SYSTEM ANALYTICS</h3>
323
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.65rem;">
333
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 0.65rem;">
324
334
  <div style="background: #040912; border: 1px solid var(--blue-border); padding: 0.65rem; border-radius: 6px; text-align: center;">
325
335
  <div style="font-size: 0.7rem; color: #88aacc;">TOTAL GAMES</div>
326
336
  <div style="font-size: 1.3rem; font-weight: 800; color: #00ffaa;" id="admin-stat-games">788</div>
327
337
  </div>
338
+ <div style="background: #040912; border: 1px solid var(--blue-border); padding: 0.65rem; border-radius: 6px; text-align: center;">
339
+ <div style="font-size: 0.7rem; color: #88aacc;">ONLINE USERS</div>
340
+ <div style="font-size: 1.3rem; font-weight: 800; color: #00e676;" id="admin-stat-online">1</div>
341
+ </div>
328
342
  <div style="background: #040912; border: 1px solid var(--blue-border); padding: 0.65rem; border-radius: 6px; text-align: center;">
329
343
  <div style="font-size: 0.7rem; color: #88aacc;">TURBO CACHED</div>
330
344
  <div style="font-size: 1.3rem; font-weight: 800; color: #00d2ff;" id="admin-stat-cached">0</div>
@@ -340,6 +354,21 @@
340
354
  </div>
341
355
  </div>
342
356
 
357
+ <!-- Superpower 6: Cross-Site Version Control & Auto-Forwarder -->
358
+ <div class="admin-section" style="border-color: #00ffaa; background: #02140a;">
359
+ <h3 style="font-size: 0.95rem; color: #00ffaa; margin-bottom: 0.4rem;">🌐 GLOBAL VERSION CONTROLLER &amp; AUTO-REDIRECT</h3>
360
+ <p style="color: #88ffcc; font-size: 0.8rem; margin-bottom: 0.75rem;">
361
+ When you publish a new version, update this once. Any user visiting ANY old version (1.0.9, 2.0.x, etc.) will be notified and instantly brought to the new site!
362
+ </p>
363
+ <div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.65rem;">
364
+ <input type="text" id="admin-latest-version-input" class="red-chat-input" placeholder="Latest Version (e.g. 2.3.0)" value="2.3.0" style="max-width: 140px;">
365
+ <input type="text" id="admin-latest-url-input" class="red-chat-input" placeholder="New UNPKG URL" value="https://unpkg.com/reaper-arcade-hub@2.3.0/main/index.html" style="flex: 1; min-width: 250px;">
366
+ <button class="btn-primary" style="background: #00e676; color: #02050a; font-weight: 900; border-color: #00ff88;" onclick="app.publishLatestVersionToFirebase()">
367
+ BROADCAST VERSION &amp; FORWARD ALL SITES
368
+ </button>
369
+ </div>
370
+ </div>
371
+
343
372
  <!-- Superpower 2: Global Enforce Turbo Mode (Anti-Lag Broadcast) -->
344
373
  <div class="admin-section" style="border-color: #00ffaa; background: #03140c;">
345
374
  <div style="display: flex; justify-content: space-between; align-items: center;">
@@ -412,14 +441,14 @@
412
441
  </div>
413
442
  </div>
414
443
 
415
- <!-- Connected Players Directory (With Promote/Demote/Kick/Ban) -->
444
+ <!-- Connected Real Players Directory (With Version & Controls) -->
416
445
  <div class="admin-section">
417
446
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
418
- <h3 style="font-size: 0.95rem; color: #fff;">LIVE PLAYERS DIRECTORY &amp; ROLES</h3>
447
+ <h3 style="font-size: 0.95rem; color: #00e676;">🟢 REALTIME ONLINE PLAYERS (ZERO BOTS)</h3>
419
448
  <button onclick="app.renderAdminUsersDirectory()" class="btn-stealth" style="font-size: 0.75rem;">REFRESH LIST</button>
420
449
  </div>
421
- <p style="color: #88aacc; font-size: 0.8rem; margin-bottom: 0.65rem;">Manage connected users, promote to Chat Mod, de-rank, kick, or permanently ban.</p>
422
- <div id="admin-users-directory" style="max-height: 200px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.4rem;"></div>
450
+ <p style="color: #88aacc; font-size: 0.8rem; margin-bottom: 0.65rem;">Active real players currently connected to the hub. View their current site version, rank, and moderation actions.</p>
451
+ <div id="admin-users-directory" style="max-height: 220px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.4rem;"></div>
423
452
  </div>
424
453
 
425
454
  <!-- Custom Access Key Generator -->
package/main/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.3.0 Turbo Edition)
2
+ window.REAPER_VERSION = "2.3.0";
3
+ window.REAPER_LATEST_URL = "https://unpkg.com/reaper-arcade-hub@2.3.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
@@ -795,40 +817,192 @@ class ReaperHubApp {
795
817
  const container = document.getElementById('admin-users-directory');
796
818
  if (!container) return;
797
819
 
798
- if (this.leaderboardData.length === 0) {
799
- this.leaderboardData = [
800
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
801
- ];
820
+ // Collect REAL active users only (Zero fake bots/users)
821
+ const userMap = new Map();
822
+ if (this.currentUser && this.currentUser.username && this.currentUser.username !== 'User') {
823
+ userMap.set(this.currentUser.username, {
824
+ username: this.currentUser.username,
825
+ rank: this.currentUser.rank || 'member',
826
+ version: window.REAPER_VERSION || '2.3.0',
827
+ playtime: this.playtimeSeconds || 0
828
+ });
829
+ }
830
+
831
+ if (this.onlineUsers && this.onlineUsers.length > 0) {
832
+ this.onlineUsers.forEach(u => {
833
+ if (u && u.username && u.username !== 'User' && u.username !== 'Member' && u.username !== 'Anon') {
834
+ userMap.set(u.username, u);
835
+ }
836
+ });
837
+ }
838
+
839
+ const realUsers = Array.from(userMap.values());
840
+
841
+ if (realUsers.length <= 1) {
842
+ container.innerHTML = `
843
+ <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;">
844
+ 🟢 Only you are currently online. (0 fake users / 0 ghost bots)
845
+ </div>
846
+ `;
847
+ return;
802
848
  }
803
849
 
804
- container.innerHTML = this.leaderboardData.map(u => {
850
+ container.innerHTML = realUsers.map(u => {
805
851
  const isSelf = u.username === this.currentUser.username;
806
852
  const currentRank = (u.rank || 'member').toLowerCase();
807
- const timeStr = this.formatPlaytime(u.seconds || 0);
853
+ const timeStr = this.formatPlaytime(u.playtime || u.seconds || 0);
854
+ const userVer = u.version || '2.3.0';
855
+ const isLatest = userVer === window.REAPER_VERSION;
856
+
857
+ const verBadge = isLatest
858
+ ? `<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>`
859
+ : `<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
860
 
809
861
  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>
862
+ <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;">
863
+ <div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
864
+ <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #00ffaa; box-shadow: 0 0 8px #00ffaa;"></span>
865
+ <strong style="color: #fff;" class="${currentRank === 'owner' ? 'owner-animated-name' : ''}">${this.escapeHtml(u.username)}</strong>
813
866
  <span class="rank-pill rank-${currentRank}">${currentRank.toUpperCase()}</span>
814
- <span style="color: #88ccff; margin-left: 0.5rem;">Playtime: ${timeStr}</span>
867
+ ${verBadge}
868
+ <span style="color: #88ccff; font-size: 0.75rem;">Playtime: ${timeStr}</span>
815
869
  </div>
816
870
  <div style="display: flex; gap: 0.35rem;">
817
871
  ${!isSelf ? `
818
872
  ${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>
873
+ <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
874
  ` : 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>
875
+ <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
876
  ` : ''}
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>'}
877
+ <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>
878
+ <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>
879
+ ` : '<span style="color: #ffd700; font-weight: 800; font-size: 0.75rem;">YOU</span>'}
880
+ </div>
881
+ </div>
882
+ `;
883
+ }).join('');
884
+ }
885
+
886
+ renderModUsersDirectory() {
887
+ const container = document.getElementById('mod-users-directory');
888
+ if (!container) return;
889
+
890
+ const userMap = new Map();
891
+ if (this.currentUser && this.currentUser.username && this.currentUser.username !== 'User') {
892
+ userMap.set(this.currentUser.username, {
893
+ username: this.currentUser.username,
894
+ rank: this.currentUser.rank || 'member',
895
+ version: window.REAPER_VERSION || '2.3.0'
896
+ });
897
+ }
898
+
899
+ if (this.onlineUsers && this.onlineUsers.length > 0) {
900
+ this.onlineUsers.forEach(u => {
901
+ if (u && u.username && u.username !== 'User' && u.username !== 'Member' && u.username !== 'Anon') {
902
+ userMap.set(u.username, u);
903
+ }
904
+ });
905
+ }
906
+
907
+ const realUsers = Array.from(userMap.values());
908
+
909
+ if (realUsers.length <= 1) {
910
+ container.innerHTML = `
911
+ <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;">
912
+ 🟢 Only you are currently online. (0 fake users)
913
+ </div>
914
+ `;
915
+ return;
916
+ }
917
+
918
+ container.innerHTML = realUsers.map(u => {
919
+ const isSelf = u.username === this.currentUser.username;
920
+ const currentRank = (u.rank || 'member').toLowerCase();
921
+ const userVer = u.version || '2.3.0';
922
+ const isLatest = userVer === window.REAPER_VERSION;
923
+
924
+ const verBadge = isLatest
925
+ ? `<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>`
926
+ : `<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>`;
927
+
928
+ return `
929
+ <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;">
930
+ <div style="display: flex; align-items: center; gap: 0.45rem;">
931
+ <span style="display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #00ffaa;"></span>
932
+ <strong style="color: #fff;">${this.escapeHtml(u.username)}</strong>
933
+ <span class="rank-pill rank-${currentRank}">${currentRank.toUpperCase()}</span>
934
+ ${verBadge}
935
+ </div>
936
+ <div>
937
+ ${!isSelf ? `
938
+ <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>
939
+ ` : '<span style="color: #00ffaa; font-size: 0.7rem;">YOU</span>'}
826
940
  </div>
827
941
  </div>
828
942
  `;
829
943
  }).join('');
830
944
  }
831
945
 
946
+ // Cross-Site Old Version Interceptor & Forwarder
947
+ handleOldVersionDetected(info) {
948
+ if (!info || !info.version || info.version === window.REAPER_VERSION) return;
949
+ if (info.forceUpgrade === false) return;
950
+
951
+ const currentVer = window.REAPER_VERSION;
952
+ const latestVer = info.version;
953
+ const latestUrl = info.url || window.REAPER_LATEST_URL;
954
+
955
+ let modal = document.getElementById('version-upgrade-modal');
956
+ if (!modal) {
957
+ modal = document.createElement('div');
958
+ modal.id = 'version-upgrade-modal';
959
+ modal.style.cssText = 'position: fixed; inset: 0; background: rgba(2, 5, 10, 0.97); 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(12px);';
960
+ document.body.appendChild(modal);
961
+ }
962
+
963
+ modal.innerHTML = `
964
+ <div style="max-width: 500px; background: #040e1c; border: 2px solid #00d2ff; border-radius: 12px; padding: 2.5rem 2rem; box-shadow: 0 0 50px rgba(0, 210, 255, 0.4);">
965
+ <div style="font-size: 3rem; margin-bottom: 0.5rem;">⚡</div>
966
+ <div style="font-size: 1.4rem; font-weight: 900; color: #00d2ff; letter-spacing: 0.08em; margin-bottom: 0.5rem;">NEW VERSION ACTIVE</div>
967
+ <p style="color: #adc4e2; font-size: 0.9rem; line-height: 1.6; margin-bottom: 1.25rem;">
968
+ You are currently visiting an older version of Reaper Hub (<strong style="color: #ff5555;">v${currentVer}</strong>).<br>
969
+ The new upgraded site is live at <strong style="color: #00ffaa;">v${latestVer}</strong> with zero lag and fixes.
970
+ </p>
971
+ <div style="color: #00ffaa; font-size: 0.85rem; font-weight: 800; margin-bottom: 1.5rem; font-family: var(--font-mono);">
972
+ 🚀 BRINGING YOU TO THE NEW SITE IN 2 SECONDS...
973
+ </div>
974
+ <button class="btn-primary" style="width: 100%; justify-content: center; padding: 0.85rem; font-size: 0.9rem; background: #00d2ff; color: #000; font-weight: 900;" onclick="window.location.replace('${latestUrl}')">
975
+ OPEN NEW SITE NOW ➔
976
+ </button>
977
+ </div>
978
+ `;
979
+
980
+ setTimeout(() => {
981
+ window.location.replace(latestUrl);
982
+ }, 2000);
983
+ }
984
+
985
+ // Root Owner Version Control Broadcast
986
+ async publishLatestVersionToFirebase() {
987
+ const verInput = document.getElementById('admin-latest-version-input');
988
+ const urlInput = document.getElementById('admin-latest-url-input');
989
+ const version = verInput && verInput.value.trim() ? verInput.value.trim() : window.REAPER_VERSION;
990
+ const url = urlInput && urlInput.value.trim() ? urlInput.value.trim() : window.REAPER_LATEST_URL;
991
+
992
+ if (window.reaperFirebase) {
993
+ await window.reaperFirebase.setLatestVersionInfo({
994
+ version: version,
995
+ url: url,
996
+ forceUpgrade: true,
997
+ updatedAt: Date.now()
998
+ });
999
+ // Also broadcast an emergency alert to all legacy versions (like 1.0.9) that listen to reaperGlobalAlert
1000
+ await window.reaperFirebase.setGlobalAlert(`NOTICE: Please update to the latest Reaper site: ${url}`);
1001
+ }
1002
+
1003
+ this.showToast(`Version ${version} broadcasted! All older sites will forward here.`);
1004
+ }
1005
+
832
1006
  async sendGlobalAlert() {
833
1007
  if (this.currentUser.rank !== 'owner') return;
834
1008
  const input = document.getElementById('admin-broadcast-msg');
@@ -1167,24 +1341,18 @@ class ReaperHubApp {
1167
1341
  window.reaperFirebase.db.ref("reaperPlaytime").on("value", (snap) => {
1168
1342
  const val = snap.val();
1169
1343
  if (val) {
1170
- this.leaderboardData = Object.values(val);
1344
+ this.leaderboardData = Object.values(val).filter(u => {
1345
+ return u && u.username && u.username !== 'User' && u.username !== 'Member' && u.username !== 'Anon' && (u.seconds || 0) > 0;
1346
+ });
1171
1347
  this.leaderboardData.sort((a, b) => (b.seconds || 0) - (a.seconds || 0));
1172
1348
  } else {
1173
- this.leaderboardData = [
1174
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
1175
- ];
1349
+ this.leaderboardData = [];
1176
1350
  }
1177
1351
  this.renderLeaderboardUI();
1178
- this.renderAdminUsersDirectory();
1179
1352
  });
1180
1353
  } catch (e) {
1181
1354
  this.renderLeaderboardUI();
1182
1355
  }
1183
- } else {
1184
- this.leaderboardData = [
1185
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
1186
- ];
1187
- this.renderLeaderboardUI();
1188
1356
  }
1189
1357
  }
1190
1358
 
@@ -1200,13 +1368,21 @@ class ReaperHubApp {
1200
1368
  const container = document.getElementById('leaderboard-rows-container');
1201
1369
  if (!container) return;
1202
1370
 
1203
- if (this.leaderboardData.length === 0) {
1204
- this.leaderboardData = [
1205
- { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
1206
- ];
1371
+ // Filter strictly to real players with recorded playtime
1372
+ const realPlayers = (this.leaderboardData || []).filter(p => {
1373
+ return p && p.username && p.username !== 'User' && p.username !== 'Member' && p.username !== 'Anon' && (p.seconds || 0) > 0;
1374
+ });
1375
+
1376
+ if (realPlayers.length === 0) {
1377
+ container.innerHTML = `
1378
+ <div style="text-align: center; padding: 2rem; color: #88aacc; font-family: var(--font-mono); font-size: 0.85rem;">
1379
+ Play any game to register your playtime on the global leaderboard!
1380
+ </div>
1381
+ `;
1382
+ return;
1207
1383
  }
1208
1384
 
1209
- container.innerHTML = this.leaderboardData.slice(0, 20).map((player, idx) => {
1385
+ container.innerHTML = realPlayers.slice(0, 20).map((player, idx) => {
1210
1386
  const rankNum = idx + 1;
1211
1387
  const timeStr = this.formatPlaytime(player.seconds || 0);
1212
1388
  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();
package/package.json CHANGED
@@ -1,24 +1,14 @@
1
1
  {
2
2
  "name": "reaper-arcade-hub",
3
- "version": "2.2.0",
4
- "description": "Reaper Hub v2.2.0 - Turbo Anti-Lag Engine (60 FPS Memory Pre-cache), Expanded Admin Superpowers, 788 High-Speed Games, Realtime Chat & Hardened Key Gatekeeper",
3
+ "version": "2.3.0",
4
+ "description": "Private Core Hub",
5
5
  "main": "main/index.html",
6
6
  "unpkg": "main/index.html",
7
7
  "jsdelivr": "main/index.html",
8
8
  "scripts": {
9
9
  "test": "node js/security-shield.js"
10
10
  },
11
- "keywords": [
12
- "reaper",
13
- "games",
14
- "html5",
15
- "unblocked",
16
- "arcade",
17
- "turbo",
18
- "antilag",
19
- "leaderboard",
20
- "keysystem"
21
- ],
11
+ "keywords": [],
22
12
  "author": "asfsaf",
23
13
  "license": "MIT",
24
14
  "publishConfig": {
@@ -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();