Ramazanpro995 commited on
Commit
c8456bc
·
verified ·
1 Parent(s): 1aabc72

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +148 -53
  2. core.py +55 -98
  3. style.css +96 -0
app.py CHANGED
@@ -71,15 +71,57 @@ HEAD = """
71
  clearTimeout(window.__cvToast);window.__cvToast=setTimeout(()=>node.classList.remove('show'),2100);
72
  }
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  document.addEventListener('click', async (event)=>{
75
  const translate=event.target.closest('[data-cloudvault-translate]');
76
  if(translate){
77
  event.preventDefault();
78
- const current=window.location.href;
79
- const target='https://translate.google.com/translate?sl=auto&tl=en&u='+encodeURIComponent(current);
80
- window.open(target,'_blank','noopener,noreferrer');
81
  return;
82
  }
 
 
 
83
  const copy=event.target.closest('[data-copy-token]');
84
  if(copy){
85
  const link=location.origin+DOWNLOAD_BASE+encodeURIComponent(copy.dataset.copyToken);
@@ -105,6 +147,47 @@ memory_lock = threading.RLock()
105
  memory_auto_paused = False
106
  last_memory_gb = 0.0
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  def _memory_used_gb() -> float:
110
  candidates = [
@@ -341,25 +424,13 @@ def chat_room_controls(user_id: str | None):
341
  user = STORE.get_user(user_id)
342
  if not user or not PLANS[user.get("plan", "free")].chat_access:
343
  return gr.update(choices=[], value=None, visible=False, interactive=False), ""
344
- state = STORE.chat_room_state(user["id"])
345
- rooms = state.get("rooms", [])
346
- room_map = {room["id"]: room for room in rooms}
347
- current = state.get("current")
348
- current_room = room_map.get(current, {})
349
- multi_visible = state.get("mode") == "multi" and len(rooms) > 1
350
- choices = [
351
- (f"{'⏸️' if room.get('paused') else '🌐'} {room.get('name')} · {room.get('members', 0)}/{state.get('capacity', 0)} users", room["id"])
352
- for room in rooms
353
- ]
354
- can_choose = bool(state.get("can_choose"))
355
- assignment_text = (
356
- "You can choose any available room." if can_choose
357
- else "Plus accounts are assigned automatically to an available room."
358
  )
359
- if state.get("mode") == "single":
360
- assignment_text = "Single-room mode is active. Everyone speaks in Global 1."
361
- info = f'''<div class="room-status cv-card"><div><span>🌐 CURRENT CHAT ROOM</span><h3>{html.escape(str(current_room.get('name', 'Global 1')))}</h3><p>{html.escape(assignment_text)}</p></div><div class="room-counter"><b>{int(current_room.get('messages', 0))}/200</b><small>messages kept</small></div></div>'''
362
- return gr.update(choices=choices, value=current, visible=multi_visible, interactive=can_choose), info
363
 
364
 
365
  def chat_html(user_id: str | None, room_id: str | None = None) -> tuple[str, str]:
@@ -379,10 +450,10 @@ def chat_html(user_id: str | None, room_id: str | None = None) -> tuple[str, str
379
  return '<div class="chat-paused cv-card"><span>⏸️</span><h3>Global Chat is paused for Plus accounts</h3></div>', "Plus Chat paused"
380
  if user.get("role") != "admin" and user.get("plan") == "pro" and not cfg.get("pro_chat_enabled", True):
381
  return '<div class="chat-paused cv-card"><span>⏸️</span><h3>Global Chat is paused for Pro accounts</h3></div>', "Pro Chat paused"
 
382
  state = STORE.chat_room_state(user["id"])
383
  valid_rooms = {room["id"]: room for room in state.get("rooms", [])}
384
- selected = room_id if room_id in valid_rooms and state.get("can_choose") else state.get("current")
385
- room = valid_rooms.get(selected, {"name": "Global 1", "paused": False})
386
  if room.get("paused") and user.get("role") != "admin":
387
  return f'<div class="chat-paused cv-card"><span>⏸️</span><h3>{html.escape(str(room.get("name", "Room")))} is paused</h3><p>The developer paused writing in this room.</p></div>', "Room paused by developer."
388
  messages = STORE.chat_messages(user["id"], selected)
@@ -399,8 +470,8 @@ def chat_html(user_id: str | None, room_id: str | None = None) -> tuple[str, str
399
  clock = created.astimezone(timezone.utc).strftime("%H:%M") if created else ""
400
  blocks.append(f'<div class="chat-line {"mine" if mine else "theirs"}"><div class="chat-bubble {tier}"><div class="chat-name">{sender}<span>{clock}</span></div><p>{text}</p></div></div>')
401
  body = "".join(blocks)
402
- room_name = html.escape(str(room.get("name", "Global 1")))
403
- status = f"{room_name} · {len(messages)}/200 messages · {memory_gb:.2f} GB memory"
404
  return f'<div class="chat-shell"><div class="chat-top"><div><span class="online-dot"></span><b>{room_name}</b><small>{"Up to 400 messages are visible for Pro" if plan.key in {"pro", "admin"} else "Up to 200 messages are visible for Plus"}</small></div><em>3s cooldown · 1000 characters</em></div><div class="chat-stream">{body}</div></div>', status
405
 
406
 
@@ -415,7 +486,7 @@ def send_chat_message(text: str, user_id: str | None, room_id: str | None):
415
  messages, status = chat_html(user_id, room_id)
416
  return "❌ Global Chat is currently paused.", text, messages, status, room_update, room_info
417
  try:
418
- STORE.send_chat(user_id, text, room_id)
419
  room_update, room_info = chat_room_controls(user_id)
420
  messages, status = chat_html(user_id, room_id)
421
  return "✅ Message accepted. It will appear in 2 seconds.", "", messages, status, room_update, room_info
@@ -442,26 +513,34 @@ def change_chat_room_ui(room_id: str | None, user_id: str | None):
442
 
443
  def developer_room_data(admin_id: str | None):
444
  if not admin_id or not STORE.is_admin(admin_id):
445
- return gr.update(value="single"), gr.update(value=200), gr.update(choices=[], value=None), ""
446
  state = STORE.chat_room_state(admin_id)
447
- choices = []
448
- cards = []
449
- for room in state.get("rooms", []):
450
- label = f"{room.get('name')} · {room.get('members', 0)}/{state.get('capacity', 0)} users · {room.get('messages', 0)}/200 messages"
451
- choices.append((label, room["id"]))
452
- cards.append(f'''<article class="room-admin-card {'paused' if room.get('paused') else ''}"><span>{'⏸️' if room.get('paused') else '🌐'}</span><div><h4>{html.escape(str(room.get('name')))}</h4><p>{int(room.get('members',0))}/{int(state.get('capacity',0))} assigned users · {int(room.get('messages',0))}/200 messages</p></div><b>{'PAUSED' if room.get('paused') else 'LIVE'}</b></article>''')
453
- overview = '<div class="room-admin-grid">'+''.join(cards)+'</div>' if cards else '<div class="empty-state cv-card"><h3>No chat rooms</h3></div>'
454
- values = [value for _, value in choices]
455
- selected = state.get("current") if state.get("current") in values else (values[0] if values else None)
456
- return gr.update(value=state.get("mode", "single")), gr.update(value=state.get("capacity", 200)), gr.update(choices=choices, value=selected), overview
 
 
 
 
 
 
 
 
457
 
458
 
459
  def apply_chat_room_configuration(mode: str, capacity: float, admin_id: str | None):
460
  try:
461
  if not admin_id:
462
  raise PermissionError("Developer access required.")
463
- STORE.set_chat_configuration(admin_id, mode, int(capacity))
464
- status = "✅ Global Chat room configuration updated."
465
  except Exception as exc:
466
  status = f"❌ {exc}"
467
  return status, *developer_room_data(admin_id), *chat_room_controls(admin_id)
@@ -681,7 +760,19 @@ with gr.Blocks(title="CloudVault", fill_width=True) as demo:
681
  with gr.Row(elem_classes="navbar"):
682
  gr.HTML('<div class="brand"><span>☁️</span><div><b>CloudVault</b><small>Secure file command center</small></div><em>LIVE</em></div>')
683
  gr.HTML('<div class="nav-space"></div>')
684
- gr.HTML('<a class="cloudvault-translate-btn" href="https://translate.google.com/" target="_blank" rel="noopener noreferrer" data-cloudvault-translate title="Open this page in Google Translate and choose any supported language"><span>🌐</span><b>Translate</b><small>All Google languages</small></a>')
 
 
 
 
 
 
 
 
 
 
 
 
685
  home_btn = gr.Button("🏠 Home", variant="primary", elem_classes="nav-btn")
686
  dashboard_top_btn = gr.Button("⚡ Dashboard", elem_classes="nav-btn")
687
  login_top_btn = gr.Button("🔐 Login", elem_classes="nav-btn")
@@ -744,9 +835,9 @@ with gr.Blocks(title="CloudVault", fill_width=True) as demo:
744
  with gr.Column(scale=2, min_width=245, elem_classes="sidebar"):
745
  sidebar_profile = gr.HTML()
746
  storage_nav = gr.Button("☁️ Storage\nFiles and uploads", variant="primary", elem_classes="side-btn")
747
- analytics_nav = gr.Button("📊 Analytics\nCountries and devices", elem_classes="side-btn")
748
- settings_nav = gr.Button("⚙️ Settings\nPackage and account", elem_classes="side-btn")
749
- chat_nav = gr.Button("💬 Global Chat\nPlus and Pro only", visible=False, elem_classes="side-btn")
750
  developer_nav = gr.Button("👑 Developer Analytics\nAdmin controls", visible=False, elem_classes="side-btn developer")
751
  change_plan_btn = gr.Button("💳 Change Package", elem_classes="side-btn")
752
  logout_btn = gr.Button("🚪 Log Out", elem_classes="side-btn danger")
@@ -800,6 +891,7 @@ with gr.Blocks(title="CloudVault", fill_width=True) as demo:
800
  developer_panel = gr.Column(visible=False)
801
  with developer_panel:
802
  developer_header = gr.HTML()
 
803
  developer_system_status = gr.HTML()
804
  with gr.Column(elem_classes="admin-card cv-card"):
805
  gr.HTML('<div class="panel-heading"><div><span>👥 ACCOUNT CONTROL</span><h3>Ban, unban or delete an account</h3><p>Usernames and package details are visible. Passwords are never shown.</p></div></div>')
@@ -823,17 +915,16 @@ with gr.Blocks(title="CloudVault", fill_width=True) as demo:
823
  clear_chat_btn = gr.Button("🧹 Clear Chat")
824
  cancel_shutdown_btn = gr.Button("♻️ Cancel Shutdown")
825
  with gr.Column(elem_classes="admin-card room-control-card cv-card"):
826
- gr.HTML('<div class="panel-heading"><div><span>🌐 GLOBAL CHAT ROOMS</span><h3>Single room or automatic multi-room system</h3><p>Each room stores up to 400 messages. Plus sees the latest 200; Pro sees the latest 400 and can choose rooms.</p></div></div>')
827
- with gr.Row():
828
- admin_chat_mode = gr.Radio(label="Chat mode", choices=[("Single room", "single"), ("Multiple rooms", "multi")], value="single")
829
- admin_room_capacity = gr.Number(label="Maximum users per room", value=200, minimum=1, maximum=100000, precision=0)
830
- admin_apply_chat_rooms = gr.Button("💾 Apply Room Configuration", variant="primary")
831
- admin_room_select = gr.Dropdown(label="Select a room", choices=[])
832
- with gr.Row():
833
- admin_toggle_room_btn = gr.Button("⏯️ Start / Pause Selected Room")
834
- admin_clear_room_btn = gr.Button("🧹 Clear Selected Room", variant="stop")
835
  admin_rooms_html = gr.HTML()
 
 
 
836
  admin_room_action_status = gr.Markdown()
 
 
 
 
837
  with gr.Column(elem_classes="admin-card destructive cv-card"):
838
  gr.HTML('<div class="panel-heading"><div><span>🧨 DESTRUCTIVE ACTIONS</span><h3>Mass account and shutdown controls</h3><p>Use the exact confirmation phrase before pressing a button.</p></div></div>')
839
  mass_confirm = gr.Textbox(label="Confirmation", placeholder="DELETE FREE, DELETE ALL or SHUTDOWN")
@@ -918,7 +1009,11 @@ with gr.Blocks(title="CloudVault", fill_width=True) as demo:
918
  chat_timer = gr.Timer(2.0, active=True)
919
  chat_timer.tick(chat_html, inputs=[session_user, chat_room_select], outputs=[chat_messages_html, chat_status_html], queue=False)
920
 
921
- logout_btn.click(logout_user, outputs=[session_user, login_status, *navigation_outputs], queue=False)
 
 
 
 
922
  delete_account_btn.click(delete_account, inputs=[delete_confirm, session_user], outputs=[session_user, account_status_md, *navigation_outputs], queue=False)
923
 
924
  def refresh_developer(admin_id):
 
71
  clearTimeout(window.__cvToast);window.__cvToast=setTimeout(()=>node.classList.remove('show'),2100);
72
  }
73
 
74
+ function syncTranslateLanguages(){
75
+ const nativeSelect=document.querySelector('#google_translate_element select.goog-te-combo');
76
+ const customSelect=document.getElementById('cv-language-select');
77
+ if(!nativeSelect || !customSelect) return false;
78
+ if(customSelect.options.length<=1){
79
+ [...nativeSelect.options].forEach((option,index)=>{
80
+ if(index===0 || !option.value) return;
81
+ const clone=document.createElement('option');
82
+ clone.value=option.value;
83
+ clone.textContent=option.textContent;
84
+ customSelect.appendChild(clone);
85
+ });
86
+ }
87
+ return customSelect.options.length>1;
88
+ }
89
+
90
+ window.cloudVaultGoogleTranslateInit=function(){
91
+ new google.translate.TranslateElement({
92
+ pageLanguage:'en',
93
+ autoDisplay:false,
94
+ includedLanguages:''
95
+ },'google_translate_element');
96
+ let attempts=0;
97
+ const timer=setInterval(()=>{
98
+ attempts++;
99
+ if(syncTranslateLanguages() || attempts>40) clearInterval(timer);
100
+ },250);
101
+ };
102
+
103
+ document.addEventListener('change',(event)=>{
104
+ const customSelect=event.target.closest('#cv-language-select');
105
+ if(!customSelect || !customSelect.value) return;
106
+ const nativeSelect=document.querySelector('#google_translate_element select.goog-te-combo');
107
+ if(nativeSelect){
108
+ nativeSelect.value=customSelect.value;
109
+ nativeSelect.dispatchEvent(new Event('change',{bubbles:true}));
110
+ document.getElementById('cv-translate-panel')?.classList.remove('open');
111
+ }
112
+ });
113
+
114
  document.addEventListener('click', async (event)=>{
115
  const translate=event.target.closest('[data-cloudvault-translate]');
116
  if(translate){
117
  event.preventDefault();
118
+ syncTranslateLanguages();
119
+ document.getElementById('cv-translate-panel')?.classList.toggle('open');
 
120
  return;
121
  }
122
+ if(!event.target.closest('.cloudvault-translate-wrap')){
123
+ document.getElementById('cv-translate-panel')?.classList.remove('open');
124
+ }
125
  const copy=event.target.closest('[data-copy-token]');
126
  if(copy){
127
  const link=location.origin+DOWNLOAD_BASE+encodeURIComponent(copy.dataset.copyToken);
 
147
  memory_auto_paused = False
148
  last_memory_gb = 0.0
149
 
150
+ active_users_lock = threading.RLock()
151
+ active_users_seen: dict[str, float] = {}
152
+
153
+
154
+ def active_users_heartbeat(user_id: str | None):
155
+ """Refresh the live-user count every 3 seconds.
156
+
157
+ A user is treated as active while their browser keeps sending heartbeats.
158
+ Entries older than 7 seconds are removed to tolerate normal timer jitter.
159
+ """
160
+ now = time.monotonic()
161
+ with active_users_lock:
162
+ if user_id and STORE.get_user(user_id):
163
+ active_users_seen[user_id] = now
164
+ stale = [uid for uid, seen_at in active_users_seen.items() if now - seen_at > 7.0]
165
+ for uid in stale:
166
+ active_users_seen.pop(uid, None)
167
+ count = len(active_users_seen)
168
+
169
+ is_admin = bool(user_id and STORE.is_admin(user_id))
170
+ if not is_admin:
171
+ return gr.update(value="", visible=False)
172
+
173
+ return gr.update(
174
+ value=(
175
+ '<section class="live-users-card cv-card">'
176
+ '<div class="live-users-pulse"><i></i><span>LIVE USERS</span></div>'
177
+ f'<strong>{count}</strong>'
178
+ f'<p>{count} active user{"s" if count != 1 else ""} · refreshed every 3 seconds</p>'
179
+ '</section>'
180
+ ),
181
+ visible=True,
182
+ )
183
+
184
+
185
+ def remove_active_user(user_id: str | None):
186
+ with active_users_lock:
187
+ if user_id:
188
+ active_users_seen.pop(user_id, None)
189
+
190
+
191
 
192
  def _memory_used_gb() -> float:
193
  candidates = [
 
424
  user = STORE.get_user(user_id)
425
  if not user or not PLANS[user.get("plan", "free")].chat_access:
426
  return gr.update(choices=[], value=None, visible=False, interactive=False), ""
427
+ info = (
428
+ '<div class="room-status cv-card single-room">'
429
+ '<div><span>🌐 GLOBAL CHAT</span><h3>One shared room</h3>'
430
+ '<p>All Plus, Pro and developer accounts chat together in the same room.</p></div>'
431
+ '<div class="room-counter"><b>1</b><small>room</small></div></div>'
 
 
 
 
 
 
 
 
 
432
  )
433
+ return gr.update(choices=[("Global Chat", "global-1")], value="global-1", visible=False, interactive=False), info
 
 
 
434
 
435
 
436
  def chat_html(user_id: str | None, room_id: str | None = None) -> tuple[str, str]:
 
450
  return '<div class="chat-paused cv-card"><span>⏸️</span><h3>Global Chat is paused for Plus accounts</h3></div>', "Plus Chat paused"
451
  if user.get("role") != "admin" and user.get("plan") == "pro" and not cfg.get("pro_chat_enabled", True):
452
  return '<div class="chat-paused cv-card"><span>⏸️</span><h3>Global Chat is paused for Pro accounts</h3></div>', "Pro Chat paused"
453
+ selected = "global-1"
454
  state = STORE.chat_room_state(user["id"])
455
  valid_rooms = {room["id"]: room for room in state.get("rooms", [])}
456
+ room = valid_rooms.get("global-1", {"name": "Global Chat", "paused": False})
 
457
  if room.get("paused") and user.get("role") != "admin":
458
  return f'<div class="chat-paused cv-card"><span>⏸️</span><h3>{html.escape(str(room.get("name", "Room")))} is paused</h3><p>The developer paused writing in this room.</p></div>', "Room paused by developer."
459
  messages = STORE.chat_messages(user["id"], selected)
 
470
  clock = created.astimezone(timezone.utc).strftime("%H:%M") if created else ""
471
  blocks.append(f'<div class="chat-line {"mine" if mine else "theirs"}"><div class="chat-bubble {tier}"><div class="chat-name">{sender}<span>{clock}</span></div><p>{text}</p></div></div>')
472
  body = "".join(blocks)
473
+ room_name = "Global Chat"
474
+ status = f"{room_name} · {len(messages)} visible messages"
475
  return f'<div class="chat-shell"><div class="chat-top"><div><span class="online-dot"></span><b>{room_name}</b><small>{"Up to 400 messages are visible for Pro" if plan.key in {"pro", "admin"} else "Up to 200 messages are visible for Plus"}</small></div><em>3s cooldown · 1000 characters</em></div><div class="chat-stream">{body}</div></div>', status
476
 
477
 
 
486
  messages, status = chat_html(user_id, room_id)
487
  return "❌ Global Chat is currently paused.", text, messages, status, room_update, room_info
488
  try:
489
+ STORE.send_chat(user_id, text, "global-1")
490
  room_update, room_info = chat_room_controls(user_id)
491
  messages, status = chat_html(user_id, room_id)
492
  return "✅ Message accepted. It will appear in 2 seconds.", "", messages, status, room_update, room_info
 
513
 
514
  def developer_room_data(admin_id: str | None):
515
  if not admin_id or not STORE.is_admin(admin_id):
516
+ return gr.update(value="single"), gr.update(value=0), gr.update(choices=[], value=None), ""
517
  state = STORE.chat_room_state(admin_id)
518
+ room = next(iter(state.get("rooms", [])), {"id": "global-1", "messages": 0, "paused": False})
519
+ overview = (
520
+ '<div class="room-admin-grid"><article class="room-admin-card '
521
+ + ('paused' if room.get("paused") else '')
522
+ + '"><span>'
523
+ + ('⏸️' if room.get("paused") else '🌐')
524
+ + '</span><div><h4>Global Chat</h4><p>One shared room · '
525
+ + str(int(room.get("messages", 0)))
526
+ + ' stored messages</p></div><b>'
527
+ + ('PAUSED' if room.get("paused") else 'LIVE')
528
+ + '</b></article></div>'
529
+ )
530
+ return (
531
+ gr.update(value="single"),
532
+ gr.update(value=0),
533
+ gr.update(choices=[("Global Chat", "global-1")], value="global-1"),
534
+ overview,
535
+ )
536
 
537
 
538
  def apply_chat_room_configuration(mode: str, capacity: float, admin_id: str | None):
539
  try:
540
  if not admin_id:
541
  raise PermissionError("Developer access required.")
542
+ STORE.set_chat_configuration(admin_id, "single", 0)
543
+ status = "✅ Global Chat uses one shared room."
544
  except Exception as exc:
545
  status = f"❌ {exc}"
546
  return status, *developer_room_data(admin_id), *chat_room_controls(admin_id)
 
760
  with gr.Row(elem_classes="navbar"):
761
  gr.HTML('<div class="brand"><span>☁️</span><div><b>CloudVault</b><small>Secure file command center</small></div><em>LIVE</em></div>')
762
  gr.HTML('<div class="nav-space"></div>')
763
+ gr.HTML('''<div class="cloudvault-translate-wrap">
764
+ <button type="button" class="cloudvault-translate-btn" data-cloudvault-translate aria-expanded="false">
765
+ <span>🌐</span><b>Translate</b><small>Choose language</small>
766
+ </button>
767
+ <div id="cv-translate-panel" class="cv-translate-panel">
768
+ <div class="cv-translate-title"><span>🌍</span><div><b>Select language</b><small>Google Translate supported languages</small></div></div>
769
+ <select id="cv-language-select" aria-label="Choose translation language">
770
+ <option value="">Choose a language…</option>
771
+ </select>
772
+ <div id="google_translate_element" class="cv-native-translate"></div>
773
+ </div>
774
+ </div>
775
+ <script src="https://translate.google.com/translate_a/element.js?cb=cloudVaultGoogleTranslateInit"></script>''')
776
  home_btn = gr.Button("🏠 Home", variant="primary", elem_classes="nav-btn")
777
  dashboard_top_btn = gr.Button("⚡ Dashboard", elem_classes="nav-btn")
778
  login_top_btn = gr.Button("🔐 Login", elem_classes="nav-btn")
 
835
  with gr.Column(scale=2, min_width=245, elem_classes="sidebar"):
836
  sidebar_profile = gr.HTML()
837
  storage_nav = gr.Button("☁️ Storage\nFiles and uploads", variant="primary", elem_classes="side-btn")
838
+ analytics_nav = gr.Button("📊 Analytics", elem_classes="side-btn")
839
+ settings_nav = gr.Button("⚙️ Settings", elem_classes="side-btn")
840
+ chat_nav = gr.Button("💬 Global Chat", visible=False, elem_classes="side-btn")
841
  developer_nav = gr.Button("👑 Developer Analytics\nAdmin controls", visible=False, elem_classes="side-btn developer")
842
  change_plan_btn = gr.Button("💳 Change Package", elem_classes="side-btn")
843
  logout_btn = gr.Button("🚪 Log Out", elem_classes="side-btn danger")
 
891
  developer_panel = gr.Column(visible=False)
892
  with developer_panel:
893
  developer_header = gr.HTML()
894
+ active_users_html = gr.HTML(visible=False)
895
  developer_system_status = gr.HTML()
896
  with gr.Column(elem_classes="admin-card cv-card"):
897
  gr.HTML('<div class="panel-heading"><div><span>👥 ACCOUNT CONTROL</span><h3>Ban, unban or delete an account</h3><p>Usernames and package details are visible. Passwords are never shown.</p></div></div>')
 
915
  clear_chat_btn = gr.Button("🧹 Clear Chat")
916
  cancel_shutdown_btn = gr.Button("♻️ Cancel Shutdown")
917
  with gr.Column(elem_classes="admin-card room-control-card cv-card"):
918
+ gr.HTML('<div class="panel-heading"><div><span>🌐 GLOBAL CHAT</span><h3>One shared room</h3><p>All eligible users use the same Global Chat room. There is no room capacity limit.</p></div></div>')
 
 
 
 
 
 
 
 
919
  admin_rooms_html = gr.HTML()
920
+ with gr.Row():
921
+ admin_toggle_room_btn = gr.Button("⏯️ Start / Pause Global Chat Room")
922
+ admin_clear_room_btn = gr.Button("🧹 Clear Global Chat Messages", variant="stop")
923
  admin_room_action_status = gr.Markdown()
924
+ admin_chat_mode = gr.Radio(choices=[("Single room", "single")], value="single", visible=False)
925
+ admin_room_capacity = gr.Number(value=0, visible=False)
926
+ admin_apply_chat_rooms = gr.Button("Apply", visible=False)
927
+ admin_room_select = gr.Dropdown(choices=[("Global Chat", "global-1")], value="global-1", visible=False)
928
  with gr.Column(elem_classes="admin-card destructive cv-card"):
929
  gr.HTML('<div class="panel-heading"><div><span>🧨 DESTRUCTIVE ACTIONS</span><h3>Mass account and shutdown controls</h3><p>Use the exact confirmation phrase before pressing a button.</p></div></div>')
930
  mass_confirm = gr.Textbox(label="Confirmation", placeholder="DELETE FREE, DELETE ALL or SHUTDOWN")
 
1009
  chat_timer = gr.Timer(2.0, active=True)
1010
  chat_timer.tick(chat_html, inputs=[session_user, chat_room_select], outputs=[chat_messages_html, chat_status_html], queue=False)
1011
 
1012
+ active_users_timer = gr.Timer(3.0, active=True)
1013
+ active_users_timer.tick(active_users_heartbeat, inputs=session_user, outputs=active_users_html, queue=False)
1014
+
1015
+ logout_click = logout_btn.click(remove_active_user, inputs=session_user, outputs=None, queue=False)
1016
+ logout_click.then(logout_user, outputs=[session_user, login_status, *navigation_outputs], queue=False)
1017
  delete_account_btn.click(delete_account, inputs=[delete_confirm, session_user], outputs=[session_user, account_status_md, *navigation_outputs], queue=False)
1018
 
1019
  def refresh_developer(admin_id):
core.py CHANGED
@@ -317,7 +317,7 @@ class StateStore:
317
  "delete_paid_on_expiry": False,
318
  "shutdown_mode": False,
319
  "chat_mode": "single",
320
- "chat_room_capacity": 200,
321
  }
322
  if not isinstance(self.state.get("users"), dict):
323
  self.state["users"] = {}; changed = True
@@ -334,22 +334,25 @@ class StateStore:
334
  for key, value in defaults.items():
335
  if key not in self.state["config"]:
336
  self.state["config"][key] = value; changed = True
337
- if self.state["config"].get("chat_mode") not in {"single", "multi"}:
 
338
  self.state["config"]["chat_mode"] = "single"; changed = True
339
- try:
340
- capacity = int(self.state["config"].get("chat_room_capacity", 200))
341
- except (TypeError, ValueError):
342
- capacity = 200
343
- capacity = max(1, min(capacity, 100000))
344
- if self.state["config"].get("chat_room_capacity") != capacity:
345
- self.state["config"]["chat_room_capacity"] = capacity; changed = True
346
  if "global-1" not in self.state["chat_rooms"]:
347
  self.state["chat_rooms"]["global-1"] = {
348
  "id": "global-1", "name": "Global 1", "paused": False, "created_at": iso()
349
  }
350
  changed = True
 
 
 
 
 
 
 
351
  for message in self.state["chat"]:
352
- if not message.get("room_id"):
353
  message["room_id"] = "global-1"; changed = True
354
  for user in self.state["users"].values():
355
  if "role" not in user:
@@ -360,22 +363,15 @@ class StateStore:
360
  user["ban_until"] = None; changed = True
361
  if "delete_on_expiry" not in user:
362
  user["delete_on_expiry"] = False; changed = True
363
- if "chat_room_id" not in user:
364
  user["chat_room_id"] = "global-1"; changed = True
365
- # Keep only the newest 400 messages per room.
366
- kept: list[dict[str, Any]] = []
367
- for room_id in list(self.state["chat_rooms"]):
368
- room_messages = [m for m in self.state["chat"] if m.get("room_id", "global-1") == room_id]
369
- kept.extend(room_messages[-200:])
370
- orphaned = [m for m in self.state["chat"] if m.get("room_id", "global-1") not in self.state["chat_rooms"]]
371
- if orphaned:
372
- for message in orphaned:
373
- message["room_id"] = "global-1"
374
- kept.extend(orphaned[-200:])
375
- changed = True
376
- if len(kept) != len(self.state["chat"]):
377
  changed = True
378
- self.state["chat"] = sorted(kept, key=lambda m: m.get("created_at", ""))
379
  self.state["version"] = 4
380
  return changed
381
 
@@ -990,27 +986,24 @@ class StateStore:
990
  user = self.state["users"].get(user_id)
991
  if not self._chat_eligible_unlocked(user):
992
  return {"mode": "single", "capacity": 0, "current": None, "can_choose": False, "rooms": []}
993
- current_before = user.get("chat_room_id")
994
- current = self._assign_room_unlocked(user_id)
995
- if current_before != current:
996
- self._save_unlocked("Assign CloudVault chat room")
997
- rooms = []
998
- for room_id, room in sorted(self.state["chat_rooms"].items(), key=lambda pair: self._room_sort_key_unlocked(pair[0])):
999
- rooms.append({
1000
- "id": room_id,
1001
- "name": room.get("name", room_id),
1002
- "paused": bool(room.get("paused", False)),
1003
- "members": self._room_member_count_unlocked(room_id),
1004
- "messages": sum(1 for m in self.state["chat"] if m.get("room_id", "global-1") == room_id),
1005
- })
1006
- mode = self.state["config"].get("chat_mode", "single")
1007
- can_choose = bool(mode == "multi" and len(rooms) > 1 and (user.get("role") == "admin" or user.get("plan") == "pro"))
1008
  return {
1009
- "mode": mode,
1010
- "capacity": int(self.state["config"].get("chat_room_capacity", 200)),
1011
- "current": current,
1012
- "can_choose": can_choose,
1013
- "rooms": rooms,
 
 
 
 
 
 
1014
  }
1015
 
1016
  def change_chat_room(self, user_id: str, room_id: str) -> str:
@@ -1037,44 +1030,23 @@ class StateStore:
1037
  return room_id
1038
 
1039
  def set_chat_configuration(self, admin_id: str, mode: str, capacity: int) -> dict[str, Any]:
1040
- mode = (mode or "single").strip().lower()
1041
- if mode not in {"single", "multi"}:
1042
- raise ValueError("Chat mode must be single or multi.")
1043
- try:
1044
- capacity = int(capacity)
1045
- except (TypeError, ValueError):
1046
- raise ValueError("Room capacity must be a whole number.")
1047
- capacity = max(1, min(capacity, 100000))
1048
  with self.lock:
1049
  self._assert_admin_unlocked(admin_id)
1050
- self.state["config"]["chat_mode"] = mode
1051
- self.state["config"]["chat_room_capacity"] = capacity
1052
- if "global-1" not in self.state["chat_rooms"]:
1053
- self._ensure_room_unlocked("global-1")
1054
- eligible = [uid for uid, user in self.state["users"].items() if self._chat_eligible_unlocked(user)]
1055
- if mode == "single":
1056
- for uid in eligible:
1057
- self.state["users"][uid]["chat_room_id"] = "global-1"
1058
- all_messages = sorted(self.state["chat"], key=lambda m: m.get("created_at", ""))[-400:]
1059
- for message in all_messages:
1060
- message["room_id"] = "global-1"
1061
- self.state["chat"] = all_messages
1062
- room1 = self.state["chat_rooms"]["global-1"]
1063
- room1["paused"] = False
1064
- self.state["chat_rooms"] = {"global-1": room1}
1065
- else:
1066
- required = max(1, (len(eligible) + capacity - 1) // capacity)
1067
- while len(self.state["chat_rooms"]) < required:
1068
- self._new_room_unlocked()
1069
- active_rooms = [rid for rid, room in sorted(self.state["chat_rooms"].items(), key=lambda pair: self._room_sort_key_unlocked(pair[0])) if not room.get("paused")]
1070
- if not active_rooms:
1071
- self.state["chat_rooms"]["global-1"]["paused"] = False
1072
- active_rooms = ["global-1"]
1073
- secrets.SystemRandom().shuffle(eligible)
1074
- for index, uid in enumerate(eligible):
1075
- room_index = min(index // capacity, len(active_rooms) - 1)
1076
- self.state["users"][uid]["chat_room_id"] = active_rooms[room_index]
1077
- self._save_unlocked("Update CloudVault chat room configuration")
1078
  return dict(self.state["config"])
1079
 
1080
  def toggle_chat_room(self, admin_id: str, room_id: str) -> bool:
@@ -1122,18 +1094,8 @@ class StateStore:
1122
  raise PermissionError("Global Chat is paused for Plus accounts.")
1123
  if user.get("role") != "admin" and user.get("plan") == "pro" and not cfg.get("pro_chat_enabled", True):
1124
  raise PermissionError("Global Chat is paused for Pro accounts.")
1125
- if room_id and (user.get("role") == "admin" or user.get("plan") == "pro"):
1126
- # Inline room change to avoid a second state write.
1127
- selected = self.state["chat_rooms"].get(room_id)
1128
- if not selected:
1129
- raise ValueError("The selected chat room does not exist.")
1130
- if selected.get("paused") and user.get("role") != "admin":
1131
- raise ValueError("The selected chat room is paused.")
1132
- capacity = int(cfg.get("chat_room_capacity", 200))
1133
- if user.get("chat_room_id") != room_id and user.get("role") != "admin" and self._room_member_count_unlocked(room_id) >= capacity:
1134
- raise ValueError("The selected chat room is full.")
1135
- user["chat_room_id"] = room_id
1136
- assigned_room = self._assign_room_unlocked(user_id)
1137
  room = self.state["chat_rooms"].get(assigned_room)
1138
  if room and room.get("paused") and user.get("role") != "admin":
1139
  raise PermissionError("This chat room is paused by the developer.")
@@ -1154,10 +1116,7 @@ class StateStore:
1154
  }
1155
  user["last_chat_at"] = iso(now)
1156
  self.state["chat"].append(record)
1157
- room_messages = [m for m in self.state["chat"] if m.get("room_id", "global-1") == assigned_room]
1158
- if len(room_messages) > 400:
1159
- remove_ids = {m.get("id") for m in room_messages[:-400]}
1160
- self.state["chat"] = [m for m in self.state["chat"] if m.get("id") not in remove_ids]
1161
  self._save_unlocked("Send CloudVault Global Chat message")
1162
  return dict(record)
1163
 
@@ -1167,8 +1126,6 @@ class StateStore:
1167
  if not self._chat_eligible_unlocked(user):
1168
  return []
1169
  assigned = self._assign_room_unlocked(user_id)
1170
- if room_id and (user.get("role") == "admin" or user.get("plan") == "pro") and room_id in self.state["chat_rooms"]:
1171
- assigned = room_id
1172
  now = utcnow()
1173
  limit = 400 if user.get("role") == "admin" or user.get("plan") == "pro" else 200
1174
  return [
 
317
  "delete_paid_on_expiry": False,
318
  "shutdown_mode": False,
319
  "chat_mode": "single",
320
+ "chat_room_capacity": 0,
321
  }
322
  if not isinstance(self.state.get("users"), dict):
323
  self.state["users"] = {}; changed = True
 
334
  for key, value in defaults.items():
335
  if key not in self.state["config"]:
336
  self.state["config"][key] = value; changed = True
337
+ # CloudVault uses exactly one unlimited Global Chat room.
338
+ if self.state["config"].get("chat_mode") != "single":
339
  self.state["config"]["chat_mode"] = "single"; changed = True
340
+ if self.state["config"].get("chat_room_capacity") != 0:
341
+ self.state["config"]["chat_room_capacity"] = 0; changed = True
 
 
 
 
 
342
  if "global-1" not in self.state["chat_rooms"]:
343
  self.state["chat_rooms"]["global-1"] = {
344
  "id": "global-1", "name": "Global 1", "paused": False, "created_at": iso()
345
  }
346
  changed = True
347
+ if set(self.state["chat_rooms"]) != {"global-1"}:
348
+ room1 = self.state["chat_rooms"].get("global-1", {
349
+ "id": "global-1", "name": "Global Chat", "paused": False, "created_at": iso()
350
+ })
351
+ room1["name"] = "Global Chat"
352
+ self.state["chat_rooms"] = {"global-1": room1}
353
+ changed = True
354
  for message in self.state["chat"]:
355
+ if message.get("room_id") != "global-1":
356
  message["room_id"] = "global-1"; changed = True
357
  for user in self.state["users"].values():
358
  if "role" not in user:
 
363
  user["ban_until"] = None; changed = True
364
  if "delete_on_expiry" not in user:
365
  user["delete_on_expiry"] = False; changed = True
366
+ if user.get("chat_room_id") != "global-1":
367
  user["chat_room_id"] = "global-1"; changed = True
368
+ # Keep only the newest 400 messages in the single Global Chat room.
369
+ normalized_chat = sorted(self.state["chat"], key=lambda m: m.get("created_at", ""))[-400:]
370
+ for message in normalized_chat:
371
+ message["room_id"] = "global-1"
372
+ if len(normalized_chat) != len(self.state["chat"]):
 
 
 
 
 
 
 
373
  changed = True
374
+ self.state["chat"] = normalized_chat
375
  self.state["version"] = 4
376
  return changed
377
 
 
986
  user = self.state["users"].get(user_id)
987
  if not self._chat_eligible_unlocked(user):
988
  return {"mode": "single", "capacity": 0, "current": None, "can_choose": False, "rooms": []}
989
+ user["chat_room_id"] = "global-1"
990
+ room = self.state["chat_rooms"].setdefault(
991
+ "global-1",
992
+ {"id": "global-1", "name": "Global Chat", "paused": False, "created_at": iso()},
993
+ )
994
+ room["name"] = "Global Chat"
 
 
 
 
 
 
 
 
 
995
  return {
996
+ "mode": "single",
997
+ "capacity": 0,
998
+ "current": "global-1",
999
+ "can_choose": False,
1000
+ "rooms": [{
1001
+ "id": "global-1",
1002
+ "name": "Global Chat",
1003
+ "paused": bool(room.get("paused", False)),
1004
+ "members": self._room_member_count_unlocked("global-1"),
1005
+ "messages": len(self.state["chat"]),
1006
+ }],
1007
  }
1008
 
1009
  def change_chat_room(self, user_id: str, room_id: str) -> str:
 
1030
  return room_id
1031
 
1032
  def set_chat_configuration(self, admin_id: str, mode: str, capacity: int) -> dict[str, Any]:
 
 
 
 
 
 
 
 
1033
  with self.lock:
1034
  self._assert_admin_unlocked(admin_id)
1035
+ self.state["config"]["chat_mode"] = "single"
1036
+ self.state["config"]["chat_room_capacity"] = 0
1037
+ room = self.state["chat_rooms"].setdefault(
1038
+ "global-1",
1039
+ {"id": "global-1", "name": "Global Chat", "paused": False, "created_at": iso()},
1040
+ )
1041
+ room["name"] = "Global Chat"
1042
+ self.state["chat_rooms"] = {"global-1": room}
1043
+ for user in self.state["users"].values():
1044
+ if self._chat_eligible_unlocked(user):
1045
+ user["chat_room_id"] = "global-1"
1046
+ for message in self.state["chat"]:
1047
+ message["room_id"] = "global-1"
1048
+ self.state["chat"] = sorted(self.state["chat"], key=lambda m: m.get("created_at", ""))[-400:]
1049
+ self._save_unlocked("Use one unlimited Global Chat room")
 
 
 
 
 
 
 
 
 
 
 
 
 
1050
  return dict(self.state["config"])
1051
 
1052
  def toggle_chat_room(self, admin_id: str, room_id: str) -> bool:
 
1094
  raise PermissionError("Global Chat is paused for Plus accounts.")
1095
  if user.get("role") != "admin" and user.get("plan") == "pro" and not cfg.get("pro_chat_enabled", True):
1096
  raise PermissionError("Global Chat is paused for Pro accounts.")
1097
+ user["chat_room_id"] = "global-1"
1098
+ assigned_room = "global-1"
 
 
 
 
 
 
 
 
 
 
1099
  room = self.state["chat_rooms"].get(assigned_room)
1100
  if room and room.get("paused") and user.get("role") != "admin":
1101
  raise PermissionError("This chat room is paused by the developer.")
 
1116
  }
1117
  user["last_chat_at"] = iso(now)
1118
  self.state["chat"].append(record)
1119
+ self.state["chat"] = sorted(self.state["chat"], key=lambda m: m.get("created_at", ""))[-400:]
 
 
 
1120
  self._save_unlocked("Send CloudVault Global Chat message")
1121
  return dict(record)
1122
 
 
1126
  if not self._chat_eligible_unlocked(user):
1127
  return []
1128
  assigned = self._assign_room_unlocked(user_id)
 
 
1129
  now = utcnow()
1130
  limit = 400 if user.get("role") == "admin" or user.get("plan") == "pro" else 200
1131
  return [
style.css CHANGED
@@ -148,3 +148,99 @@ body:after{content:"";position:fixed;inset:-30%;pointer-events:none;opacity:.12;
148
  .cloudvault-translate-btn>small{align-self:start;color:#8fc8db;font-size:8px;line-height:1.1;white-space:nowrap}
149
  .goog-te-banner-frame,.skiptranslate iframe{display:none!important}body{top:0!important}
150
  @media(max-width:900px){.cloudvault-translate-btn{min-width:54px;width:54px;padding:8px;grid-template-columns:1fr}.cloudvault-translate-btn>span{grid-row:auto}.cloudvault-translate-btn>b,.cloudvault-translate-btn>small{display:none}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  .cloudvault-translate-btn>small{align-self:start;color:#8fc8db;font-size:8px;line-height:1.1;white-space:nowrap}
149
  .goog-te-banner-frame,.skiptranslate iframe{display:none!important}body{top:0!important}
150
  @media(max-width:900px){.cloudvault-translate-btn{min-width:54px;width:54px;padding:8px;grid-template-columns:1fr}.cloudvault-translate-btn>span{grid-row:auto}.cloudvault-translate-btn>b,.cloudvault-translate-btn>small{display:none}}
151
+
152
+ /* V15 — inline Google Translate language menu */
153
+ .cloudvault-translate-wrap{position:relative;min-width:0;z-index:80}
154
+ .cloudvault-translate-btn{width:166px;min-width:166px;max-width:166px;border:0;cursor:pointer;text-align:left}
155
+ .cv-translate-panel{position:absolute;top:calc(100% + 10px);left:0;width:320px;max-width:min(88vw,320px);padding:15px;border:1px solid rgba(96,165,250,.34);border-radius:18px;background:linear-gradient(145deg,#101a2e,#070b14);box-shadow:0 26px 80px rgba(0,0,0,.65);opacity:0;visibility:hidden;transform:translateY(-8px) scale(.98);transform-origin:top left;transition:.2s ease;z-index:9999}
156
+ .cv-translate-panel.open{opacity:1;visibility:visible;transform:translateY(0) scale(1)}
157
+ .cv-translate-title{display:flex;align-items:center;gap:11px;margin-bottom:12px;padding-bottom:11px;border-bottom:1px solid rgba(148,163,184,.14)}
158
+ .cv-translate-title>span{width:38px;height:38px;display:grid;place-items:center;border-radius:12px;background:linear-gradient(135deg,#0e7490,#3730a3);font-size:19px}
159
+ .cv-translate-title b,.cv-translate-title small{display:block}.cv-translate-title b{font-size:13px}.cv-translate-title small{margin-top:3px;color:#7f91aa;font-size:9px}
160
+ #cv-language-select{width:100%!important;max-height:380px!important;min-height:46px!important;padding:0 13px!important;border:1px solid #30415f!important;border-radius:13px!important;background:#08101e!important;color:#f7fbff!important;font-size:12px!important;font-weight:750!important;cursor:pointer}
161
+ #cv-language-select option{background:#08101e;color:#eef4ff;padding:8px}
162
+ .cv-native-translate{position:absolute!important;width:1px!important;height:1px!important;overflow:hidden!important;opacity:0!important;pointer-events:none!important}
163
+ .goog-te-banner-frame,.skiptranslate iframe{display:none!important}body{top:0!important}
164
+ @media(max-width:1100px){.cloudvault-translate-btn{width:54px;min-width:54px;max-width:54px;padding:8px;grid-template-columns:1fr}.cloudvault-translate-btn>b,.cloudvault-translate-btn>small{display:none}.cv-translate-panel{left:0}}
165
+ @media(max-width:700px){.cv-translate-panel{position:fixed;top:86px;left:50%;transform:translate(-50%,-8px) scale(.98);transform-origin:top center}.cv-translate-panel.open{transform:translate(-50%,0) scale(1)}}
166
+
167
+
168
+ /* V15: compact sidebar labels and live active-user card */
169
+ .side-btn button,
170
+ .side-btn {
171
+ white-space: nowrap !important;
172
+ line-height: 1.1 !important;
173
+ }
174
+
175
+ .live-users-card {
176
+ display: grid;
177
+ grid-template-columns: auto 1fr;
178
+ align-items: center;
179
+ gap: 14px 22px;
180
+ padding: 22px 24px;
181
+ margin-bottom: 16px;
182
+ overflow: hidden;
183
+ position: relative;
184
+ }
185
+
186
+ .live-users-card::after {
187
+ content: "";
188
+ position: absolute;
189
+ inset: -80% auto auto 45%;
190
+ width: 260px;
191
+ height: 260px;
192
+ border-radius: 50%;
193
+ background: radial-gradient(circle, rgba(38, 224, 154, .22), transparent 68%);
194
+ animation: cvLiveGlow 3s ease-in-out infinite;
195
+ pointer-events: none;
196
+ }
197
+
198
+ .live-users-card strong {
199
+ grid-row: 1 / span 2;
200
+ font-size: clamp(2.2rem, 5vw, 4.6rem);
201
+ color: #72f2bd;
202
+ letter-spacing: -0.06em;
203
+ text-shadow: 0 0 28px rgba(64, 240, 170, .25);
204
+ z-index: 1;
205
+ }
206
+
207
+ .live-users-card p {
208
+ margin: 0;
209
+ color: var(--cv-muted);
210
+ z-index: 1;
211
+ }
212
+
213
+ .live-users-pulse {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 9px;
217
+ color: #d9fff0;
218
+ font-size: .76rem;
219
+ letter-spacing: .16em;
220
+ font-weight: 800;
221
+ z-index: 1;
222
+ }
223
+
224
+ .live-users-pulse i {
225
+ width: 10px;
226
+ height: 10px;
227
+ border-radius: 999px;
228
+ background: #43eca7;
229
+ box-shadow: 0 0 0 0 rgba(67, 236, 167, .55);
230
+ animation: cvLivePulse 1.5s infinite;
231
+ }
232
+
233
+ .room-status.single-room {
234
+ grid-template-columns: 1fr auto;
235
+ }
236
+
237
+ @keyframes cvLivePulse {
238
+ 0% { box-shadow: 0 0 0 0 rgba(67, 236, 167, .55); }
239
+ 70% { box-shadow: 0 0 0 12px rgba(67, 236, 167, 0); }
240
+ 100% { box-shadow: 0 0 0 0 rgba(67, 236, 167, 0); }
241
+ }
242
+
243
+ @keyframes cvLiveGlow {
244
+ 0%,100% { transform: scale(.9); opacity: .5; }
245
+ 50% { transform: scale(1.12); opacity: 1; }
246
+ }