← back to Dear Bubbe Admin
frontend/index.html
2190 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dear Bubbe Admin - Restricted Access</title>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
/* Login Screen */
.login-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.login-card {
background: white;
border-radius: 20px;
padding: 40px;
max-width: 400px;
width: 100%;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
}
.login-card h1 {
color: #333;
margin-bottom: 10px;
}
.restricted-notice {
color: #dc3545;
font-weight: bold;
margin: 20px 0;
padding: 15px;
background: #fee;
border-radius: 10px;
border: 2px solid #dc3545;
}
/* Dashboard */
.dashboard {
display: none;
min-height: 100vh;
background: #f5f5f5;
}
.dashboard.active {
display: block;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
}
.user-info img {
width: 40px;
height: 40px;
border-radius: 50%;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.stat-card h3 {
color: #666;
font-size: 14px;
margin-bottom: 10px;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #333;
}
.stat-card.alert {
border-left: 4px solid #dc3545;
}
.stat-card.success {
border-left: 4px solid #28a745;
}
/* Alerts Section */
.alerts-container {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
margin-bottom: 30px;
}
.alert-item {
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.alert-item.high {
background: #fee;
border-left: 4px solid #dc3545;
}
.alert-item.medium {
background: #fff3cd;
border-left: 4px solid #ffc107;
}
.alert-item.low {
background: #f0f0f0;
border-left: 4px solid #6c757d;
}
.alert-badge {
padding: 5px 10px;
border-radius: 5px;
font-size: 12px;
font-weight: bold;
}
.badge-antisemitic {
background: #dc3545;
color: white;
}
.badge-racist {
background: #fd7e14;
color: white;
}
/* Users Table */
.users-table {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: #f8f9fa;
padding: 12px;
text-align: left;
font-weight: 600;
color: #666;
}
td {
padding: 12px;
border-bottom: 1px solid #eee;
}
.btn {
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
font-size: 14px;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-blocked {
background: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
<!-- Login Screen -->
<div class="login-container" id="loginScreen">
<div class="login-card">
<h1>🥯 Dear Bubbe Admin</h1>
<p style="color: #666; margin-bottom: 20px;">Administrative Dashboard</p>
<div class="restricted-notice">
⚠️ RESTRICTED ACCESS<br>
Only authorized administrators can access this system
</div>
<!-- Password Login Form -->
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
<p style="color: #666; margin-bottom: 15px; font-size: 14px;">Admin Login</p>
<form id="passwordLoginForm" onsubmit="handlePasswordLogin(event)">
<input
type="email"
id="adminEmail"
placeholder="Email address"
required
style="width: 100%; padding: 12px; margin-bottom: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 16px;"
value="steve@designerwallcoverings.com"
readonly
/>
<input
type="password"
id="adminPassword"
placeholder="Password"
required
style="width: 100%; padding: 12px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 5px; font-size: 16px;"
/>
<button
type="submit"
style="width: 100%; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 5px; font-size: 16px; font-weight: bold; cursor: pointer;">
Sign In
</button>
</form>
</div>
<!-- Google OAuth (disabled for now) -->
<div style="margin-top: 20px; opacity: 0.5; pointer-events: none;">
<p style="color: #999; font-size: 12px; margin-bottom: 10px;">Google Sign-In temporarily unavailable</p>
<div id="g_id_onload"
data-client_id="493480746821-mhpjc0glkkogcf845ubi2hkhksoqj39k.apps.googleusercontent.com"
data-callback="handleGoogleLogin"
data-auto_prompt="false"
data-ux_mode="popup"
data-login_uri="http://45.61.58.125:5013">
</div>
<div class="g_id_signin"
data-type="standard"
data-size="large"
data-theme="outline"
data-text="sign_in_with"
data-shape="rectangular"
data-logo_alignment="left">
</div>
</div>
</div>
</div>
<!-- Dashboard -->
<div class="dashboard" id="dashboard">
<!-- Active Users Bottom Bar -->
<div id="activeUsersBar" style="position: fixed; bottom: 0; left: 0; right: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 10px 20px; display: flex; align-items: center; gap: 20px; box-shadow: 0 -2px 10px rgba(0,0,0,0.2); z-index: 1000; overflow-x: auto;">
<div style="display: flex; align-items: center; gap: 10px; font-weight: bold; min-width: fit-content;">
<span style="display: inline-block; width: 10px; height: 10px; background: #4caf50; border-radius: 50%; animation: pulse 2s infinite;"></span>
<span id="activeUsersLabel">Active Users (0)</span>
</div>
<div id="activeUsersList" style="display: flex; gap: 15px; flex: 1; overflow-x: auto;">
<!-- Active users will be dynamically added here -->
</div>
</div>
<div class="header">
<div class="header-content">
<h1>Dear Bubbe Admin Dashboard</h1>
<div class="user-info">
<img id="userPicture" src="" alt="Admin">
<div>
<div id="userName" style="font-weight: bold;"></div>
<div style="font-size: 12px;">Administrator</div>
</div>
<button class="btn" onclick="logout()" style="background: white; color: #333; margin-left: 20px;">
Sign Out
</button>
</div>
</div>
</div>
<div class="container">
<!-- Action Buttons Bar -->
<div style="background: white; border-radius: 10px; padding: 15px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); display: flex; gap: 10px; flex-wrap: wrap;">
<button onclick="refreshDashboard()" style="padding: 8px 15px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;">
🔄 Refresh Dashboard
</button>
<button onclick="generateReport()" style="padding: 8px 15px; background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer;">
📊 Generate Report
</button>
<button onclick="exportUsers()" style="padding: 8px 15px; background: #17a2b8; color: white; border: none; border-radius: 5px; cursor: pointer;">
📥 Export Users
</button>
<button onclick="exportAlerts()" style="padding: 8px 15px; background: #ffc107; color: black; border: none; border-radius: 5px; cursor: pointer;">
📥 Export Alerts
</button>
<button onclick="exportChats()" style="padding: 8px 15px; background: #6f42c1; color: white; border: none; border-radius: 5px; cursor: pointer;">
📥 Export Chats
</button>
<button onclick="clearAlerts()" style="padding: 8px 15px; background: #dc3545; color: white; border: none; border-radius: 5px; cursor: pointer;">
🗑️ Clear Alerts
</button>
<button onclick="clearChats()" style="padding: 8px 15px; background: #dc3545; color: white; border: none; border-radius: 5px; cursor: pointer;">
🗑️ Clear Chats
</button>
<button onclick="window.open('http://45.61.58.125:3011', '_blank')" style="padding: 8px 15px; background: #20c997; color: white; border: none; border-radius: 5px; cursor: pointer;">
🌐 Open Bubbe Site
</button>
</div>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card success">
<h3>TOTAL USERS</h3>
<div class="stat-value" id="totalUsers">0</div>
</div>
<div class="stat-card">
<h3>ACTIVE NOW</h3>
<div class="stat-value" id="activeUsers">0</div>
</div>
<div class="stat-card alert">
<h3>TOTAL ALERTS</h3>
<div class="stat-value" id="totalAlerts">0</div>
</div>
<div class="stat-card alert">
<h3>CRITICAL ALERTS</h3>
<div class="stat-value" id="criticalAlerts">0</div>
</div>
</div>
<!-- Real-time Alerts -->
<div class="alerts-container">
<h2 style="margin-bottom: 20px;">🚨 Real-time Alerts</h2>
<div id="alertsList">
<p style="color: #999;">No alerts to display</p>
</div>
</div>
<!-- Live Chat Monitoring -->
<div class="live-chat-container" style="background: white; border-radius: 10px; padding: 20px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
<h2 style="margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
<span style="color: #28a745; animation: pulse 2s infinite;">🔴</span>
Live User Activity
<span id="liveUserCount" style="background: #28a745; color: white; padding: 2px 10px; border-radius: 20px; font-size: 14px;">0 active</span>
</h2>
<div id="liveChatsList" style="max-height: 400px; overflow-y: auto; border: 1px solid #eee; border-radius: 8px; padding: 10px;">
<p style="color: #999; text-align: center;">No active conversations</p>
</div>
</div>
<!-- Users Table -->
<div class="users-table">
<h2 style="margin-bottom: 20px;">👥 User Management</h2>
<table>
<thead>
<tr>
<th>Email</th>
<th>Name</th>
<th>Status</th>
<th>Joined</th>
<th>Last Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="usersTableBody">
<tr>
<td colspan="6" style="text-align: center; color: #999;">Loading users...</td>
</tr>
</tbody>
</table>
</div>
<!-- Civil Rights Security - Banned Terms -->
<div class="banned-terms-container" style="background: white; border-radius: 10px; padding: 20px; margin-top: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
<h2 style="color: #dc3545; margin-bottom: 20px;">🚫 Civil Rights Security - Banned Terms</h2>
<div id="bannedTermsInfo" style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<div><strong>Version:</strong> <span id="bannedVersion">-</span></div>
<div><strong>Last Updated:</strong> <span id="bannedUpdated">-</span></div>
<div><strong>Total Terms:</strong> <span id="bannedTotal" style="color: #dc3545; font-weight: bold;">0</span></div>
</div>
<div style="margin-top: 10px; color: #666;">
<strong>Sources:</strong> <span id="bannedSources">-</span>
</div>
</div>
<!-- Category Filter -->
<div style="margin-bottom: 15px;">
<label style="margin-right: 10px;">Filter by Category:</label>
<select id="categoryFilter" onchange="filterBannedTerms()" style="padding: 8px; border-radius: 5px; border: 1px solid #ddd;">
<option value="all">All Categories</option>
<option value="ethnic_slurs">Ethnic Slurs</option>
<option value="antisemitic_slurs">Antisemitic Slurs</option>
<option value="racial_slurs">Racial Slurs</option>
<option value="lgbtq_slurs">LGBTQ+ Slurs</option>
<option value="holocaust_references">Holocaust References</option>
<option value="offensive_phrases">Offensive Phrases</option>
</select>
<label style="margin-left: 20px; margin-right: 10px;">Severity:</label>
<select id="severityFilter" onchange="filterBannedTerms()" style="padding: 8px; border-radius: 5px; border: 1px solid #ddd;">
<option value="all">All Severities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
</select>
</div>
<!-- Terms List -->
<div id="bannedTermsList" style="overflow-y: auto; max-height: 500px; border: 1px solid #e0e0e0; border-radius: 8px; padding: 10px;">
Loading banned terms...
</div>
<!-- Add New Banned Term -->
<div style="margin-top: 20px; padding: 15px; background: #fff5f5; border-radius: 8px; border: 1px solid #ffcdd2;">
<h4 style="color: #dc3545; margin-bottom: 15px;">➕ Add New Banned Term</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px;">
<input type="text" id="newBannedTerm" placeholder="Term *" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
<input type="text" id="newBannedVariants" placeholder="Variants (comma-separated)" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
<select id="newBannedCategory" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
<option value="">Select Category *</option>
<option value="ethnic_slurs">Ethnic Slurs</option>
<option value="antisemitic_slurs">Antisemitic Slurs</option>
<option value="racial_slurs">Racial Slurs</option>
<option value="lgbtq_slurs">LGBTQ+ Slurs</option>
<option value="holocaust_references">Holocaust References</option>
<option value="offensive_phrases">Offensive Phrases</option>
</select>
<select id="newBannedSeverity" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
<option value="">Select Severity *</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
</select>
<input type="text" id="newBannedReason" placeholder="Reason" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; grid-column: 1 / -1;">
<input type="text" id="newBannedReplacement" placeholder="Replacement" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
<button onclick="addBannedTerm()" style="background: #dc3545; color: white; padding: 8px 15px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold;">Add Banned Term</button>
</div>
</div>
<!-- Acceptable Yiddish Section -->
<div style="margin-top: 20px; background: #e8f5e9; padding: 15px; border-radius: 8px;">
<h3 style="color: #28a745;">✅ Acceptable Yiddish Terms</h3>
<!-- Add New Acceptable Term -->
<div style="margin: 15px 0; padding: 15px; background: white; border-radius: 8px;">
<h4 style="color: #4caf50; margin-bottom: 15px;">➕ Add New Acceptable Term</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px;">
<input type="text" id="newAcceptableTerm" placeholder="Term *" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
<input type="text" id="newAcceptableVariants" placeholder="Variants (comma-separated)" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
<input type="text" id="newAcceptableMeaning" placeholder="Meaning *" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; grid-column: 1 / -1;">
<input type="text" id="newAcceptableUsage" placeholder="Usage" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px; grid-column: 1 / -1;">
<input type="text" id="newAcceptableNotes" placeholder="Notes" style="padding: 8px; border: 1px solid #ddd; border-radius: 5px;">
<button onclick="addAcceptableTerm()" style="background: #4caf50; color: white; padding: 8px 15px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold;">Add Acceptable Term</button>
</div>
</div>
<!-- Display Acceptable Terms -->
<div id="acceptableTermsList" style="max-height: 400px; overflow-y: auto; margin-top: 15px;">
<!-- Will be populated dynamically -->
</div>
</div>
</div>
</div>
</div>
<script>
let authToken = null;
let socket = null;
const API_URL = 'http://45.61.58.125:5013';
// Handle Google Login
// Handle Password Login
window.handlePasswordLogin = async function(event) {
event.preventDefault();
const email = document.getElementById('adminEmail').value;
const password = document.getElementById('adminPassword').value;
try {
const res = await fetch(`${API_URL}/api/auth/password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
password: password
})
});
const data = await res.json();
if (res.ok) {
// Login successful
authToken = data.token;
localStorage.setItem('adminToken', authToken);
// Update UI
document.getElementById('userName').textContent = data.user.name;
if (data.user.picture) {
document.getElementById('userPicture').src = data.user.picture;
}
// Show dashboard
document.getElementById('loginScreen').style.display = 'none';
document.getElementById('dashboard').classList.add('active');
// Initialize dashboard
initDashboard();
} else {
// Access denied
alert(data.message || 'Invalid password. Please try again.');
}
} catch (error) {
console.error('Login error:', error);
alert('Login failed. Please try again.');
}
}
// Make handleGoogleLogin globally accessible for Google OAuth callback
window.handleGoogleLogin = async function(response) {
try {
const res = await fetch(`${API_URL}/api/auth/google`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
credential: response.credential
})
});
const data = await res.json();
if (res.ok) {
// Login successful
authToken = data.token;
localStorage.setItem('adminToken', authToken);
// Update UI
document.getElementById('userName').textContent = data.user.name;
document.getElementById('userPicture').src = data.user.picture;
// Show dashboard
document.getElementById('loginScreen').style.display = 'none';
document.getElementById('dashboard').classList.add('active');
// Initialize dashboard
initDashboard();
} else {
// Access denied
if (data.error && data.error.includes('restricted')) {
alert('❌ ACCESS DENIED\n\nThis admin panel is restricted to authorized administrators only.\n\nOnly steve@designerwallcoverings.com can access this system.');
} else {
alert(data.message || 'Access denied. Only authorized administrators can access this system.');
}
// Sign out from Google to allow retry with different account
if (google && google.accounts && google.accounts.id) {
google.accounts.id.disableAutoSelect();
}
}
} catch (error) {
console.error('Login error:', error);
alert('Login failed. Please try again.');
}
}
// Initialize Dashboard
async function initDashboard() {
// Connect WebSocket
socket = io(API_URL, {
auth: {
token: authToken
}
});
socket.on('new-alert', (alert) => {
addAlertToList(alert);
updateAlertCount();
});
socket.on('stats-update', (stats) => {
document.getElementById('activeUsers').textContent = stats.activeUsers;
});
// Live chat updates
socket.on('live-chat-update', (data) => {
updateLiveChat(data);
});
socket.on('user-status-change', (data) => {
console.log('User status changed:', data);
loadUsers(); // Reload users table
});
socket.on('user-deleted', (data) => {
console.log('User deleted:', data);
loadUsers(); // Reload users table
});
// Load initial data
await loadDashboardStats();
await loadUsers();
await loadAlerts();
await loadBannedTerms();
await loadActiveSessions();
}
// Load Dashboard Stats
async function loadDashboardStats() {
try {
const response = await fetch(`${API_URL}/api/dashboard/stats`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const stats = await response.json();
document.getElementById('totalUsers').textContent = stats.totalUsers;
document.getElementById('activeUsers').textContent = stats.activeToday;
document.getElementById('totalAlerts').textContent = stats.totalAlerts;
document.getElementById('criticalAlerts').textContent = stats.criticalAlerts;
}
} catch (error) {
console.error('Failed to load stats:', error);
}
}
// Load Users
async function loadUsers() {
try {
const response = await fetch(`${API_URL}/api/users`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const users = await response.json();
const tbody = document.getElementById('usersTableBody');
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: #999;">No users found</td></tr>';
return;
}
tbody.innerHTML = users.map(user => `
<tr>
<td>${user.email}</td>
<td>${user.preferredName || user.name || 'Unknown'}</td>
<td>
<span class="status-badge ${user.blocked ? 'status-blocked' : 'status-active'}">
${user.blocked ? 'Blocked' : 'Active'}
</span>
</td>
<td>${new Date(user.createdAt).toLocaleDateString()}</td>
<td>${user.lastActive ? new Date(user.lastActive).toLocaleString() : 'Never'}</td>
<td>
<div class="user-actions">
${user.active === false
? `<button class="action-btn btn-activate" onclick="activateUser('${user.email}')">Activate</button>`
: `<button class="action-btn btn-deactivate" onclick="deactivateUser('${user.email}')">Deactivate</button>`
}
${user.blocked
? `<button class="action-btn btn-activate" onclick="unblockUser('${user.email}')">Unblock</button>`
: `<button class="action-btn btn-block" onclick="blockUser('${user.email}')">Block</button>`
}
<button class="action-btn btn-delete" onclick="deleteUser('${user.email}')">Delete</button>
</div>
</td>
</tr>
`).join('');
}
} catch (error) {
console.error('Failed to load users:', error);
}
}
// Load Alerts
async function loadAlerts() {
try {
const response = await fetch(`${API_URL}/api/alerts`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
const alertsList = document.getElementById('alertsList');
const allAlerts = [...(data.antisemitic || []), ...(data.racist || []), ...(data.other || [])]
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
.slice(0, 10);
if (allAlerts.length === 0) {
alertsList.innerHTML = '<p style="color: #999;">No alerts to display</p>';
return;
}
alertsList.innerHTML = allAlerts.map(alert => `
<div class="alert-item ${alert.severity.toLowerCase()}">
<div>
<span class="alert-badge badge-${alert.type}">${alert.type.toUpperCase()}</span>
<strong style="margin-left: 10px;">${alert.userId}</strong>
<div style="margin-top: 5px; color: #666;">${alert.message.substring(0, 100)}...</div>
</div>
<div style="text-align: right;">
<div style="color: #999; font-size: 12px;">${new Date(alert.timestamp).toLocaleString()}</div>
<div style="color: #666; font-size: 12px;">IP: ${alert.ip || 'Unknown'}</div>
</div>
</div>
`).join('');
}
} catch (error) {
console.error('Failed to load alerts:', error);
}
}
// Block User
async function blockUser(email) {
if (!confirm(`Are you sure you want to block ${email}?`)) return;
const reason = prompt('Enter reason for blocking:');
if (!reason) return;
try {
const response = await fetch(`${API_URL}/api/users/${email}/block`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
blocked: true,
reason: reason
})
});
if (response.ok) {
alert('User blocked successfully');
loadUsers();
}
} catch (error) {
console.error('Failed to block user:', error);
alert('Failed to block user');
}
}
// Unblock User
async function unblockUser(email) {
if (!confirm(`Are you sure you want to unblock ${email}?`)) return;
try {
const response = await fetch(`${API_URL}/api/users/${email}/block`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
blocked: false,
reason: 'Unblocked by admin'
})
});
if (response.ok) {
alert('User unblocked successfully');
loadUsers();
}
} catch (error) {
console.error('Failed to unblock user:', error);
alert('Failed to unblock user');
}
}
// Add Alert to List
function addAlertToList(alert) {
const alertsList = document.getElementById('alertsList');
const alertHtml = `
<div class="alert-item ${alert.severity.toLowerCase()}" style="animation: slideIn 0.5s;">
<div>
<span class="alert-badge badge-${alert.type}">🚨 NEW - ${alert.type.toUpperCase()}</span>
<strong style="margin-left: 10px;">${alert.userId}</strong>
<div style="margin-top: 5px; color: #666;">${alert.message.substring(0, 100)}...</div>
</div>
<div style="text-align: right;">
<div style="color: #999; font-size: 12px;">${new Date(alert.timestamp).toLocaleString()}</div>
<div style="color: #666; font-size: 12px;">IP: ${alert.ip || 'Unknown'}</div>
</div>
</div>
`;
// Remove "no alerts" message if present
if (alertsList.innerHTML.includes('No alerts to display')) {
alertsList.innerHTML = '';
}
// Add new alert at the top
alertsList.insertAdjacentHTML('afterbegin', alertHtml);
}
// Load Banned Terms
let allBannedTerms = [];
async function loadBannedTerms() {
try {
const response = await fetch(`${API_URL}/api/banned-terms`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const data = await response.json();
allBannedTerms = data.terms || [];
// Update info section
document.getElementById('bannedVersion').textContent = data.version || '-';
document.getElementById('bannedUpdated').textContent = data.lastUpdated || '-';
document.getElementById('bannedTotal').textContent = data.totalTerms || 0;
document.getElementById('bannedSources').textContent = data.sources ? data.sources.join(', ') : '-';
// Display banned terms
displayBannedTerms(allBannedTerms);
// Load acceptable terms separately
loadAcceptableTerms();
} catch (error) {
console.error('Failed to load banned terms:', error);
document.getElementById('bannedTermsList').innerHTML = '<div style="color: red;">Failed to load banned terms</div>';
}
}
// Display Banned Terms
function displayBannedTerms(terms) {
const container = document.getElementById('bannedTermsList');
if (terms.length === 0) {
container.innerHTML = '<div style="color: #666; text-align: center;">No banned terms found</div>';
return;
}
const html = terms.map(term => {
const severityColor = term.severity === 'critical' ? '#dc3545' : term.severity === 'high' ? '#ff9800' : '#ffc107';
const variantsDisplay = term.variants && term.variants.length > 0
? `<div style="margin-top: 5px; font-size: 12px; color: #666;">Variants: ${term.variants.join(', ')}</div>`
: '';
return `
<div style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 10px; margin-bottom: 10px; background: #fafafa; position: relative;">
<button onclick="deleteBannedTerm('${encodeURIComponent(term.term)}')"
style="position: absolute; top: 5px; right: 5px; background: #dc3545; color: white; border: none; border-radius: 3px; padding: 2px 8px; cursor: pointer; font-size: 11px;">
Delete
</button>
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<strong style="color: ${severityColor}; font-size: 16px;">${term.term}</strong>
${variantsDisplay}
<div style="margin-top: 5px; color: #333;">${term.reason || 'No reason provided'}</div>
${term.replacement ? `<div style="margin-top: 5px; color: #28a745;">Replacement: "${term.replacement}"</div>` : ''}
</div>
<div style="text-align: right; margin-right: 50px;">
<span style="background: ${severityColor}; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px;">${term.severity.toUpperCase()}</span>
<div style="margin-top: 5px; color: #666; font-size: 12px;">${term.category.replace(/_/g, ' ').toUpperCase()}</div>
</div>
</div>
</div>
`;
}).join('');
container.innerHTML = html;
}
// Load acceptable terms
async function loadAcceptableTerms() {
try {
const response = await fetch(`${API_URL}/api/acceptable-terms`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const data = await response.json();
displayAcceptableTerms(data.terms || []);
} catch (error) {
console.error('Failed to load acceptable terms:', error);
document.getElementById('acceptableTermsList').innerHTML = '<div style="color: red;">Failed to load acceptable terms</div>';
}
}
// Display acceptable terms
function displayAcceptableTerms(terms) {
const container = document.getElementById('acceptableTermsList');
if (terms.length === 0) {
container.innerHTML = '<div style="color: #666; text-align: center; padding: 20px;">No acceptable terms defined</div>';
return;
}
const html = terms.map(term => {
const variantsDisplay = term.variants && term.variants.length > 0
? `<div style="margin-top: 5px; font-size: 12px; color: #666;">Variants: ${term.variants.join(', ')}</div>`
: '';
return `
<div style="border: 1px solid #4caf50; border-radius: 8px; padding: 12px; margin-bottom: 10px; background: white; position: relative;">
<button onclick="deleteAcceptableTerm('${encodeURIComponent(term.term)}')"
style="position: absolute; top: 5px; right: 5px; background: #dc3545; color: white; border: none; border-radius: 3px; padding: 2px 8px; cursor: pointer; font-size: 12px;">
Delete
</button>
<div>
<strong style="color: #4caf50; font-size: 16px;">${term.term}</strong>
${variantsDisplay}
<div style="margin-top: 5px; color: #333;"><strong>Meaning:</strong> ${term.meaning}</div>
${term.usage ? `<div style="margin-top: 3px; color: #666;"><strong>Usage:</strong> ${term.usage}</div>` : ''}
${term.notes ? `<div style="margin-top: 3px; color: #999; font-style: italic;">${term.notes}</div>` : ''}
</div>
</div>
`;
}).join('');
container.innerHTML = html;
}
// Add new banned term
async function addBannedTerm() {
const term = document.getElementById('newBannedTerm').value.trim();
const category = document.getElementById('newBannedCategory').value;
const severity = document.getElementById('newBannedSeverity').value;
const variants = document.getElementById('newBannedVariants').value.trim();
const reason = document.getElementById('newBannedReason').value.trim();
const replacement = document.getElementById('newBannedReplacement').value.trim();
if (!term || !category || !severity) {
alert('Term, category, and severity are required');
return;
}
try {
const response = await fetch(`${API_URL}/api/banned-terms`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
term,
category,
severity,
variants,
reason,
replacement
})
});
if (response.ok) {
alert('Banned term added successfully');
// Clear form
document.getElementById('newBannedTerm').value = '';
document.getElementById('newBannedVariants').value = '';
document.getElementById('newBannedCategory').value = '';
document.getElementById('newBannedSeverity').value = '';
document.getElementById('newBannedReason').value = '';
document.getElementById('newBannedReplacement').value = '';
// Reload terms
loadBannedTerms();
} else {
const error = await response.json();
alert('Failed to add term: ' + error.error);
}
} catch (error) {
console.error('Error adding banned term:', error);
alert('Failed to add banned term');
}
}
// Add new acceptable term
async function addAcceptableTerm() {
const term = document.getElementById('newAcceptableTerm').value.trim();
const meaning = document.getElementById('newAcceptableMeaning').value.trim();
const variants = document.getElementById('newAcceptableVariants').value.trim();
const usage = document.getElementById('newAcceptableUsage').value.trim();
const notes = document.getElementById('newAcceptableNotes').value.trim();
if (!term || !meaning) {
alert('Term and meaning are required');
return;
}
try {
const response = await fetch(`${API_URL}/api/acceptable-terms`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
term,
meaning,
variants,
usage,
notes
})
});
if (response.ok) {
alert('Acceptable term added successfully');
// Clear form
document.getElementById('newAcceptableTerm').value = '';
document.getElementById('newAcceptableMeaning').value = '';
document.getElementById('newAcceptableVariants').value = '';
document.getElementById('newAcceptableUsage').value = '';
document.getElementById('newAcceptableNotes').value = '';
// Reload terms
loadAcceptableTerms();
} else {
const error = await response.json();
alert('Failed to add term: ' + error.error);
}
} catch (error) {
console.error('Error adding acceptable term:', error);
alert('Failed to add acceptable term');
}
}
// Delete banned term
async function deleteBannedTerm(encodedTerm) {
const term = decodeURIComponent(encodedTerm);
if (!confirm(`Delete banned term "${term}"?`)) return;
try {
const response = await fetch(`${API_URL}/api/banned-terms/${encodeURIComponent(term)}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
alert('Term deleted successfully');
loadBannedTerms();
} else {
alert('Failed to delete term');
}
} catch (error) {
console.error('Error deleting term:', error);
alert('Failed to delete term');
}
}
// Delete acceptable term
async function deleteAcceptableTerm(encodedTerm) {
const term = decodeURIComponent(encodedTerm);
if (!confirm(`Delete acceptable term "${term}"?`)) return;
try {
const response = await fetch(`${API_URL}/api/acceptable-terms/${encodeURIComponent(term)}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
alert('Term deleted successfully');
loadAcceptableTerms();
} else {
alert('Failed to delete term');
}
} catch (error) {
console.error('Error deleting term:', error);
alert('Failed to delete term');
}
}
// Filter Banned Terms
function filterBannedTerms() {
const categoryFilter = document.getElementById('categoryFilter').value;
const severityFilter = document.getElementById('severityFilter').value;
let filtered = allBannedTerms;
if (categoryFilter !== 'all') {
filtered = filtered.filter(term => term.category === categoryFilter);
}
if (severityFilter !== 'all') {
filtered = filtered.filter(term => term.severity === severityFilter);
}
displayBannedTerms(filtered);
}
// Load Active Sessions
async function loadActiveSessions() {
try {
const response = await fetch(`${API_URL}/api/live-chats`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const chats = await response.json();
// Populate active users from existing sessions
for (const chat of chats) {
if (!activeUsers.has(chat.userId)) {
activeUsers.set(chat.userId, {
userId: chat.userId,
userName: chat.userName || 'Anonymous',
userEmail: chat.userEmail || '',
ip: chat.ip,
mode: chat.mode,
lastActivity: chat.lastActivity,
isTyping: false,
lastMessage: ''
});
}
// Also populate live chats
if (!liveChats.has(chat.userId)) {
liveChats.set(chat.userId, {
userId: chat.userId,
userName: chat.userName || 'Anonymous',
userEmail: chat.userEmail || '',
ip: chat.ip,
mode: chat.mode,
messages: chat.messages || []
});
}
}
// Update displays
updateActiveUsersBar();
renderLiveChats();
}
} catch (error) {
console.error('Failed to load active sessions:', error);
}
}
// Logout
function logout() {
localStorage.removeItem('adminToken');
if (socket) socket.disconnect();
location.reload();
}
// Live Chat Functions
const liveChats = new Map();
const activeUsers = new Map();
function updateLiveChat(data) {
const { userId, userName, userEmail, ip, location, type, message, response, mode, timestamp } = data;
// Update or create chat entry
if (!liveChats.has(userId)) {
liveChats.set(userId, {
userId,
userName: userName || 'Anonymous',
userEmail: userEmail || '',
ip,
location: location || 'Unknown',
mode,
messages: []
});
}
const chat = liveChats.get(userId);
// Update user info if provided
if (userName) chat.userName = userName;
if (userEmail) chat.userEmail = userEmail;
if (location) chat.location = location;
// Update active users tracking
if (!activeUsers.has(userId)) {
activeUsers.set(userId, {
userId,
userName: userName || 'Anonymous',
userEmail: userEmail || '',
ip,
location: location || 'Unknown',
mode,
lastActivity: timestamp,
isTyping: false,
lastMessage: ''
});
}
const activeUser = activeUsers.get(userId);
activeUser.lastActivity = timestamp;
// Update user info if provided
if (userName) activeUser.userName = userName;
if (userEmail) activeUser.userEmail = userEmail;
if (location) activeUser.location = location;
if (type === 'user_typing') {
chat.messages.push({
type: 'user',
text: message,
timestamp
});
activeUser.isTyping = true;
activeUser.lastMessage = message;
// Clear typing after 3 seconds
setTimeout(() => {
activeUser.isTyping = false;
updateActiveUsersBar();
}, 3000);
} else if (type === 'bubbe_response') {
chat.messages.push({
type: 'bubbe',
text: response,
timestamp
});
activeUser.isTyping = false;
}
renderLiveChats();
updateActiveUsersBar();
}
function updateActiveUsersBar() {
const activeUsersLabel = document.getElementById('activeUsersLabel');
const activeUsersList = document.getElementById('activeUsersList');
// Remove inactive users (no activity for 5 minutes)
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
for (const [userId, user] of activeUsers.entries()) {
if (user.lastActivity < fiveMinutesAgo) {
activeUsers.delete(userId);
}
}
activeUsersLabel.textContent = `Active Users (${activeUsers.size})`;
if (activeUsers.size === 0) {
activeUsersList.innerHTML = '<span style="opacity: 0.7; font-size: 14px;">No active users currently</span>';
return;
}
let html = '';
for (const [userId, user] of activeUsers) {
const displayName = user.userName || 'Anonymous';
const initial = displayName.charAt(0).toUpperCase();
const timeSince = getTimeSince(user.lastActivity);
const displayEmail = user.userEmail ? ` (${user.userEmail})` : '';
const locationInfo = user.location ? ` | 📍 ${user.location}` : '';
html += `
<div class="active-user-badge" onclick="scrollToUserChat('${userId}')" title="${displayName}${displayEmail}${locationInfo} | IP: ${user.ip}">
<div class="user-avatar">${initial}</div>
<div class="user-info-text">
<span class="user-email-text">${displayName}</span>
<span class="user-status-text">
${user.isTyping ?
'Typing<span class="typing-indicator"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></span>' :
timeSince}
</span>
</div>
</div>
`;
}
activeUsersList.innerHTML = html;
}
function getTimeSince(timestamp) {
const now = new Date();
const then = new Date(timestamp);
const diffMs = now - then;
const diffSecs = Math.floor(diffMs / 1000);
if (diffSecs < 60) return 'Just now';
if (diffSecs < 120) return '1 min ago';
if (diffSecs < 3600) return `${Math.floor(diffSecs / 60)} mins ago`;
return `${Math.floor(diffSecs / 3600)} hours ago`;
}
window.scrollToUserChat = function(userId) {
// Scroll to the user's chat in the live chat container
const chatContainer = document.getElementById('liveChatsList');
if (chatContainer) {
chatContainer.scrollIntoView({ behavior: 'smooth' });
}
}
function renderLiveChats() {
const container = document.getElementById('liveChatsList');
const activeCount = liveChats.size;
document.getElementById('liveUserCount').textContent = `${activeCount} active`;
if (activeCount === 0) {
container.innerHTML = '<p style="color: #999; text-align: center;">No active conversations</p>';
return;
}
let html = '';
for (const [userId, chat] of liveChats) {
const lastMessages = chat.messages.slice(-5); // Show last 5 messages
const displayName = chat.userName || 'Anonymous';
const displayEmail = chat.userEmail ? ` (${chat.userEmail})` : '';
html += `
<div style="margin-bottom: 20px; border-bottom: 1px solid #eee; padding-bottom: 10px;">
<div style="font-weight: bold; margin-bottom: 10px; color: #667eea;">
👤 ${displayName}${displayEmail} | IP: ${chat.ip} | 📍 ${chat.location || 'Unknown'} | Mode: ${chat.mode}
</div>
`;
for (const msg of lastMessages) {
const className = msg.type === 'user' ? 'chat-user' : 'chat-bubbe';
const label = msg.type === 'user' ? `👤 ${displayName}` : '👵 Bubbe';
const messageId = `msg-${userId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
html += `
<div class="chat-message ${className}" id="${messageId}">
<div class="chat-header">
<strong>${label}</strong>
<span>${new Date(msg.timestamp).toLocaleTimeString()}</span>
</div>
<div class="chat-text">${msg.text}</div>
<div class="social-share-buttons" style="margin-top: 8px; display: flex; gap: 8px;">
<button onclick="shareToTwitter('${messageId}', '${label}', '${encodeURIComponent(msg.text).replace(/'/g, "\\'")}')"
class="social-btn twitter-btn" title="Share to Twitter/X">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</button>
<button onclick="shareToFacebook('${messageId}', '${encodeURIComponent(msg.text).replace(/'/g, "\\'")}')"
class="social-btn facebook-btn" title="Share to Facebook">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</button>
<button onclick="shareToLinkedIn('${messageId}', '${encodeURIComponent(msg.text).replace(/'/g, "\\'")}')"
class="social-btn linkedin-btn" title="Share to LinkedIn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</button>
<button onclick="copyMessage('${messageId}', '${label}', '${encodeURIComponent(msg.text).replace(/'/g, "\\'")}')"
class="social-btn copy-btn" title="Copy to clipboard">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
<button onclick="flagMessage('${messageId}', '${userId}', '${encodeURIComponent(msg.text).replace(/'/g, "\\'")}')"
class="social-btn flag-btn" title="Flag as problematic">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/>
</svg>
</button>
</div>
</div>
`;
}
html += '</div>';
}
container.innerHTML = html;
container.scrollTop = container.scrollHeight;
}
// User Management Functions
async function deactivateUser(email) {
if (!confirm(`Deactivate user ${email}?`)) return;
const reason = prompt('Enter reason for deactivation:');
if (!reason) return;
try {
const response = await fetch(`${API_URL}/api/users/${email}/status`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ active: false, reason })
});
if (response.ok) {
alert(`User ${email} has been deactivated`);
loadUsers();
} else {
alert('Failed to deactivate user');
}
} catch (error) {
console.error('Error:', error);
alert('Failed to deactivate user');
}
}
async function activateUser(email) {
if (!confirm(`Reactivate user ${email}?`)) return;
try {
const response = await fetch(`${API_URL}/api/users/${email}/status`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ active: true })
});
if (response.ok) {
alert(`User ${email} has been reactivated`);
loadUsers();
} else {
alert('Failed to activate user');
}
} catch (error) {
console.error('Error:', error);
alert('Failed to activate user');
}
}
async function deleteUser(email) {
if (!confirm(`⚠️ PERMANENTLY delete user ${email}? This cannot be undone!`)) return;
if (!confirm(`Are you absolutely sure? All user data will be deleted.`)) return;
const reason = prompt('Enter reason for deletion:');
if (!reason) return;
try {
const response = await fetch(`${API_URL}/api/users/${email}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ reason })
});
if (response.ok) {
alert(`User ${email} has been permanently deleted`);
loadUsers();
} else {
alert('Failed to delete user');
}
} catch (error) {
console.error('Error:', error);
alert('Failed to delete user');
}
}
async function blockUser(email) {
if (!confirm(`Block user ${email}?`)) return;
const reason = prompt('Enter reason for blocking:');
if (!reason) return;
try {
const response = await fetch(`${API_URL}/api/users/${email}/block`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ blocked: true, reason })
});
if (response.ok) {
alert(`User ${email} has been blocked`);
loadUsers();
} else {
alert('Failed to block user');
}
} catch (error) {
console.error('Error:', error);
alert('Failed to block user');
}
}
async function unblockUser(email) {
if (!confirm(`Unblock user ${email}?`)) return;
try {
const response = await fetch(`${API_URL}/api/users/${email}/block`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ blocked: false })
});
if (response.ok) {
alert(`User ${email} has been unblocked`);
loadUsers();
} else {
alert('Failed to unblock user');
}
} catch (error) {
console.error('Error:', error);
alert('Failed to unblock user');
}
}
// Initialize Google Sign-In
function initializeGoogleSignIn() {
if (typeof google !== 'undefined' && google.accounts && google.accounts.id) {
google.accounts.id.initialize({
client_id: '493480746821-mhpjc0glkkogcf845ubi2hkhksoqj39k.apps.googleusercontent.com',
callback: handleGoogleLogin,
auto_select: false,
cancel_on_tap_outside: false,
ux_mode: 'popup'
});
google.accounts.id.renderButton(
document.querySelector('.g_id_signin'),
{
theme: 'outline',
size: 'large',
text: 'signin_with',
shape: 'rectangular',
width: 250
}
);
} else {
// Retry after a delay if Google library hasn't loaded yet
setTimeout(initializeGoogleSignIn, 500);
}
}
// Social Media Sharing Functions with unique Bubbe intros
const bubbeIntros = [
"Bubbe tells it like it is",
"Bubbe's got no filter",
"Bubbe says what you're thinking",
"Bubbe drops truth bombs",
"Bubbe keeps it real",
"Bubbe's wisdom hits different",
"Bubbe doesn't sugarcoat",
"Bubbe serves reality checks",
"Bubbe's brutal honesty",
"Bubbe speaks facts",
"Bubbe's savage advice",
"Bubbe pulls no punches",
"Bubbe's hot take",
"Bubbe's unfiltered thoughts",
"Bubbe lays down the law",
"Bubbe's tough love",
"Bubbe's reality therapy",
"Bubbe's truth serum",
"Bubbe's straight talk",
"Bubbe's no-nonsense wisdom"
];
let introIndex = 0;
function getUniqueIntro() {
const intro = bubbeIntros[introIndex % bubbeIntros.length];
introIndex++;
return intro;
}
function shareToTwitter(messageId, label, text) {
const intro = getUniqueIntro();
const decodedText = decodeURIComponent(text);
const preview = decodedText.substring(0, 140);
const shareText = `${intro}: "${preview}..." #BubbeAI #Bubbe #NoFilter #JewishGrandma\n\nVisit Bubbe.AI`;
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}`;
window.open(url, '_blank', 'width=600,height=400');
}
function shareToFacebook(messageId, text) {
const intro = getUniqueIntro();
const decodedText = decodeURIComponent(text);
const preview = decodedText.substring(0, 250);
const shareText = `${intro}:\n\n"${preview}..."\n\n#BubbeAI #Bubbe #JewishWisdom #NoFilter\n\nVisit Bubbe.AI`;
const url = `https://www.facebook.com/sharer/sharer.php?quote=${encodeURIComponent(shareText)}`;
window.open(url, '_blank', 'width=600,height=400');
}
function shareToLinkedIn(messageId, text) {
const intro = getUniqueIntro();
const decodedText = decodeURIComponent(text);
const preview = decodedText.substring(0, 200);
const shareText = `${intro}:\n\n"${preview}..."\n\nAI-powered wisdom with zero sugar-coating. #BubbeAI #Bubbe #CustomerExperience #AIInnovation\n\nVisit Bubbe.AI`;
const url = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent('http://45.61.58.125:3011')}&summary=${encodeURIComponent(shareText)}`;
window.open(url, '_blank', 'width=600,height=400');
}
function copyMessage(messageId, label, text) {
const fullText = `${label}: ${decodeURIComponent(text)}`;
navigator.clipboard.writeText(fullText).then(() => {
const btn = event.target;
const originalText = btn.innerHTML;
btn.innerHTML = '✓ Copied!';
btn.style.background = '#28a745';
setTimeout(() => {
btn.innerHTML = originalText;
btn.style.background = '';
}, 2000);
}).catch(err => {
alert('Failed to copy text');
});
}
function flagMessage(messageId, userId, text) {
if (confirm('Flag this message as inappropriate or concerning?')) {
const flaggedMessages = JSON.parse(localStorage.getItem('flaggedMessages') || '[]');
flaggedMessages.push({
id: messageId,
userId: userId,
text: decodeURIComponent(text),
timestamp: new Date().toISOString(),
flaggedBy: 'Admin'
});
localStorage.setItem('flaggedMessages', JSON.stringify(flaggedMessages));
const messageElement = document.querySelector(`[data-message-id="${messageId}"]`);
if (messageElement) {
messageElement.style.border = '2px solid #dc3545';
messageElement.style.background = '#fff5f5';
}
showNotification('Message flagged for review', 'warning');
updateFlaggedCount();
}
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background: ${type === 'warning' ? '#ff9800' : type === 'error' ? '#dc3545' : '#28a745'};
color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 10000;
animation: slideIn 0.3s;
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
function updateFlaggedCount() {
const flaggedMessages = JSON.parse(localStorage.getItem('flaggedMessages') || '[]');
const badges = document.querySelectorAll('.flagged-count');
badges.forEach(badge => {
badge.textContent = flaggedMessages.length;
badge.style.display = flaggedMessages.length > 0 ? 'inline-block' : 'none';
});
}
function scrollToUserChat(userId) {
const chatSection = document.getElementById('liveChatsDisplay');
if (chatSection) {
const userChat = chatSection.querySelector(`[data-user-id="${userId}"]`);
if (userChat) {
userChat.scrollIntoView({ behavior: 'smooth', block: 'start' });
userChat.style.background = '#fffbf0';
setTimeout(() => {
userChat.style.background = '';
}, 2000);
}
}
}
// Export functions
function exportAlerts() {
const alerts = [];
const alertElements = document.querySelectorAll('#alertsList .alert-item');
alertElements.forEach(el => {
alerts.push({
type: el.querySelector('.alert-badge')?.textContent || '',
user: el.querySelector('strong')?.textContent || '',
message: el.querySelector('div[style*="color: #666"]')?.textContent || '',
timestamp: el.querySelector('div[style*="color: #999"]')?.textContent || ''
});
});
if (alerts.length === 0) {
showNotification('No alerts to export', 'warning');
return;
}
const csv = convertToCSV(alerts);
downloadCSV(csv, 'alerts_export.csv');
showNotification('Alerts exported successfully', 'success');
}
function exportUsers() {
fetch(`${API_URL}/api/users`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
})
.then(res => res.json())
.then(users => {
const csv = convertToCSV(users);
downloadCSV(csv, 'users_export.csv');
showNotification('Users exported successfully', 'success');
})
.catch(err => {
showNotification('Failed to export users', 'error');
});
}
function exportChats() {
const chats = Array.from(liveChats.values());
if (chats.length === 0) {
showNotification('No chats to export', 'warning');
return;
}
const exportData = chats.map(chat => ({
userId: chat.userId,
userName: chat.userName,
email: chat.userEmail,
ip: chat.ip,
location: chat.location,
messageCount: chat.messages.length,
lastMessage: chat.messages[chat.messages.length - 1]?.text || ''
}));
const csv = convertToCSV(exportData);
downloadCSV(csv, 'chats_export.csv');
showNotification('Chats exported successfully', 'success');
}
function convertToCSV(data) {
if (!data || data.length === 0) return '';
const headers = Object.keys(data[0]);
const csvHeaders = headers.join(',');
const csvRows = data.map(row =>
headers.map(header => {
const value = row[header];
const escaped = String(value || '').replace(/"/g, '""');
return escaped.includes(',') || escaped.includes('\n') ? `"${escaped}"` : escaped;
}).join(',')
);
return [csvHeaders, ...csvRows].join('\n');
}
function downloadCSV(csv, filename) {
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
function clearAlerts() {
if (confirm('Clear all alerts? This cannot be undone.')) {
document.getElementById('alertsList').innerHTML = '<div style="color: #666; text-align: center;">No alerts to display</div>';
document.getElementById('totalAlerts').textContent = '0';
showNotification('Alerts cleared', 'info');
}
}
function clearChats() {
if (confirm('Clear all chat history? This cannot be undone.')) {
liveChats.clear();
document.getElementById('liveChatsDisplay').innerHTML = '<div style="color: #666; text-align: center;">No active chats</div>';
showNotification('Chat history cleared', 'info');
}
}
function refreshDashboard() {
showNotification('Refreshing dashboard...', 'info');
loadDashboard();
}
function refreshAlerts() {
loadAlerts();
showNotification('Alerts refreshed', 'success');
}
function refreshUsers() {
loadUsers();
showNotification('Users refreshed', 'success');
}
function refreshChats() {
loadActiveSessions();
showNotification('Chats refreshed', 'success');
}
function generateReport() {
const report = {
generatedAt: new Date().toISOString(),
totalUsers: parseInt(document.getElementById('totalUsers').textContent) || 0,
activeUsers: parseInt(document.getElementById('activeUsers').textContent) || 0,
totalAlerts: parseInt(document.getElementById('totalAlerts').textContent) || 0,
blockedUsers: parseInt(document.getElementById('blockedUsers').textContent) || 0,
flaggedMessages: JSON.parse(localStorage.getItem('flaggedMessages') || '[]').length,
bannedTermsCount: allBannedTerms.length,
acceptableTermsCount: document.querySelectorAll('#acceptableTermsList > div').length
};
const reportText = `
DEAR BUBBE ADMIN REPORT
Generated: ${new Date().toLocaleString()}
========================================
STATISTICS:
- Total Users: ${report.totalUsers}
- Active Users: ${report.activeUsers}
- Total Alerts: ${report.totalAlerts}
- Blocked Users: ${report.blockedUsers}
- Flagged Messages: ${report.flaggedMessages}
CONTENT MODERATION:
- Banned Terms: ${report.bannedTermsCount}
- Acceptable Terms: ${report.acceptableTermsCount}
========================================
`.trim();
const blob = new Blob([reportText], { type: 'text/plain;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `bubbe_report_${Date.now()}.txt`;
link.click();
showNotification('Report generated successfully', 'success');
}
// Check for existing token on load
window.onload = function() {
const token = localStorage.getItem('adminToken');
if (token) {
authToken = token;
// Auto-login with stored token
// You might want to validate the token first
document.getElementById('loginScreen').style.display = 'none';
document.getElementById('dashboard').classList.add('active');
initDashboard();
}
// Initialize Google Sign-In
initializeGoogleSignIn();
}
</script>
<style>
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Live Chat Styles */
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.chat-message {
margin: 10px 0;
padding: 10px;
border-radius: 8px;
animation: slideIn 0.3s ease;
}
.chat-user {
background: #e3f2fd;
border-left: 3px solid #2196f3;
}
.chat-bubbe {
background: #fff3e0;
border-left: 3px solid #ff9800;
}
.chat-header {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 12px;
color: #666;
}
.chat-text {
color: #333;
word-wrap: break-word;
}
.user-actions {
display: flex;
gap: 5px;
}
.action-btn {
padding: 4px 8px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s;
}
.btn-deactivate {
background: #ffc107;
color: white;
}
.btn-delete {
background: #dc3545;
color: white;
}
.btn-activate {
background: #28a745;
color: white;
}
.btn-block {
background: #6c757d;
color: white;
}
.action-btn:hover {
opacity: 0.8;
transform: scale(1.05);
}
/* Active Users Bar Styles */
.active-user-badge {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.2);
padding: 5px 12px;
border-radius: 20px;
white-space: nowrap;
animation: slideIn 0.3s ease;
}
.active-user-badge:hover {
background: rgba(255, 255, 255, 0.3);
cursor: pointer;
}
.user-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: white;
color: #667eea;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 12px;
}
.user-info-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.user-email-text {
font-size: 12px;
font-weight: 500;
}
.user-status-text {
font-size: 10px;
opacity: 0.9;
}
@keyframes fadeInOut {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.typing-indicator {
display: inline-flex;
gap: 2px;
margin-left: 5px;
}
.typing-dot {
width: 3px;
height: 3px;
background: white;
border-radius: 50%;
animation: bounce 1.4s infinite;
}
.typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-5px);
}
}
/* Add padding to bottom of container to avoid overlap with active users bar */
.container {
padding-bottom: 80px;
}
/* Social Share Buttons */
.social-share-buttons {
opacity: 0.7;
transition: opacity 0.3s;
}
.chat-message:hover .social-share-buttons {
opacity: 1;
}
.social-btn {
padding: 4px 8px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.twitter-btn {
background: #1DA1F2;
color: white;
}
.twitter-btn:hover {
background: #1a91da;
transform: scale(1.1);
}
.facebook-btn {
background: #4267B2;
color: white;
}
.facebook-btn:hover {
background: #365899;
transform: scale(1.1);
}
.linkedin-btn {
background: #0077B5;
color: white;
}
.linkedin-btn:hover {
background: #006097;
transform: scale(1.1);
}
.copy-btn {
background: #6c757d;
color: white;
}
.copy-btn:hover {
background: #5a6268;
transform: scale(1.1);
}
.flag-btn {
background: #dc3545;
color: white;
}
.flag-btn:hover {
background: #c82333;
transform: scale(1.1);
}
.toast-notification {
position: fixed;
bottom: 100px;
right: 20px;
background: #333;
color: white;
padding: 12px 20px;
border-radius: 8px;
z-index: 10000;
animation: slideInRight 0.3s ease;
display: none;
}
.toast-notification.show {
display: block;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>
</body>
</html>