const API_BASE = 'https://hermes.squaremcp.com'; // State let currentUser = null; let isAdmin = false; // DOM refs const loginView = document.getElementById('login-view'); const resetRequestView = document.getElementById('reset-request-view'); const resetConfirmView = document.getElementById('reset-confirm-view'); const dashboardView = document.getElementById('dashboard-view'); const loginForm = document.getElementById('login-form'); const signupForm = document.getElementById('signup-form'); const resetRequestForm = document.getElementById('reset-request-form'); const resetConfirmForm = document.getElementById('reset-confirm-form'); const loginError = document.getElementById('login-error'); const signupError = document.getElementById('signup-error'); const tabBtns = document.querySelectorAll('.tab-btn'); const userEmail = document.getElementById('user-email'); const logoutBtn = document.getElementById('logout-btn'); const modal = document.getElementById('connect-modal'); const modalBody = document.getElementById('modal-body'); const modalClose = document.querySelector('.modal-close'); const modalBackdrop = document.querySelector('.modal-backdrop'); const navLinks = document.querySelectorAll('.nav-link'); const platformGrid = document.querySelector('.platform-grid'); const analyticsSection = document.getElementById('analytics-section'); const invoicesSection = document.getElementById('invoices-section'); const adminSection = document.getElementById('admin-section'); const adminNav = document.getElementById('admin-nav'); let platformChartInstance = null; let dailyChartInstance = null; // Platform config const PLATFORM_CONFIG = { tiktok: { name: 'TikTok', type: 'oauth', oauthUrl: 'https://tiktok.squaremcp.com/auth/tiktok/start', scopes: 'user.info.basic,user.info.profile,user.info.stats,video.list,video.publish', }, facebook: { name: 'Facebook', type: 'token', fields: [ { key: 'accessToken', label: 'Page Access Token', type: 'password' }, { key: 'pageId', label: 'Page ID', type: 'text' }, ], help: 'Get your Page Access Token from the Facebook Graph API Explorer. Run GET /me/accounts and copy the access_token for your page.', }, instagram: { name: 'Instagram', type: 'token', fields: [ { key: 'accessToken', label: 'Instagram Access Token', type: 'password' }, { key: 'businessAccountId', label: 'Business Account ID', type: 'text' }, ], help: 'Your Instagram account must be a Business/Creator account connected to a Facebook Page. Get the token from Graph API Explorer with instagram_basic scope.', }, linkedin: { name: 'LinkedIn', type: 'token', fields: [ { key: 'accessToken', label: 'LinkedIn Access Token', type: 'password' }, ], help: 'Generate an access token from the LinkedIn Developer Portal with w_member_social scope.', }, twitter: { name: 'Twitter / X', type: 'token', fields: [ { key: 'accessToken', label: 'Access Token', type: 'password' }, { key: 'accessTokenSecret', label: 'Access Token Secret', type: 'password' }, { key: 'apiKey', label: 'API Key', type: 'password' }, { key: 'apiSecret', label: 'API Secret', type: 'password' }, ], help: 'Get all four values from the Twitter Developer Portal under Keys and Tokens.', }, telegram: { name: 'Telegram', type: 'token', fields: [ { key: 'accessToken', label: 'Bot Token', type: 'password' }, ], help: 'Message @BotFather on Telegram and create a new bot. Copy the token it gives you.', }, discord: { name: 'Discord', type: 'token', fields: [ { key: 'accessToken', label: 'Bot Token', type: 'password' }, ], help: 'Go to discord.com/developers/applications → New Application → Bot → Reset Token.', }, whatsapp: { name: 'WhatsApp', type: 'token', fields: [ { key: 'accessToken', label: 'Access Token', type: 'password' }, { key: 'phoneNumberId', label: 'Phone Number ID', type: 'text' }, { key: 'businessAccountId', label: 'Business Account ID', type: 'text' }, ], help: 'From Meta Business Settings → WhatsApp → API Setup. You need a verified business phone number.', }, email: { name: 'Email', type: 'token', fields: [ { key: 'host', label: 'IMAP Host', type: 'text' }, { key: 'port', label: 'IMAP Port', type: 'text' }, { key: 'user', label: 'Email Address', type: 'email' }, { key: 'password', label: 'App Password', type: 'password' }, { key: 'smtpHost', label: 'SMTP Host', type: 'text' }, { key: 'smtpPort', label: 'SMTP Port', type: 'text' }, ], help: 'Use an app-specific password (not your login password). Common hosts: Gmail=smtp.gmail.com, Yahoo=smtp.mail.yahoo.com.', }, }; // Auth helpers async function apiPost(path, body) { const res = await fetch(`${API_BASE}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(body), }); return res.json(); } async function apiGet(path) { const res = await fetch(`${API_BASE}${path}`, { credentials: 'include' }); return res.json(); } // Views function hideAllViews() { loginView.classList.add('hidden'); resetRequestView.classList.add('hidden'); resetConfirmView.classList.add('hidden'); dashboardView.classList.add('hidden'); } function showLogin() { hideAllViews(); loginView.classList.remove('hidden'); } function showDashboard() { hideAllViews(); dashboardView.classList.remove('hidden'); userEmail.textContent = currentUser?.email || ''; updateConnectionStatus(); updateUsage(); loadInvoices(); if (isAdmin) loadAdminPanel(); } // Tab switching tabBtns.forEach(btn => { btn.addEventListener('click', () => { tabBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); const tab = btn.dataset.tab; if (tab === 'login') { loginForm.classList.remove('hidden'); signupForm.classList.add('hidden'); } else { loginForm.classList.add('hidden'); signupForm.classList.remove('hidden'); } }); }); // Navigation navLinks.forEach(link => { link.addEventListener('click', () => { navLinks.forEach(l => l.classList.remove('active')); link.classList.add('active'); const view = link.dataset.view; platformGrid.classList.toggle('hidden', view !== 'platforms'); document.querySelector('.welcome').classList.toggle('hidden', view !== 'platforms'); document.querySelector('.usage-bar').classList.toggle('hidden', view !== 'platforms'); analyticsSection.classList.toggle('hidden', view !== 'analytics'); invoicesSection.classList.toggle('hidden', view !== 'invoices'); adminSection.classList.toggle('hidden', view !== 'admin'); document.getElementById('webhooks-section').classList.toggle('hidden', view !== 'webhooks'); if (view === 'analytics') loadAnalytics(); if (view === 'invoices') loadInvoices(); if (view === 'admin') loadAdminPanel(); if (view === 'webhooks') loadWebhookConfig(); }); }); // Login loginForm.addEventListener('submit', async (e) => { e.preventDefault(); loginError.textContent = ''; const fd = new FormData(loginForm); const data = await apiPost('/api/auth/login', { email: fd.get('email'), password: fd.get('password'), }); if (data.error) { loginError.textContent = data.error; return; } currentUser = data; isAdmin = data.role === 'admin'; // If we were sent here from an OAuth flow, redirect back const returnTo = new URLSearchParams(window.location.search).get('return_to'); if (returnTo && returnTo.startsWith('https://hermes.squaremcp.com/')) { window.location.href = returnTo; return; } showDashboard(); }); // Signup signupForm.addEventListener('submit', async (e) => { e.preventDefault(); signupError.textContent = ''; const fd = new FormData(signupForm); const data = await apiPost('/api/auth/signup', { email: fd.get('email'), password: fd.get('password'), }); if (data.error) { signupError.textContent = data.error; return; } currentUser = data; isAdmin = false; showDashboard(); }); // Logout logoutBtn.addEventListener('click', async () => { await apiPost('/api/auth/logout', {}); currentUser = null; isAdmin = false; showLogin(); }); // Connect MCP Client — start the browser OAuth flow document.getElementById('connect-mcp-btn')?.addEventListener('click', () => { window.open(`${API_BASE}/oauth/connect-mcp`, '_blank', 'width=560,height=600,noopener'); }); // Password reset request resetRequestForm?.addEventListener('submit', async (e) => { e.preventDefault(); const errorEl = document.getElementById('reset-request-error'); const successEl = document.getElementById('reset-request-success'); errorEl.textContent = ''; successEl.textContent = ''; const fd = new FormData(resetRequestForm); const data = await apiPost('/api/auth/forgot-password', { email: fd.get('email') }); if (data.resetUrl) { successEl.textContent = `Reset link: ${data.resetUrl}`; } else { successEl.textContent = data.message; } }); document.getElementById('back-to-login')?.addEventListener('click', (e) => { e.preventDefault(); showLogin(); }); // Password reset confirm resetConfirmForm?.addEventListener('submit', async (e) => { e.preventDefault(); const errorEl = document.getElementById('reset-confirm-error'); const successEl = document.getElementById('reset-confirm-success'); errorEl.textContent = ''; successEl.textContent = ''; const fd = new FormData(resetConfirmForm); const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get('token'); if (!token) { errorEl.textContent = 'Missing reset token'; return; } const data = await apiPost('/api/auth/reset-password', { token, password: fd.get('password'), }); if (data.error) { errorEl.textContent = data.error; return; } successEl.textContent = 'Password updated! Redirecting to login...'; setTimeout(() => { window.location.href = '/'; }, 2000); }); // Add forgot password link to login form const forgotLink = document.createElement('a'); forgotLink.href = '#'; forgotLink.textContent = 'Forgot password?'; forgotLink.style.cssText = 'color:#888;font-size:13px;text-align:center;display:block;margin-top:8px;'; forgotLink.addEventListener('click', (e) => { e.preventDefault(); hideAllViews(); resetRequestView.classList.remove('hidden'); }); loginForm.appendChild(forgotLink); // Connection status async function updateConnectionStatus() { try { const [connData, healthData] = await Promise.all([ apiGet('/api/connections').catch(() => ({ connections: {} })), apiGet('/api/health/platforms').catch(() => ({ health: [] })), ]); const connections = connData.connections || {}; const healthMap = {}; (healthData.health || []).forEach(h => { healthMap[h.platform] = h.status; }); document.querySelectorAll('.platform-card').forEach(card => { const platform = card.dataset.platform; const badge = card.querySelector('.status-badge'); const btn = card.querySelector('.btn-connect'); const health = healthMap[platform]; if (health === 'healthy') { badge.textContent = 'Connected'; badge.className = 'status-badge connected'; btn.textContent = 'Manage'; } else if (health === 'expired') { badge.textContent = 'Token expired'; badge.className = 'status-badge expired'; btn.textContent = 'Reconnect'; } else if (connections[platform]) { badge.textContent = 'Connected'; badge.className = 'status-badge connected'; btn.textContent = 'Manage'; } else { badge.textContent = 'Not connected'; badge.className = 'status-badge disconnected'; btn.textContent = 'Connect'; } }); } catch { // ignore } } // Usage async function updateUsage() { try { const data = await apiGet('/api/usage'); const badge = document.getElementById('plan-badge'); const text = document.getElementById('usage-text'); const fill = document.getElementById('usage-bar-fill'); badge.textContent = data.plan; if (data.monthlyLimit === -1) { text.textContent = `${data.used} calls (unlimited)`; fill.style.width = '5%'; fill.className = 'usage-bar-fill'; } else { text.textContent = `${data.used} / ${data.monthlyLimit} calls this month`; const pct = Math.min(100, (data.used / data.monthlyLimit) * 100); fill.style.width = pct + '%'; fill.className = 'usage-bar-fill' + (pct > 90 ? ' danger' : pct > 70 ? ' warning' : ''); } } catch { // ignore } } // Invoices async function loadInvoices() { try { const data = await apiGet('/api/invoices'); const list = document.getElementById('invoices-list'); const invoices = data.invoices || []; if (!invoices.length) { list.innerHTML = '

No invoices yet.

'; return; } list.innerHTML = invoices.map(inv => `
${inv.invoice_number}
${inv.period_start} → ${inv.period_end}
$${inv.amount}
${inv.status}
`).join(''); } catch { document.getElementById('invoices-list').innerHTML = '

Failed to load invoices.

'; } } // Analytics const PLATFORM_COLORS = { email: '#ea4335', linkedin: '#0a66c2', twitter: '#000000', instagram: '#e1306c', facebook: '#1877f2', tiktok: '#010101', whatsapp: '#25d366', telegram: '#0088cc', discord: '#5865f2', snapchat: '#fffc00', obsidian: '#7c3aed', }; async function loadAnalytics() { const emptyEl = document.getElementById('analytics-empty'); try { const [usageData, dailyData] = await Promise.all([ apiGet('/api/usage'), apiGet('/api/usage/daily'), ]); const breakdown = usageData.breakdown || {}; const daily = dailyData.daily || []; const hasData = Object.keys(breakdown).length > 0 || daily.length > 0; emptyEl.classList.toggle('hidden', hasData); document.querySelector('.charts-grid').classList.toggle('hidden', !hasData); if (!hasData) return; // Platform breakdown chart const platformLabels = Object.keys(breakdown); const platformCounts = platformLabels.map(p => breakdown[p]); const platformColors = platformLabels.map(p => PLATFORM_COLORS[p] || '#888'); if (platformChartInstance) platformChartInstance.destroy(); platformChartInstance = new Chart(document.getElementById('platform-chart'), { type: 'bar', data: { labels: platformLabels, datasets: [{ label: 'Calls', data: platformCounts, backgroundColor: platformColors, borderRadius: 6, }], }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#888' }, grid: { color: '#2a2a2b' } }, y: { ticks: { color: '#e5e5e5' }, grid: { display: false } }, }, }, }); // Daily activity chart const dailyLabels = daily.map(d => d.date); const dailyCounts = daily.map(d => Number(d.count)); if (dailyChartInstance) dailyChartInstance.destroy(); dailyChartInstance = new Chart(document.getElementById('daily-chart'), { type: 'bar', data: { labels: dailyLabels, datasets: [{ label: 'Calls', data: dailyCounts, backgroundColor: '#10a37f', borderRadius: 4, }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#888', maxRotation: 45 }, grid: { color: '#2a2a2b' } }, y: { ticks: { color: '#888' }, grid: { color: '#2a2a2b' }, beginAtZero: true }, }, }, }); } catch { emptyEl.classList.remove('hidden'); emptyEl.querySelector('p').textContent = 'Failed to load analytics.'; document.querySelector('.charts-grid').classList.add('hidden'); } } // Admin panel async function loadAdminPanel() { try { const data = await apiGet('/api/admin/customers'); const container = document.getElementById('admin-customers'); const customers = data.customers || []; container.innerHTML = ` ${customers.map(c => ` `).join('')}
EmailPlanRoleActiveJoinedActions
${c.email} ${c.plan} ${c.role} ${c.active ? 'Yes' : 'No'} ${new Date(c.created_at).toLocaleDateString()}
`; } catch { document.getElementById('admin-customers').innerHTML = '

Failed to load admin data.

'; } } window.generateInvoice = async function(customerId) { try { const res = await fetch(`${API_BASE}/api/admin/customers/${customerId}/invoice`, { method: 'POST', credentials: 'include', }); const data = await res.json(); if (data.error) { alert(data.error); return; } alert(`Invoice ${data.invoice_number} created for $${data.amount}`); loadAdminPanel(); } catch { alert('Failed to generate invoice'); } }; // Modal function openModal(html) { modalBody.innerHTML = html; modal.classList.remove('hidden'); } function closeModal() { modal.classList.add('hidden'); modalBody.innerHTML = ''; } modalClose.addEventListener('click', closeModal); modalBackdrop.addEventListener('click', closeModal); // Platform connection function renderConnectForm(platform) { const cfg = PLATFORM_CONFIG[platform]; if (!cfg) return ''; if (cfg.type === 'oauth') { const state = btoa(JSON.stringify({ platform, user: currentUser?.id })); const url = `${cfg.oauthUrl}?state=${encodeURIComponent(state)}&scope=${encodeURIComponent(cfg.scopes)}`; return `

Connect ${cfg.name}

You'll be redirected to ${cfg.name} to authorize SquareMCP.

${cfg.help || ''}
Connect with ${cfg.name}
`; } const fieldsHtml = cfg.fields.map(f => ` `).join(''); return `

Connect ${cfg.name}

Paste your credentials below. They are encrypted and stored securely.

${fieldsHtml}
${cfg.help}

`; } document.querySelectorAll('.btn-connect').forEach(btn => { btn.addEventListener('click', () => { const platform = btn.dataset.platform; openModal(renderConnectForm(platform)); const form = modalBody.querySelector('form.connect-form'); if (form) { form.addEventListener('submit', async (e) => { e.preventDefault(); const errorEl = document.getElementById('connect-error'); errorEl.textContent = ''; errorEl.style.color = '#dc2626'; const fd = new FormData(form); const credentials = {}; for (const [key, value] of fd.entries()) { credentials[key] = value; } try { const res = await fetch(`${API_BASE}/api/connect/${platform}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(credentials), }); const data = await res.json(); if (data.error) { errorEl.textContent = data.error; return; } errorEl.textContent = 'Connected successfully!'; errorEl.style.color = '#10a37f'; setTimeout(() => { closeModal(); updateConnectionStatus(); }, 1000); } catch (err) { errorEl.textContent = 'Network error. Please try again.'; } }); } }); }); // Check session on load async function checkSession() { // Check for reset token in URL const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('token')) { hideAllViews(); resetConfirmView.classList.remove('hidden'); return; } // If we were sent here from an OAuth flow, redirect back after confirming session const returnTo = urlParams.get('return_to'); try { const data = await apiGet('/api/auth/me'); if (data.id) { currentUser = data; isAdmin = data.role === 'admin'; if (isAdmin) adminNav.classList.remove('hidden'); // Already logged in — bounce back to the OAuth authorize URL if present if (returnTo && returnTo.startsWith('https://hermes.squaremcp.com/')) { window.location.href = returnTo; return; } showDashboard(); } else { showLogin(); } } catch { showLogin(); } } // Webhook config async function loadWebhookConfig() { try { const data = await apiGet('/api/webhooks/config'); const display = document.getElementById('webhook-url-display'); const deleteBtn = document.getElementById('webhook-delete-btn'); const input = document.getElementById('webhook-url-input'); if (data.webhookUrl) { display.textContent = data.webhookUrl; deleteBtn.classList.remove('hidden'); input.value = data.webhookUrl; } else { display.textContent = 'No webhook configured'; deleteBtn.classList.add('hidden'); input.value = ''; } document.getElementById('webhook-secret-box').classList.add('hidden'); } catch { // ignore } } document.getElementById('webhook-form')?.addEventListener('submit', async (e) => { e.preventDefault(); const url = document.getElementById('webhook-url-input').value.trim(); const data = await apiPost('/api/webhooks/config', { webhook_url: url }); if (data.error) { alert(data.error); return; } document.getElementById('webhook-url-display').textContent = data.webhookUrl; document.getElementById('webhook-delete-btn').classList.remove('hidden'); if (data.webhookSecret) { document.getElementById('webhook-secret-value').textContent = data.webhookSecret; document.getElementById('webhook-secret-box').classList.remove('hidden'); } }); document.getElementById('webhook-delete-btn')?.addEventListener('click', async () => { if (!confirm('Remove webhook? This cannot be undone.')) return; await fetch(`${API_BASE}/api/webhooks/config`, { method: 'DELETE', credentials: 'include' }); loadWebhookConfig(); }); window.copyWebhookSecret = () => { const val = document.getElementById('webhook-secret-value').textContent; navigator.clipboard.writeText(val).then(() => alert('Secret copied!')); }; // Init checkSession();