sunnyware / static /admin.html
shamiur's picture
Part 200: fix Paddle mode detection (sandbox vs live vs simulated)
cd5ff40
Raw History Blame Contribute Delete
17.3 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Sunnyware - SaaS Control Plane</title>
<style>
:root {
--bg: #0b0d12; --sidebar: #12161f; --panel: #12161f; --border: #232a38;
--text: #e7ecf3; --muted: #8892a6; --accent: #5b8def; --danger: #ef5b5b;
--success: #2ecc71; --warn: #f1c40f;
}
* { box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font: 14px/1.5 system-ui, -apple-system, sans-serif; margin: 0; display: flex; height: 100vh; overflow: hidden; }
/* Sidebar */
.sidebar { width: 240px; background: var(--sidebar); border-right: 1px solid var(--border); display: flex; flex-direction: column; }
.logo { padding: 24px; font-size: 18px; font-weight: bold; border-bottom: 1px solid var(--border); }
.nav-item { padding: 12px 24px; color: var(--muted); cursor: pointer; border-left: 3px solid transparent; transition: 0.2s; }
.nav-item:hover { background: rgba(255,255,255,0.02); color: var(--text); }
.nav-item.active { color: var(--text); border-left-color: var(--accent); background: rgba(91, 141, 239, 0.1); }
/* Main Content */
.main { flex: 1; overflow-y: auto; padding: 32px; }
.hidden { display: none !important; }
h1 { margin-top: 0; font-size: 24px; }
h2 { font-size: 18px; border-bottom: 1px solid var(--border); padding-bottom: 8px; margin-bottom: 16px; }
/* Cards */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px; }
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 20px; }
.card .title { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
.card .value { font-size: 28px; font-weight: bold; }
/* Tables & Forms */
table { width: 100%; border-collapse: collapse; margin-top: 16px; background: var(--panel); border-radius: 8px; overflow: hidden; }
th, td { text-align: left; padding: 12px 16px; border-bottom: 1px solid var(--border); }
th { background: rgba(0,0,0,0.2); color: var(--muted); font-weight: normal; font-size: 12px; text-transform: uppercase; }
input, select, textarea {
background: #0b0d12; border: 1px solid var(--border); color: var(--text);
padding: 10px; border-radius: 6px; font: inherit; width: 100%; margin-bottom: 12px;
}
input:focus, select:focus { outline: none; border-color: var(--accent); }
button {
background: var(--accent); color: #fff; border: 0; padding: 10px 16px;
border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer;
}
button:hover { opacity: 0.8; }
button.danger { background: var(--danger); }
button.success { background: var(--success); }
.form-row { display: flex; gap: 12px; margin-bottom: 24px; align-items: flex-start; }
.form-row input, .form-row select { margin-bottom: 0; }
/* Toast */
#toast {
position: fixed; bottom: 24px; right: 24px; padding: 12px 24px;
border-radius: 6px; color: #fff; font-weight: 500; opacity: 0;
transform: translateY(10px); transition: all 0.3s; z-index: 1000;
}
#toast.show { opacity: 1; transform: translateY(0); }
#toast.error { background: var(--danger); }
#toast.success { background: var(--success); }
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar" id="sidebar" style="display:none;">
<div class="logo">Sunnyware SaaS</div>
<div class="nav-item active" onclick="nav('dashboard')">Dashboard</div>
<div class="nav-item" onclick="nav('orgs')">Organizations</div>
<div class="nav-item" onclick="nav('plans')">Plans</div>
<div class="nav-item" onclick="nav('invoices')">Invoices</div>
<div class="nav-item" onclick="nav('crm')">CRM Notes</div>
<div class="nav-item" onclick="nav('users')">Users (SSO)</div>
<div class="nav-item" onclick="nav('approvals')">HITL Approvals</div>
</div>
<!-- Main Content -->
<div class="main" id="main-content">
<!-- Auth Section -->
<div id="auth-section">
<h1>Admin Login</h1>
<div class="card" style="max-width: 400px;">
<div class="title">Admin API Key</div>
<input type="password" id="api-key" placeholder="master-admin-key">
<button onclick="saveKey()">Authenticate</button>
</div>
</div>
<!-- Dashboard -->
<div id="section-dashboard" class="hidden">
<h1>Dashboard</h1>
<div class="grid" id="dash-grid">
<!-- Filled by JS -->
</div>
<h2>Paddle Billing Integration</h2>
<div class="card">
<p id="stripe-status">Checking...</p>
</div>
</div>
<!-- Organizations -->
<div id="section-orgs" class="hidden">
<h1>Organizations</h1>
<div class="form-row">
<input type="text" id="new-org-name" placeholder="Company Name">
<input type="email" id="new-org-email" placeholder="Owner Email">
<select id="new-org-plan">
<option value="free">Free Plan</option>
<option value="pro">Pro Plan</option>
<option value="enterprise">Enterprise Plan</option>
</select>
<button onclick="createOrg()">Create Org</button>
</div>
<table>
<thead><tr><th>Org ID</th><th>Name</th><th>Plan</th><th>Status</th><th>Actions</th></tr></thead>
<tbody id="orgs-tbody"></tbody>
</table>
</div>
<!-- Plans -->
<div id="section-plans" class="hidden">
<h1>Plans & Pricing</h1>
<table>
<thead><tr><th>Plan</th><th>Price / Month</th><th>Daily Calls</th><th>Features</th></tr></thead>
<tbody id="plans-tbody"></tbody>
</table>
</div>
<!-- Invoices -->
<div id="section-invoices" class="hidden">
<h1>Invoices</h1>
<table>
<thead><tr><th>Invoice ID</th><th>Org ID</th><th>Amount</th><th>Status</th><th>Date</th></tr></thead>
<tbody id="invoices-tbody"></tbody>
</table>
</div>
<!-- CRM -->
<div id="section-crm" class="hidden">
<h1>CRM Notes</h1>
<div class="form-row">
<select id="crm-org-select"><option value="">Select Organization...</option></select>
<input type="text" id="crm-note-text" placeholder="Add a note...">
<button onclick="createNote()">Save Note</button>
</div>
<div id="crm-notes-list"></div>
</div>
<!-- Users (SSO) -->
<div id="section-users" class="hidden">
<h1>SSO Users</h1>
<table>
<thead><tr><th>Email</th><th>Role</th><th>Key Preview</th><th>Actions</th></tr></thead>
<tbody id="users-tbody"></tbody>
</table>
</div>
<!-- Approvals -->
<div id="section-approvals" class="hidden">
<h1>Pending Approvals (HITL)</h1>
<table>
<thead><tr><th>Tool</th><th>Args</th><th>Time</th><th>Actions</th></tr></thead>
<tbody id="approvals-tbody"></tbody>
</table>
</div>
</div>
<div id="toast"></div>
<script>
let API_KEY = localStorage.getItem('sunnyware_admin_key') || '';
function showToast(msg, type) {
const t = document.getElementById('toast');
t.textContent = msg; t.className = type + ' show';
setTimeout(() => t.className = '', 3000);
}
async function apiCall(path, method='GET', body=null) {
const opts = { method, headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
const res = await fetch(path, opts);
if (res.status === 401) { showToast('Invalid Key', 'error'); logout(); throw new Error('401'); }
if (res.status === 403) { showToast('Admin Role Required', 'error'); throw new Error('403'); }
if (!res.ok) throw new Error(await res.text());
return res.json();
}
function saveKey() {
API_KEY = document.getElementById('api-key').value.trim();
if (!API_KEY) return showToast('Key required', 'error');
localStorage.setItem('sunnyware_admin_key', API_KEY);
init();
}
function logout() {
API_KEY = ''; localStorage.removeItem('sunnyware_admin_key');
document.getElementById('sidebar').style.display = 'none';
document.getElementById('auth-section').style.display = 'block';
document.querySelectorAll('.main > div[id^="section-"]').forEach(el => el.classList.add('hidden'));
}
function nav(section) {
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.main > div[id^="section-"]').forEach(el => el.classList.add('hidden'));
const targetNav = Array.from(document.querySelectorAll('.nav-item')).find(el => el.textContent.toLowerCase().includes(section));
if (targetNav) targetNav.classList.add('active');
document.getElementById('section-' + section).classList.remove('hidden');
if (section === 'dashboard') loadDashboard();
if (section === 'orgs') loadOrgs();
if (section === 'plans') loadPlans();
if (section === 'invoices') loadInvoices();
if (section === 'crm') loadCRM();
if (section === 'users') loadUsers();
if (section === 'approvals') loadApprovals();
}
async function loadDashboard() {
try {
const d = await apiCall('/api/saas/dashboard');
document.getElementById('dash-grid').innerHTML = `
<div class="card"><div class="title">MRR</div><div class="value">$${d.mrr.toLocaleString()}</div></div>
<div class="card"><div class="title">ARR</div><div class="value">$${d.arr.toLocaleString()}</div></div>
<div class="card"><div class="title">Total Revenue</div><div class="value">$${d.total_revenue.toLocaleString()}</div></div>
<div class="card"><div class="title">Active Orgs</div><div class="value">${d.active_orgs} / ${d.total_orgs}</div></div>
<div class="card"><div class="title">Invoices</div><div class="value">${d.total_invoices}</div></div>
`;
const b = await apiCall('/api/saas/billing/status');
const _modeHtml = b.mode === 'live'
? '<strong style="color:#2ecc71;">Live Mode</strong> (Paddle Connected — Real Payments)'
: b.mode === 'sandbox'
? '<strong style="color:#f39c12;">Sandbox Mode</strong> (Paddle Sandbox — Test Payments Only)'
: '<strong style="color:#e74c3c;">Simulated</strong> (Add PADDLE_API_KEY to enable billing)';
document.getElementById('stripe-status').innerHTML = _modeHtml + ' <small style="opacity:0.7;">mode: ' + (b.mode || '?') + '</small>';
} catch(e) {}
}
async function loadOrgs() {
const d = await apiCall('/api/saas/orgs');
document.getElementById('orgs-tbody').innerHTML = d.orgs.map(o => `
<tr>
<td><code>${o.id}</code></td>
<td>${o.name}</td>
<td>
<select onchange="updatePlan('${o.id}', this.value)" style="margin:0; padding:4px; font-size:12px; width:auto; background:#12161f; border-color:var(--border);">
<option value="free" ${o.plan === 'free' ? 'selected' : ''}>Free</option>
<option value="pro" ${o.plan === 'pro' ? 'selected' : ''}>Pro ($49)</option>
<option value="enterprise" ${o.plan === 'enterprise' ? 'selected' : ''}>Enterprise ($299)</option>
</select>
</td>
<td>${o.status === 'active' ? 'Active' : o.status}</td>
<td>
<button onclick="checkout('${o.id}', '${o.plan}')" style="padding:4px 8px;font-size:12px;">Pay</button>
<button onclick="manageBilling('${o.id}')" style="padding:4px 8px;font-size:12px;">Billing</button>
<button class="danger" onclick="deleteOrg('${o.id}')" style="padding:4px 8px;font-size:12px;">Delete</button>
</td>
</tr>
`).join('');
const sel = document.getElementById('crm-org-select');
sel.innerHTML = '<option value="">Select Organization...</option>' + d.orgs.map(o => `<option value="${o.id}">${o.name} (${o.id})</option>`).join('');
}
async function updatePlan(orgId, newPlan) {
try {
await apiCall('/api/saas/orgs/' + orgId, 'PATCH', { plan: newPlan });
showToast('Plan updated to ' + newPlan, 'success');
loadOrgs();
} catch(e) {
showToast('Error updating plan', 'error');
}
}
async function createOrg() {
const name = document.getElementById('new-org-name').value;
const email = document.getElementById('new-org-email').value;
const plan = document.getElementById('new-org-plan').value;
if(!name) return showToast('Name required', 'error');
await apiCall('/api/saas/orgs', 'POST', {name, owner_email: email, plan});
showToast('Org created', 'success'); loadOrgs();
}
async function deleteOrg(id) {
if(!confirm('Delete this org?')) return;
await apiCall('/api/saas/orgs/' + id, 'DELETE');
showToast('Org deleted', 'success'); loadOrgs();
}
async function checkout(orgId, planId) {
const res = await apiCall('/api/saas/checkout', 'POST', {org_id: orgId, plan_id: planId});
if (res.ok && res.url) window.location.href = res.url;
else showToast('Checkout error', 'error');
}
async function manageBilling(orgId) {
try {
const res = await apiCall('/api/saas/portal', 'POST', {org_id: orgId});
if (res.ok && res.url) window.location.href = res.url;
else showToast('Billing portal error', 'error');
} catch(e) { showToast('Billing portal error', 'error'); }
}
async function loadPlans() {
const d = await apiCall('/api/saas/plans');
document.getElementById('plans-tbody').innerHTML = d.plans.map(p => `
<tr><td><strong>${p.name}</strong></td><td>$${p.price_monthly}</td><td>${p.calls_per_day}</td><td>${p.features.join(', ')}</td></tr>
`).join('');
}
async function loadInvoices() {
const d = await apiCall('/api/saas/invoices');
document.getElementById('invoices-tbody').innerHTML = d.invoices.map(i => `
<tr><td><code>${i.id}</code></td><td>${i.org_id}</td><td>$${(i.amount_cents/100).toFixed(2)}</td><td>${i.status}</td><td>${new Date(i.created_at*1000).toLocaleDateString()}</td></tr>
`).join('') || '<tr><td colspan="5">No invoices yet.</td></tr>';
}
async function loadCRM() {
const orgId = document.getElementById('crm-org-select').value;
const d = await apiCall('/api/saas/notes' + (orgId ? '?org_id=' + orgId : ''));
document.getElementById('crm-notes-list').innerHTML = d.notes.map(n => `
<div class="card" style="margin-bottom:8px; padding:12px;">
<strong>${n.org_id}</strong> · <small>${new Date(n.created_at*1000).toLocaleString()}</small>
<p style="margin:8px 0 0 0">${n.text}</p>
</div>
`).join('') || '<p>No notes found.</p>';
}
async function createNote() {
const orgId = document.getElementById('crm-org-select').value;
const text = document.getElementById('crm-note-text').value;
if(!orgId || !text) return showToast('Select org and enter text', 'error');
await apiCall('/api/saas/notes', 'POST', {org_id: orgId, text});
document.getElementById('crm-note-text').value = '';
showToast('Note saved', 'success'); loadCRM();
}
async function loadUsers() {
const d = await apiCall('/sso/users');
document.getElementById('users-tbody').innerHTML = d.users.map(u => `
<tr><td>${u.email}</td><td><span style="color:${u.role==='admin'?'var(--success)':'var(--accent)'}">${u.role}</span></td><td><code>${u.api_key_preview}</code></td>
<td><button class="danger" onclick="deleteUser('${u.email}')" style="padding:4px 8px;font-size:12px">Delete</button></td></tr>
`).join('');
}
async function deleteUser(email) {
if(!confirm('Delete user?')) return;
await apiCall('/sso/users/delete', 'POST', {email});
showToast('User deleted', 'success'); loadUsers();
}
async function loadApprovals() {
const d = await apiCall('/api/approvals');
document.getElementById('approvals-tbody').innerHTML = d.approvals.map(a => `
<tr><td><strong>${a.tool}</strong></td><td><code>${JSON.stringify(a.args)}</code></td><td>${new Date(a.created_at*1000).toLocaleTimeString()}</td>
<td>
<button class="success" onclick="approveReq('${a.id}')" style="padding:4px 8px;font-size:12px">Approve</button>
<button class="danger" onclick="rejectReq('${a.id}')" style="padding:4px 8px;font-size:12px">Reject</button>
</td></tr>
`).join('') || '<tr><td colspan="4">No pending approvals.</td></tr>';
}
async function approveReq(id) {
await apiCall('/api/approvals/' + id + '/approve', 'POST');
showToast('Approved', 'success'); loadApprovals();
}
async function rejectReq(id) {
await apiCall('/api/approvals/' + id + '/reject', 'POST');
showToast('Rejected', 'success'); loadApprovals();
}
function init() {
if (API_KEY) {
document.getElementById('auth-section').style.display = 'none';
document.getElementById('sidebar').style.display = 'flex';
nav('dashboard');
// Poll approvals every 10s if on that tab
setInterval(() => {
if (!document.getElementById('section-approvals').classList.contains('hidden')) loadApprovals();
}, 10000);
} else {
document.getElementById('auth-section').style.display = 'block';
document.getElementById('sidebar').style.display = 'none';
}
}
// Start
init();
</script>
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
<script>
// 1. Explicitly set Sandbox environment
Paddle.Environment.set('sandbox');
// 2. Initialize Paddle
Paddle.Setup({ token: 'test_4f5658fd15e4bb429f9e3faec71' });
// 3. Open checkout if txnId is in URL
const urlParams = new URLSearchParams(window.location.search);
const txnId = urlParams.get('_ptxn');
if (txnId) {
Paddle.Checkout.open({ transactionId: txnId });
window.history.replaceState({}, document.title, window.location.pathname);
}
</script>
</body>
</html>