Hugging8N / dashboard.html
AI Bot
Add gemini-web2api and dashboard server
c2b0f3c
Raw History Blame Contribute Delete
13.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Threads Automation Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0f1115;
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
--accent-green: #00ff88;
--accent-purple: #9d4edd;
--text-main: #ffffff;
--text-muted: #8b949e;
}
body {
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
background-image:
radial-gradient(circle at 15% 50%, rgba(157, 78, 221, 0.15), transparent 25%),
radial-gradient(circle at 85% 30%, rgba(0, 255, 136, 0.1), transparent 25%);
background-attachment: fixed;
color: var(--text-main);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3rem;
border-bottom: 1px solid var(--glass-border);
padding-bottom: 1.5rem;
}
h1 {
margin: 0;
font-size: 1.8rem;
font-weight: 700;
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 10px;
}
.status-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(0, 255, 136, 0.1);
border: 1px solid rgba(0, 255, 136, 0.2);
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
color: var(--accent-green);
box-shadow: 0 0 10px rgba(0, 255, 136, 0.2);
}
.status-dot {
width: 8px;
height: 8px;
background-color: var(--accent-green);
border-radius: 50%;
box-shadow: 0 0 8px var(--accent-green);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(0.95); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.7; }
100% { transform: scale(0.95); opacity: 1; }
}
.grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 900px) {
.grid {
grid-template-columns: 1fr;
}
}
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, border-color 0.3s ease;
}
.glass-panel:hover {
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
.panel-title {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.2rem;
font-weight: 600;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 8px;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 1rem;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--text-main);
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.8rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
}
.terminal {
background: #0d1117;
border-radius: 8px;
padding: 1rem;
font-family: 'Fira Code', monospace, Consolas;
font-size: 0.85rem;
color: #c9d1d9;
height: 500px;
overflow-y: auto;
border: 1px solid #30363d;
box-shadow: inset 0 0 20px rgba(0,0,0,0.5);
line-height: 1.5;
}
.terminal::-webkit-scrollbar {
width: 8px;
}
.terminal::-webkit-scrollbar-track {
background: #0d1117;
}
.terminal::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 4px;
}
.top-posts-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
.post-item {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: 1rem;
}
.post-text {
font-size: 0.9rem;
color: var(--text-main);
margin-bottom: 0.8rem;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.post-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
}
.meta-badge {
background: rgba(157, 78, 221, 0.15);
color: #d8b4fe;
padding: 4px 8px;
border-radius: 4px;
font-weight: 600;
}
.meta-likes {
color: var(--text-muted);
}
/* Terminal Syntax Highlighting */
.log-error { color: #ff7b72; }
.log-success { color: #3fb950; }
.log-warning { color: #d2a8ff; }
.log-info { color: #79c0ff; }
.log-time { color: #8b949e; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
Threads AI Automation
</h1>
<div class="status-badge">
<div class="status-dot"></div>
System Active
</div>
</header>
<div class="grid">
<div class="left-col">
<div class="glass-panel" style="margin-bottom: 2rem;">
<h2 class="panel-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="20" x2="18" y2="10"></line>
<line x1="12" y1="20" x2="12" y2="4"></line>
<line x1="6" y1="20" x2="6" y2="14"></line>
</svg>
Analytics
</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="val-posts">0</div>
<div class="stat-label">Total Posts</div>
</div>
<div class="stat-card">
<div class="stat-value" id="val-likes">0</div>
<div class="stat-label">Total Likes</div>
</div>
<div class="stat-card" style="grid-column: span 2;">
<div class="stat-value" id="val-replies">0</div>
<div class="stat-label">Total Comments</div>
</div>
</div>
</div>
<div class="glass-panel">
<h2 class="panel-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
Hall of Fame
</h2>
<ul class="top-posts-list" id="top-posts-container">
<li class="post-item" style="text-align: center; color: var(--text-muted); border: none;">
Waiting for data...
</li>
</ul>
</div>
</div>
<div class="right-col">
<div class="glass-panel">
<h2 class="panel-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
Live System Logs
</h2>
<div class="terminal" id="terminal-window">
Initializing connection to bot server...
</div>
</div>
</div>
</div>
</div>
<script>
function colorizeLog(line) {
if (line.includes('Error') || line.includes('Exception') || line.includes('Traceback')) {
return `<span class="log-error">${line}</span>`;
} else if (line.includes('SUCCESS') || line.includes('успешно') || line.includes('одобрен')) {
return `<span class="log-success">${line}</span>`;
} else if (line.includes('Predictor')) {
return `<span class="log-warning">${line}</span>`;
} else if (line.includes('Scheduler') || line.includes('Keep-Alive')) {
return `<span class="log-info">${line}</span>`;
}
return line;
}
async function fetchLogs() {
try {
const response = await fetch('/api/logs');
const data = await response.json();
const term = document.getElementById('terminal-window');
// Only update if logs changed to prevent scrolling issues if not needed
const formattedLogs = data.logs.split('\n').map(l => colorizeLog(l)).join('<br>');
if (term.innerHTML !== formattedLogs) {
const isScrolledToBottom = term.scrollHeight - term.clientHeight <= term.scrollTop + 1;
term.innerHTML = formattedLogs;
// Auto-scroll if user was already at the bottom
if (isScrolledToBottom) {
term.scrollTop = term.scrollHeight;
}
}
} catch (err) {
console.error("Failed to fetch logs:", err);
}
}
async function fetchStats() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
document.getElementById('val-posts').innerText = data.total_posts;
document.getElementById('val-likes').innerText = data.total_likes;
document.getElementById('val-replies').innerText = data.total_replies;
const topPostsContainer = document.getElementById('top-posts-container');
if (data.top_posts && data.top_posts.length > 0) {
topPostsContainer.innerHTML = '';
data.top_posts.forEach(post => {
const li = document.createElement('li');
li.className = 'post-item';
// Extract title from text if possible, otherwise use start of text
let displayText = post.text;
if (displayText.length > 150) displayText = displayText.substring(0, 150) + '...';
li.innerHTML = `
<div class="post-text">${displayText}</div>
<div class="post-meta">
<span class="meta-badge">Score: ${post.predicted_score}/10</span>
<span class="meta-likes">❤️ ${post.likes} | 💬 ${post.replies}</span>
</div>
`;
topPostsContainer.appendChild(li);
});
}
} catch (err) {
console.error("Failed to fetch stats:", err);
}
}
// Initial fetch
fetchLogs();
fetchStats();
// Polling
setInterval(fetchLogs, 3000);
setInterval(fetchStats, 10000); // Stats change less frequently
</script>
</body>
</html>