Spaces:
Running on Zero
Running on Zero
Upload 3 files
Browse files
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 |
-
|
| 79 |
-
|
| 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 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 =
|
| 403 |
-
status = f"{room_name} · {len(messages)}
|
| 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,
|
| 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=
|
| 446 |
state = STORE.chat_room_state(admin_id)
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 464 |
-
status = "✅ Global Chat
|
| 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('<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 748 |
-
settings_nav = gr.Button("⚙️ Settings
|
| 749 |
-
chat_nav = gr.Button("💬 Global Chat
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 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 |
-
|
|
|
|
| 338 |
self.state["config"]["chat_mode"] = "single"; changed = True
|
| 339 |
-
|
| 340 |
-
|
| 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
|
| 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"
|
| 364 |
user["chat_room_id"] = "global-1"; changed = True
|
| 365 |
-
# Keep only the newest 400 messages
|
| 366 |
-
|
| 367 |
-
for
|
| 368 |
-
|
| 369 |
-
|
| 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"] =
|
| 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 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 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":
|
| 1010 |
-
"capacity":
|
| 1011 |
-
"current":
|
| 1012 |
-
"can_choose":
|
| 1013 |
-
"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"] =
|
| 1051 |
-
self.state["config"]["chat_room_capacity"] =
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 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 |
-
|
| 1126 |
-
|
| 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 |
-
|
| 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 |
+
}
|