Agency Dashboard

Total Clients

Total Pages Generated

Agency:

Email:

Credits:

Clients

Name Email Pages Generated Credits

Direct Directory Inquiries

Name Email Phone Message Date

Assign Credits

Add New Client

Bulk Import Clients (CSV)

Expected format: name,email (no headers required, or with name,email headers)

Subscription

Status:

Plan: N/A

Monthly Credits: N/A

Renewal Date: N/A

Manage Subscription

Custom Branding

Custom Domain Configuration

White-label the generated page domain by mapping your own custom domain (e.g. seo.youragency.com) to your client pages.

Point a CNAME record in your DNS settings to localseogen.com.
Visitors to the root of your custom domain will be redirected here.

Embeddable Audit & Lead-Gen Widget

Capture high-quality local SEO leads directly on your agency website. Embed this interactive local SEO rank grid scanner form on your site; when prospects scan their website, their lead details are automatically added to your client pipeline.

Billing History

Date Credits Amount Status

1-Click Local SEO Audit Extension

Audit any prospect's website's local SEO reach in 1-click right from your browser. Find missed town opportunities and route them directly to the page generator.

Download Chrome Extension (.zip)

Installation Instructions:
1. Download and extract the ZIP file.
2. Open Chrome and navigate to chrome://extensions/.
3. Enable "Developer mode" in the top right.
4. Click "Load unpacked" and select the extracted folder.

`; codeOutput.value = html; } for (const radio of themeRadios) { radio.addEventListener('change', updateWidgetCode); } copyBtn.onclick = async () => { try { await navigator.clipboard.writeText(codeOutput.value); if (successMsg) { successMsg.style.display = 'inline'; setTimeout(() => { successMsg.style.display = 'none'; }, 2000); } } catch (err) { console.error('Failed to copy widget code: ', err); } }; updateWidgetCode(); } async function fetchBillingHistory() { const response = await fetch('/api/get-agency-billing-history'); if (response.ok) { const data = await response.json(); billingHistoryTableBody.innerHTML = ''; data.forEach(transaction => { const row = document.createElement('tr'); row.innerHTML = ` ${new Date(transaction.date).toLocaleDateString()}${transaction.credits}$${(transaction.amount / 100).toFixed(2)} ${transaction.currency.toUpperCase()}${transaction.payment_status} `; billingHistoryTableBody.appendChild(row); }); } } async function fetchDashboard() { const response = await fetch('/api/agency-dashboard'); if (response.ok) { const data = await response.json(); agencyName.textContent = data.agencyName; agencyEmail.textContent = data.email; agencyCredits.textContent = data.credits; subscriptionStatus.textContent = data.subscriptionStatus; subscriptionPlan.textContent = data.planName || 'N/A'; subscriptionMonthlyCredits.textContent = data.monthlyCredits !== undefined ? data.monthlyCredits : 'N/A'; subscriptionRenewalDate.textContent = data.renewalDate ? new Date(data.renewalDate).toLocaleDateString() : 'N/A'; totalClients.textContent = data.totalClients; totalPagesGenerated.textContent = data.totalPagesGenerated; if (data.logoUrl) { agencyLogoPreview.src = data.logoUrl; agencyLogoPreview.style.display = 'block'; } logoUrlInput.value = data.logoUrl || ''; primaryColorInput.value = data.primaryColor || '#000000'; const customDomainInput = document.getElementById('custom-domain-input'); const customDomainRedirectInput = document.getElementById('custom-domain-redirect-input'); if (customDomainInput) { customDomainInput.value = data.customDomain || ''; if (data.customDomain && data.customDomain.trim()) { setTimeout(() => { if (typeof updateDnsHealthStatusPanel === 'function') { updateDnsHealthStatusPanel(data.customDomain.trim()); } }, 500); } } if (customDomainRedirectInput) { customDomainRedirectInput.value = data.customDomainRedirect || ''; } const clientListBody = clientList.querySelector('tbody'); clientListBody.innerHTML = ''; clientSelect.innerHTML = ''; data.clients.forEach(client => { const clientRow = document.createElement('tr'); clientRow.innerHTML = ` ${client.name}${client.email}${client.pagesGenerated}${client.credits}View `; clientListBody.appendChild(clientRow); const option = document.createElement('option'); option.value = client.id; option.textContent = `${client.name} (${client.email})`; clientSelect.appendChild(option); }); // Render direct directory inquiries const directoryLeadsListBody = document.querySelector('#directory-leads-list tbody'); const directoryLeadsEmptyMsg = document.getElementById('directory-leads-empty-msg'); if (directoryLeadsListBody) { directoryLeadsListBody.innerHTML = ''; if (data.agencyLeads && data.agencyLeads.length > 0) { directoryLeadsEmptyMsg.style.display = 'none'; data.agencyLeads.forEach(lead => { const row = document.createElement('tr'); row.innerHTML = ` ${lead.name} ${lead.email} ${lead.phone || 'N/A'} ${lead.message || 'N/A'} ${new Date(lead.createdAt).toLocaleDateString()} `; directoryLeadsListBody.appendChild(row); }); } else { directoryLeadsEmptyMsg.style.display = 'block'; } } // Render partner badge option const partnerBadgeSection = document.getElementById('partner-badge-section'); const unclaimedBadgeSection = document.getElementById('unclaimed-badge-section'); if (data.hasClaimedProfile && data.agencySlug) { if (partnerBadgeSection) partnerBadgeSection.style.display = 'block'; if (unclaimedBadgeSection) unclaimedBadgeSection.style.display = 'none'; initializePartnerBadges(data.agencySlug); } else { if (partnerBadgeSection) partnerBadgeSection.style.display = 'none'; if (unclaimedBadgeSection) unclaimedBadgeSection.style.display = 'block'; } initializeLeadGenWidget(data.userId); } else { window.location.href = 'agency-login.html'; } } addClientForm.addEventListener('submit', async (e) => { e.preventDefault(); errorMessage.textContent = ''; const clientName = document.getElementById('client-name').value; const clientEmail = document.getElementById('client-email').value; const response = await fetch('/api/add-client', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientName, clientEmail }), }); if (response.ok) { fetchDashboard(); addClientForm.reset(); } else { const { message } = await response.json(); errorMessage.textContent = message; } }); const bulkImportForm = document.getElementById('bulk-import-form'); const bulkImportMessage = document.getElementById('bulk-import-message'); const bulkImportSubmitBtn = document.getElementById('bulk-import-submit-btn'); bulkImportForm.addEventListener('submit', async (e) => { e.preventDefault(); bulkImportMessage.textContent = ''; bulkImportMessage.style.color = ''; const fileInput = document.getElementById('bulk-csv-file'); const file = fileInput.files[0]; if (!file) { bulkImportMessage.textContent = 'Please select a CSV file first.'; bulkImportMessage.style.color = 'red'; return; } bulkImportSubmitBtn.disabled = true; bulkImportSubmitBtn.textContent = 'Importing...'; const reader = new FileReader(); reader.onload = async function(event) { const text = event.target.result; const clients = []; const lines = text.split(/\r?\n/); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const parts = line.split(','); if (parts.length < 2) continue; const name = parts[0].replace(/^["']|["']$/g, '').trim(); const email = parts[1].replace(/^["']|["']$/g, '').trim(); // Skip header if it is exactly name/email if (i === 0 && name.toLowerCase() === 'name' && email.toLowerCase() === 'email') { continue; } if (name && email) { clients.push({ name, email }); } } if (clients.length === 0) { bulkImportMessage.textContent = 'No valid client records found in CSV file.'; bulkImportMessage.style.color = 'red'; bulkImportSubmitBtn.disabled = false; bulkImportSubmitBtn.textContent = 'Bulk Import'; return; } try { const response = await fetch('/api/bulk-import-clients', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clients }), }); const result = await response.json(); if (response.ok) { bulkImportMessage.textContent = `Import complete! Imported: ${result.importedCount}, Skipped: ${result.skippedCount}, Failed: ${result.failedCount}.`; bulkImportMessage.style.color = 'green'; fetchDashboard(); bulkImportForm.reset(); } else { bulkImportMessage.textContent = result.message || 'An error occurred during import.'; bulkImportMessage.style.color = 'red'; } } catch (error) { console.error('Bulk import error:', error); bulkImportMessage.textContent = 'Failed to connect to the import API.'; bulkImportMessage.style.color = 'red'; } finally { bulkImportSubmitBtn.disabled = false; bulkImportSubmitBtn.textContent = 'Bulk Import'; } }; reader.readAsText(file); }); function convertToWebP(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { const img = new Image(); img.onload = function() { const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const webpDataUrl = canvas.toDataURL('image/webp', 0.82); resolve(webpDataUrl); }; img.onerror = function(err) { reject(new Error("Failed to load image for WebP conversion.")); }; img.src = e.target.result; }; reader.onerror = function(err) { reject(err); }; reader.readAsDataURL(file); }); } logoFileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; if (file) { try { const webpDataUrl = await convertToWebP(file); agencyLogoPreview.src = webpDataUrl; agencyLogoPreview.style.display = 'block'; logoUrlInput.value = ''; } catch (err) { console.error("WebP conversion failed, falling back to original:", err); const reader = new FileReader(); reader.onload = function(event) { agencyLogoPreview.src = event.target.result; agencyLogoPreview.style.display = 'block'; logoUrlInput.value = ''; }; reader.readAsDataURL(file); } } }); logoUrlInput.addEventListener('input', (e) => { const url = e.target.value; if (url) { agencyLogoPreview.src = url; agencyLogoPreview.style.display = 'block'; logoFileInput.value = ''; } else { agencyLogoPreview.style.display = 'none'; } }); brandingForm.addEventListener('submit', async(e) => { e.preventDefault(); brandingSuccessMessage.textContent = ''; errorMessage.textContent = ''; let logoUrl = logoUrlInput.value; const primaryColor = primaryColorInput.value; const file = logoFileInput.files[0]; if (file) { try { logoUrl = await convertToWebP(file); } catch (fileError) { console.error("Error reading file:", fileError); errorMessage.textContent = "Error reading selected image file."; return; } } const response = await fetch('/api/update-agency-branding', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ logoUrl, primaryColor }), }); if (response.ok) { brandingSuccessMessage.textContent = "Branding updated successfully!"; if (logoUrl) { agencyLogoPreview.src = logoUrl; agencyLogoPreview.style.display = 'block'; if (!file) { logoFileInput.value = ''; } } else { agencyLogoPreview.style.display = 'none'; } } else { const { message } = await response.json(); errorMessage.textContent = message; } }); assignCreditsForm.addEventListener('submit', async(e) => { e.preventDefault(); assignCreditsSuccessMessage.textContent = ''; errorMessage.textContent = ''; const clientId = clientSelect.value; const credits = creditsToAssignInput.value; const response = await fetch('/api/assign-credits', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId, credits }), }); if (response.ok) { assignCreditsSuccessMessage.textContent = "Credits assigned successfully!"; fetchDashboard(); assignCreditsForm.reset(); } else { const { message } = await response.json(); errorMessage.textContent = message; } }); const customDomainForm = document.getElementById('custom-domain-form'); const customDomainSuccessMsg = document.getElementById('custom-domain-success-msg'); const customDomainErrorMsg = document.getElementById('custom-domain-error-msg'); if (customDomainForm) { customDomainForm.addEventListener('submit', async (e) => { e.preventDefault(); if (customDomainSuccessMsg) customDomainSuccessMsg.style.display = 'none'; if (customDomainErrorMsg) customDomainErrorMsg.style.display = 'none'; const customDomain = document.getElementById('custom-domain-input').value; const customDomainRedirect = document.getElementById('custom-domain-redirect-input').value; try { const response = await fetch('/api/update-custom-domain', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ customDomain, customDomainRedirect }), }); const resData = await response.json(); if (response.ok) { if (customDomainSuccessMsg) { customDomainSuccessMsg.textContent = resData.message; customDomainSuccessMsg.style.display = 'block'; } } else { if (customDomainErrorMsg) { customDomainErrorMsg.textContent = resData.message || 'Failed to update custom domain settings.'; customDomainErrorMsg.style.display = 'block'; } } } catch (error) { console.error('Error updating custom domain:', error); if (customDomainErrorMsg) { customDomainErrorMsg.textContent = 'An unexpected error occurred.'; customDomainErrorMsg.style.display = 'block'; } } }); } async function updateDnsHealthStatusPanel(domain) { const panel = document.getElementById('dns-health-status-panel'); const dnsMappingStatus = document.getElementById('health-dns-mapping-status'); const dnsResolutionStatus = document.getElementById('health-dns-resolution-status'); const sslStatus = document.getElementById('health-ssl-status'); const warningsContainer = document.getElementById('health-warnings-container'); const warningsText = document.getElementById('health-warnings-text'); const heartIcon = document.getElementById('health-panel-heart-icon'); if (!panel) return; panel.style.display = 'block'; dnsMappingStatus.textContent = 'Checking...'; dnsMappingStatus.style.color = '#94a3b8'; dnsResolutionStatus.textContent = 'Checking...'; dnsResolutionStatus.style.color = '#94a3b8'; sslStatus.textContent = 'Checking...'; sslStatus.style.color = '#94a3b8'; warningsContainer.style.display = 'none'; if (heartIcon) { heartIcon.className = 'fas fa-heartbeat fa-spin'; heartIcon.style.color = '#3b82f6'; } try { const response = await fetch('/api/verify-dns', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain }) }); const resData = await response.json(); // DNS Mapping if (resData.verified) { dnsMappingStatus.textContent = 'Verified'; dnsMappingStatus.style.color = '#10b981'; } else { dnsMappingStatus.textContent = 'Failed'; dnsMappingStatus.style.color = '#ef4444'; } // DNS Resolution if (resData.dnsResolved) { dnsResolutionStatus.textContent = 'Resolving'; dnsResolutionStatus.style.color = '#10b981'; } else { dnsResolutionStatus.textContent = 'Not Resolving'; dnsResolutionStatus.style.color = '#ef4444'; } // SSL Status if (resData.sslVerified) { sslStatus.textContent = 'Active & Valid'; sslStatus.style.color = '#10b981'; } else { sslStatus.textContent = resData.dnsResolved ? 'Invalid' : 'Pending'; sslStatus.style.color = resData.dnsResolved ? '#ef4444' : '#f59e0b'; } // Warnings let warningMsg = resData.resolutionWarning; if (!resData.verified && !warningMsg) { warningMsg = `DNS verification failed. Point a CNAME record to 'localseogen.com'.`; } if (resData.dnsResolved && !resData.sslVerified) { const sslErrDetail = resData.sslError ? ` (${resData.sslError})` : ''; warningMsg = (warningMsg ? warningMsg + ' ' : '') + `SSL certificate verification failed${sslErrDetail}. Vercel may still be provisioning it, or DNS propagation is pending.`; } if (warningMsg) { warningsText.textContent = warningMsg; warningsContainer.style.display = 'block'; if (heartIcon) heartIcon.style.color = '#f59e0b'; } else { if (heartIcon) heartIcon.style.color = '#10b981'; } } catch (error) { console.error('Error updating DNS health status panel:', error); dnsMappingStatus.textContent = 'Error'; dnsMappingStatus.style.color = '#ef4444'; dnsResolutionStatus.textContent = 'Error'; dnsResolutionStatus.style.color = '#ef4444'; sslStatus.textContent = 'Error'; sslStatus.style.color = '#ef4444'; warningsText.textContent = 'Failed to fetch DNS health status details. Connection error.'; warningsContainer.style.display = 'block'; if (heartIcon) heartIcon.style.color = '#ef4444'; } finally { if (heartIcon) heartIcon.classList.remove('fa-spin'); } } const verifyDnsBtn = document.getElementById('verify-dns-btn'); if (verifyDnsBtn) { verifyDnsBtn.addEventListener('click', async () => { if (customDomainSuccessMsg) customDomainSuccessMsg.style.display = 'none'; if (customDomainErrorMsg) customDomainErrorMsg.style.display = 'none'; const customDomain = document.getElementById('custom-domain-input').value; if (!customDomain || !customDomain.trim()) { if (customDomainErrorMsg) { customDomainErrorMsg.textContent = 'Please enter a custom domain name first.'; customDomainErrorMsg.style.display = 'block'; } return; } const originalBtnText = verifyDnsBtn.textContent; verifyDnsBtn.textContent = 'Verifying...'; verifyDnsBtn.disabled = true; try { await updateDnsHealthStatusPanel(customDomain.trim()); const dnsMappingStatus = document.getElementById('health-dns-mapping-status'); if (dnsMappingStatus && dnsMappingStatus.textContent === 'Verified') { if (customDomainSuccessMsg) { customDomainSuccessMsg.textContent = 'DNS configuration verified successfully!'; customDomainSuccessMsg.style.display = 'block'; } } else { if (customDomainErrorMsg) { customDomainErrorMsg.textContent = 'DNS verification failed. See the health status panel below for diagnostics.'; customDomainErrorMsg.style.display = 'block'; } } } catch (error) { console.error('Error verifying DNS:', error); if (customDomainErrorMsg) { customDomainErrorMsg.textContent = 'An error occurred during DNS verification.'; customDomainErrorMsg.style.display = 'block'; } } finally { verifyDnsBtn.textContent = originalBtnText; verifyDnsBtn.disabled = false; } }); } refreshBillingHistoryButton.addEventListener('click', fetchBillingHistory); document.getElementById('logout-link').addEventListener('click', async () => { await fetch('/api/logout', { method: 'POST' }); window.location.href = 'index.html'; }); fetchDashboard(); fetchBillingHistory();