Spaces:
Running on Zero
Running on Zero
Download static/admin.html from shamiur/sunnyware: direct link, hf CLI and curl.
- Browser
- Download file 17.3 kB
-
https://huggingface.co/spaces/shamiur/sunnyware/resolve/main/static/admin.html
- Command line
-
hf download hf://spaces/shamiur/sunnyware/static/admin.html
-
curl -L -o admin.html https://huggingface.co/spaces/shamiur/sunnyware/resolve/main/static/admin.html
17.3 kB
| <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 ; } | |
| 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> | |