This commit is contained in:
Marcos
2026-03-22 10:10:27 -03:00
parent f72677ef27
commit 6589c62b18
4 changed files with 327 additions and 153 deletions

View File

@@ -1,11 +1,13 @@
<!DOCTYPE html>
<html lang="pt-BR" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPS AI Dashboard</title>
<!-- Favicon -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>">
<link rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%F0%9F%A4%96%3C/text%3E%3C/svg%3E">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
@@ -27,36 +29,50 @@
--transition: 0.25s ease;
}
/* Custom Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: var(--bg-base); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; border: 2px solid var(--bg-base); }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
/* Scrollbar Fix for Firefox */
* { scrollbar-width: thin; scrollbar-color: var(--accent) transparent; }
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-base);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
border: 2px solid var(--bg-base);
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
/* Scrollbar Fix for Firefox */
@supports (scrollbar-width: thin) {
* {
scrollbar-width: thin;
scrollbar-color: var(--accent) transparent;
}
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}
[data-theme="light"] {
--bg-base: #f1f5f9;
--bg-card: #ffffff;
@@ -70,6 +86,15 @@
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* Global SVG Presentation */
svg {
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
* {
margin: 0;
padding: 0;
@@ -396,21 +421,34 @@
font-size: 0.9rem;
}
.insights-content h3 { margin-bottom: 1rem; color: var(--accent); }
.insights-content table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
.insights-content h3 {
margin-bottom: 1rem;
color: var(--accent);
}
.insights-content table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
background: var(--bg-input);
border-radius: 8px;
overflow: hidden;
}
.insights-content th, .insights-content td {
padding: 0.75rem;
text-align: left;
.insights-content th,
.insights-content td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
.insights-content th { background: rgba(59, 130, 246, 0.1); color: var(--accent); font-size: 0.75rem; text-transform: uppercase; }
.insights-content th {
background: rgba(59, 130, 246, 0.1);
color: var(--accent);
font-size: 0.75rem;
text-transform: uppercase;
}
.insights-placeholder {
height: 100%;
display: flex;
@@ -567,6 +605,7 @@
align-items: center;
justify-content: center;
z-index: 2000;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
transition: opacity 0.5s ease, visibility 0.5s;
}
@@ -592,98 +631,180 @@
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.login-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.login-desc {
color: var(--text-muted);
margin-bottom: 1.5rem;
font-size: 0.85rem;
}
.login-input {
margin-bottom: 1rem;
text-align: center;
font-size: 1.1rem;
letter-spacing: 0.2rem;
}
.login-btn {
width: 100%;
padding: 0.8rem;
}
#login-error {
color: var(--danger);
font-size: 0.75rem;
margin-top: 1rem;
display: none;
}
.theme-icon-sun {
display: none;
}
.theme-icon-moon {
display: block;
}
[data-theme="light"] .theme-icon-sun {
display: block;
}
[data-theme="light"] .theme-icon-moon {
display: none;
}
.chat-input-area {
padding: 0.75rem;
gap: 0.5rem;
}
.recording-indicator {
display: none;
width: 8px;
height: 8px;
background: red;
border-radius: 50%;
animation: pulse 1s infinite;
}
.insights-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.insights-placeholder small {
display: block;
margin-top: 0.5rem;
opacity: 0.6;
}
.progress-bar {
width: 0%;
transition: width 0.5s ease;
}
</style>
</head>
<body>
<div id="login-overlay">
<div class="card login-card">
<div style="font-size: 3rem; margin-bottom: 1rem;">🔒</div>
<div class="login-icon" role="img" aria-label="Cadeado de Segurança">🔒</div>
<h2>Acesso Restrito</h2>
<p style="color:var(--text-muted); margin-bottom:1.5rem; font-size:0.85rem;">Esta VPS está protegida. Insira a senha mestra para gerenciar o Agente.</p>
<input type="password" id="web-pass-input" class="form-input" placeholder="Senha da VPS" style="margin-bottom:1rem; text-align:center; font-size: 1.1rem; letter-spacing: 0.2rem;" onkeypress="if(event.key==='Enter') attemptLogin()">
<button class="btn btn-primary" style="width:100%; padding: 0.8rem;" onclick="attemptLogin()">Entrar no Dashboard</button>
<div id="login-error" style="color:var(--danger); font-size: 0.75rem; margin-top: 1rem; display: none;">Senha incorreta. Tente novamente.</div>
<p class="login-desc">Esta VPS está protegida. Insira a senha mestra para gerenciar o Agente.</p>
<input type="password" id="web-pass-input" aria-label="Senha da VPS" autocomplete="current-password"
class="form-input login-input" placeholder="Senha da VPS"
onkeypress="if(event.key==='Enter') attemptLogin()">
<button type="button" class="btn btn-primary login-btn"
onclick="attemptLogin()">Entrar no Dashboard</button>
<div id="login-error">Senha incorreta. Tente novamente.</div>
</div>
</div>
<div class="container">
<header>
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
<h1>VPS AI Dashboard</h1>
</div>
<div class="header-actions">
<div class="status-badge" id="bot-status">Online</div>
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Alternar tema">
<svg id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none;">
<circle cx="12" cy="12" r="5"/>
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
<button type="button" class="theme-toggle" onclick="toggleTheme()" aria-label="Alternar tema">
<svg id="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="theme-icon-sun">
<circle cx="12" cy="12" r="5" />
<path
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
</svg>
<svg id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
<svg id="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="theme-icon-moon">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
</button>
</div>
</header>
<main>
<div class="stats-grid">
<div class="card stat-card">
<h3>CPU</h3>
<div class="stat-value" id="cpu-val">--%</div>
<div class="progress-bar-bg">
<div class="progress-bar" id="cpu-bar" style="width: 0%"></div>
<div class="progress-bar" id="cpu-bar"></div>
</div>
</div>
<div class="card stat-card">
<h3>RAM</h3>
<div class="stat-value" id="ram-val">-- / -- GB</div>
<div class="progress-bar-bg">
<div class="progress-bar" id="ram-bar" style="width: 0%"></div>
<div class="progress-bar" id="ram-bar"></div>
</div>
</div>
<div class="card stat-card">
<h3>Disk</h3>
<div class="stat-value" id="disk-val">--%</div>
<div class="progress-bar-bg">
<div class="progress-bar" id="disk-bar" style="width: 0%"></div>
<div class="progress-bar" id="disk-bar"></div>
</div>
</div>
</div>
<div class="section-title">Ações Rápidas</div>
<div class="actions-grid">
<button class="btn" onclick="executeAction('ping')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
<button type="button" class="btn" aria-label="Ping" onclick="executeAction('ping')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
Ping
</button>
<button class="btn" id="btn-test-llm" onclick="testLLMSpeed()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2a10 10 0 1 0 10 10H12V2z"/>
<path d="M12 2a10 10 0 0 1 10 10h-2a8 8 0 0 0-8-8V2z"/>
<path d="M12 12V2.5l5.5 5.5"/>
<button type="button" class="btn" id="btn-test-llm" aria-label="Testar LLM" onclick="testLLMSpeed()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2a10 10 0 1 0 10 10H12V2z" />
<path d="M12 2a10 10 0 0 1 10 10h-2a8 8 0 0 0-8-8V2z" />
<path d="M12 12V2.5l5.5 5.5" />
</svg>
Testar LLM
</button>
<button class="btn" onclick="executeAction('restart_bot')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6M1 20v-6h6"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
<button type="button" class="btn" aria-label="Reiniciar Bot" onclick="executeAction('restart_bot')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<path d="M23 4v6h-6M1 20v-6h6" />
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
</svg>
Reiniciar
</button>
<button class="btn" onclick="executeAction('clear_cache')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<button type="button" class="btn" aria-label="Limpar Cache" onclick="executeAction('clear_cache')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
Limpar Cache
</button>
<button class="btn btn-danger" onclick="executeAction('reboot_vps')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
<button type="button" class="btn btn-danger" aria-label="Reboot VPS" onclick="executeAction('reboot_vps')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
Reboot VPS
</button>
@@ -693,22 +814,23 @@
<div class="card">
<div class="config-grid">
<div class="form-group">
<label>Provider Ativo</label>
<select id="active_provider" class="form-input">
<label for="active_provider">Provider Ativo</label>
<select id="active_provider" aria-label="Provider Ativo" class="form-input">
<option value="ollama">Ollama (Local)</option>
<option value="gemini">Gemini Pro (Google)</option>
</select>
</div>
<div class="form-group">
<label>Gemini API Key</label>
<input type="password" id="gemini_api_key" class="form-input" placeholder="AIzaSy...">
<label for="gemini_api_key">Gemini API Key</label>
<input type="password" id="gemini_api_key" autocomplete="new-password" aria-label="Gemini API Key"
class="form-input" placeholder="AIzaSy...">
</div>
</div>
<button class="btn btn-primary" onclick="saveConfiguration()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
<button type="button" class="btn btn-primary" aria-label="Salvar Configuração" onclick="saveConfiguration()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
<polyline points="17 21 17 13 7 13 7 21" />
<polyline points="7 3 7 8 15 8" />
</svg>
Salvar
</button>
@@ -720,24 +842,27 @@
<div class="chat-wrapper">
<div class="chat-messages" id="chat-box">
<div class="chat-bubble bubble-ai">
Olá! Sou o VPS Agent. Como posso ajudar com seu servidor? Tudo o que eu fizer aparecerá aqui no terminal técnico.
Olá! Sou o VPS Agent. Como posso ajudar com seu servidor? Tudo o que eu fizer aparecerá aqui no
terminal técnico.
</div>
</div>
<div class="chat-input-area" style="padding: 0.75rem; gap: 0.5rem;">
<input type="text" id="chat-input" class="chat-input" placeholder="Comande sua VPS aqui..." onkeypress="handleKeyPress(event)">
<button class="btn" id="audio-btn" onclick="toggleRecording()" title="Gravar Áudio">
<svg id="mic-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8"/>
<div class="chat-input-area">
<input type="text" id="chat-input" aria-label="Comando do chat" class="chat-input"
placeholder="Comande sua VPS aqui..." onkeypress="handleKeyPress(event)">
<button type="button" class="btn" id="audio-btn" aria-label="Gravar Áudio"
onclick="toggleRecording()" title="Gravar Áudio">
<svg id="mic-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
<path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8" />
</svg>
<div id="recording-dot" style="display:none; width:8px; height:8px; background:red; border-radius:50%; animation: pulse 1s infinite;"></div>
<span id="recording-dot" class="recording-indicator"></span>
</button>
<button class="btn btn-primary" onclick="sendChat()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
<button type="button" class="btn btn-primary" aria-label="Enviar mensagem" onclick="sendChat()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
<line x1="22" y1="2" x2="11" y2="13" />
<polygon points="22 2 15 22 11 13 2 9 22 2" />
</svg>
</button>
</div>
@@ -746,22 +871,22 @@
<!-- Coluna 2: Painel de Insights (Refinado) -->
<div class="insights-wrapper">
<div class="insights-header">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<path d="M21 12V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7"/>
<line x1="16" y1="5" x2="16" y2="19"/>
<line x1="2" x2="16" y2="12"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path d="M21 12V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7" />
<line x1="16" y1="5" x2="16" y2="19" />
<line x1="2" x2="16" y2="12" />
</svg>
Painel de Insights Visuais
</div>
<div class="insights-content" id="insights-panel">
<div class="insights-placeholder">
<div style="font-size: 2.5rem; margin-bottom: 1rem;">📊</div>
<div class="insights-icon" role="img" aria-label="Gráfico de Insights">📊</div>
<p>Aguardando dados estruturados...</p>
<small style="display:block; margin-top:0.5rem; opacity:0.6;">Peça algo como "status dos containers" para ver o refinamento aqui.</small>
<small>Peça algo como "status dos containers" para ver o refinamento aqui.</small>
</div>
</div>
</div>
</div>
</main>
</div>
<div id="toast">Ação executada!</div>
@@ -773,7 +898,7 @@
async function apiFetch(url, options = {}) {
if (!options.headers) options.headers = {};
options.headers['X-Web-Password'] = webPassword;
const res = await fetch(url, options);
if (res.status === 401) {
showLoginOverlay();
@@ -822,15 +947,11 @@
updateThemeIcon(!isDark);
}
function updateThemeIcon(isDark) {
document.getElementById('icon-sun').style.display = isDark ? 'block' : 'none';
document.getElementById('icon-moon').style.display = isDark ? 'none' : 'block';
}
function updateThemeIcon(isDark) {}
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.dataset.theme = savedTheme;
updateThemeIcon(savedTheme === 'light');
}
// Stats
@@ -874,7 +995,7 @@
}
}
setInterval(() => { if(!document.getElementById('login-overlay').classList.contains('hidden')) return; fetchStats(); }, 3000);
setInterval(() => { if (!document.getElementById('login-overlay').classList.contains('hidden')) return; fetchStats(); }, 3000);
// Actions
async function executeAction(type) {
@@ -901,7 +1022,7 @@
function showToast(msg, isError = false) {
const toast = document.getElementById('toast');
if(!toast) return;
if (!toast) return;
toast.textContent = msg;
toast.className = isError ? 'error show' : 'show';
setTimeout(() => toast.classList.remove('show'), 3000);
@@ -931,7 +1052,7 @@
function processAIReply(fullText) {
// Separa a parte técnica da parte refinada
const refinedMatch = fullText.match(/<REFINED>([\s\S]*?)<\/REFINED>/i);
let technicalPart = fullText;
if (refinedMatch) {
technicalPart = fullText.replace(refinedMatch[0], '').trim();
@@ -946,11 +1067,11 @@
function updateInsightsPanel(markdown) {
const panel = document.getElementById('insights-panel');
if(!panel) return;
if (!panel) return;
// Injeta a senha web nas URLs das imagens para autenticação inline
const mdWithAuth = markdown.replace(/\/api\/host_file\?path=/g, '/api/host_file?pwd=' + encodeURIComponent(webPassword) + '&path=');
// Renderiza o Markdown para HTML usando marked.js
panel.innerHTML = `<div class="animate-fade-in">${marked.parse(mdWithAuth)}</div>`;
}
@@ -961,7 +1082,7 @@
function addBubble(text, sender) {
const box = document.getElementById('chat-box');
if(!box) return;
if (!box) return;
const div = document.createElement('div');
div.className = 'chat-bubble bubble-' + sender;
div.textContent = text;
@@ -977,7 +1098,7 @@
const key = document.getElementById('gemini_api_key');
if (provider) provider.value = data.active_provider || 'ollama';
if (key) key.value = data.gemini_api_key || '';
} catch (e) {}
} catch (e) { }
}
async function saveConfig() {
@@ -1010,20 +1131,20 @@
try {
const res = await apiFetch('/api/test_llm');
const data = await res.json();
if(data.status === 'success') {
if (data.status === 'success') {
showToast(`✅ LLM Online! Resposta em ${data.latency}s`);
btn.innerHTML = `${data.latency}s`;
setTimeout(() => { btn.innerHTML = originalContent; btn.disabled = false; }, 5000);
} else {
throw new Error(data.message);
}
} catch(e) {
} catch (e) {
showToast("❌ Erro no Teste LLM: " + e.message, true);
btn.innerHTML = '❌ Falhou';
btn.classList.add('btn-danger');
setTimeout(() => {
btn.innerHTML = originalContent;
btn.disabled = false;
setTimeout(() => {
btn.innerHTML = originalContent;
btn.disabled = false;
btn.classList.remove('btn-danger');
}, 5000);
}
@@ -1074,31 +1195,31 @@
const dot = document.getElementById('recording-dot');
const icon = document.getElementById('mic-icon');
const btn = document.getElementById('audio-btn');
dot.style.display = active ? 'block' : 'none';
icon.style.color = active ? 'red' : 'inherit';
btn.style.borderColor = active ? 'red' : 'var(--border)';
if (dot) dot.style.display = active ? 'inline-block' : 'none';
if (icon) icon.style.color = active ? 'red' : 'inherit';
if (btn) btn.style.borderColor = active ? 'red' : 'var(--border)';
}
async function uploadAudio(blob) {
const formData = new FormData();
formData.append('audio', blob, 'voice.webm');
showToast("✨ Transcrevendo áudio...");
try {
const res = await apiFetch('/api/chat-audio', {
method: 'POST',
body: formData
});
const data = await res.json();
if(data.text) {
if (data.text) {
addBubble(data.text, 'user');
processAIReply(data.reply);
// Se o bot retornou áudio, toca ele
if(data.audio_url) {
if (data.audio_url) {
const audio = new Audio(data.audio_url + '?t=' + Date.now());
audio.play();
}
@@ -1121,11 +1242,12 @@
} else {
showLoginOverlay();
}
} catch(e) { showLoginOverlay(); }
} catch (e) { showLoginOverlay(); }
})();
} else {
showLoginOverlay();
}
</script>
</body>
</html>
</html>