// ── ADDITIONS: appended by features #14-17 ──────────────────────────────── // ── WordPress Manager (#14) ──────────────────────────────────────────────── async function wordpress() { const [acctRes, wpRes] = await Promise.all([ Nova.api('accounts','list',{params:{limit:500}}), Nova.api('wordpress','list'), ]); const accts = acctRes?.data?.accounts || []; const installs = wpRes?.data?.installs || []; window._adminAcctsWP = accts; return `
WordPress Installs ${installs.length} install${installs.length!==1?'s':''}
${installs.length ? `
${installs.map(w => ``).join('')}
DomainPathAccountVersionStatusActions
${Nova.escHtml(w.domain)} ${Nova.escHtml(w.path||'/')} ${Nova.escHtml(w.username||'—')} ${w.wp_version ? `${Nova.escHtml(w.wp_version)}` : '—'} ${Nova.badge(w.status||'active', w.status==='active'?'green':w.status==='updating'?'yellow':'red')} ${!w.staging_of ? `` : `staging`}
` : `
No WordPress installs yet. Click "Install WordPress" to get started.
`}
`; } window.wpInstallModal = () => { const accts = window._adminAcctsWP || []; const opts = accts.map(a => ``).join(''); Nova.modal('Install WordPress', `

wp-cli will be downloaded automatically if not installed. This may take 1-2 minutes.

`, ` `); }; window.wpSubmitInstall = async () => { const btn = document.getElementById('wp-install-btn'); if (btn) { btn.disabled = true; btn.textContent = 'Installing…'; } Nova.toast('Installing WordPress — this may take 1-2 minutes…', 'info', 90000); const res = await Nova.api('wordpress','install',{method:'POST',body:{ account_id: +document.getElementById('wp-acct')?.value, domain: document.getElementById('wp-domain')?.value, path: document.getElementById('wp-path')?.value || '/', site_title: document.getElementById('wp-title')?.value, admin_user: document.getElementById('wp-admin')?.value, admin_pass: document.getElementById('wp-adminpass')?.value, admin_email:document.getElementById('wp-email')?.value, }}); document.querySelector('.modal-overlay')?.remove(); if (res?.success) { Nova.toast('WordPress installed!','success'); adminPage('wordpress'); } else Nova.toast(res?.message || 'Install failed','error'); }; window.wpUpdate = async (id, type) => { const action = type === 'core' ? 'update-core' : type === 'plugins' ? 'update-plugins' : 'update-themes'; Nova.toast(`Updating ${type}…`,'info',15000); const r = await Nova.api('wordpress', action, {method:'POST',body:{install_id:id}}); Nova.toast(r?.message || (r?.success ? 'Updated' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('wordpress'); }; window.wpInfo = async (id, domain) => { Nova.toast('Loading info…','info',5000); const r = await Nova.api('wordpress','info',{params:{install_id:id}}); if (!r?.success) { Nova.toast(r?.message,'error'); return; } const d = r.data || {}; const plugins = (d.plugins||[]).map(p => `${Nova.escHtml(p.name)}${Nova.escHtml(p.version||'')}${Nova.badge(p.status||'inactive',p.status==='active'?'green':'muted')}`).join(''); const themes = (d.themes||[]).map(t => `${Nova.escHtml(t.name)}${Nova.escHtml(t.version||'')}${Nova.badge(t.status||'inactive',t.status==='active'?'green':'muted')}`).join(''); Nova.modal(`WordPress: ${domain}`,`

Core Version

${Nova.escHtml(d.version||'—')}

Site URL

${Nova.escHtml(d.siteurl||'—')}

Plugins (${(d.plugins||[]).length})

${plugins ? `${plugins}
PluginVersionStatus
` : '

None

'}

Themes (${(d.themes||[]).length})

${themes ? `${themes}
ThemeVersionStatus
` : '

None

'}`); }; window.wpCloneStaging = (id, domain) => { Nova.confirm(`Clone ${domain} to a staging environment? This copies all files and the database.`, async () => { Nova.toast('Cloning to staging…','info',30000); const r = await Nova.api('wordpress','clone-staging',{method:'POST',body:{install_id:id}}); Nova.toast(r?.message || (r?.success ? 'Staging created' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('wordpress'); }); }; window.wpDelete = (id, domain) => { Nova.confirm(`DELETE WordPress install on ${domain}? This removes all files AND drops the database. IRREVERSIBLE.`, async () => { const r = await Nova.api('wordpress','delete',{method:'POST',body:{install_id:id}}); Nova.toast(r?.message || (r?.success ? 'Deleted' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('wordpress'); }, true); }; // ── Backup Manager — full implementation (#15) ───────────────────────────── async function backupsFull() { const [acctRes, bkRes] = await Promise.all([ Nova.api('accounts','list',{params:{limit:500}}), Nova.api('backup','list'), ]); const accts = acctRes?.data?.accounts || []; const backupList = bkRes?.data?.backups || []; const diskUsed = bkRes?.data?.disk_used || 0; window._adminAcctsBK = accts; return `
Total Backups
${backupList.length}
Disk Used
${Nova.bytes(diskUsed)}
Accounts
${accts.length}
Backup Schedules

Set per-account backup schedules. Cron runs backups automatically based on the configured frequency.

${accts.slice(0,8).map(a => ``).join('')} ${accts.length>8?`+${accts.length-8} more`:''}
All Backups
${backupList.length ? `
${backupList.map(b => ``).join('')}
AccountTypeSizeStatusStorageCreatedActions
${Nova.escHtml(b.username||b.account_id||'—')} ${Nova.badge(b.type,'default')} ${Nova.bytes(b.size||0)} ${Nova.badge(b.status, b.status==='complete'?'green':b.status==='failed'?'red':'yellow')} ${b.remote_path ? Nova.badge('remote','blue') : Nova.badge('local','muted')} ${Nova.relTime(b.created_at)} ${b.status==='complete'?`Download`:''}
` : `
No backups yet.
`}
`; } window.bkCreateModal = () => { const accts = window._adminAcctsBK || []; const opts = accts.map(a => ``).join(''); Nova.modal('Create Backup', `
`, ` `); }; window.bkSubmitCreate = async () => { const id = +document.getElementById('bk-acct')?.value; const type = document.getElementById('bk-type')?.value; document.querySelector('.modal-overlay')?.remove(); Nova.toast('Creating backup…','info',30000); const r = await Nova.api('backup','create',{method:'POST',body:{account_id:id,type}}); Nova.toast(r?.message||(r?.success?'Backup complete':'Failed'), r?.success?'success':'error'); if (r?.success) adminPage('backups'); }; window.bkRestore = (id) => { Nova.confirm('Restore this backup? Current files and databases will be overwritten. IRREVERSIBLE.', async () => { Nova.toast('Restoring…','info',30000); const r = await Nova.api('backup','restore',{method:'POST',body:{id}}); Nova.toast(r?.message||(r?.success?'Restored':'Failed'), r?.success?'success':'error'); }, true); }; window.bkDelete = (id) => { Nova.confirm('Delete this backup?', async () => { const r = await Nova.api('backup','delete',{method:'POST',body:{id}}); Nova.toast(r?.message||(r?.success?'Deleted':'Failed'), r?.success?'success':'error'); if (r?.success) adminPage('backups'); }, true); }; window.bkScheduleModal = () => { const accts = window._adminAcctsBK || []; const opts = accts.map(a => ``).join(''); Nova.modal('Configure Backup Schedule', `
`, ` `); }; window.bkScheduleForAccount = async (id, user) => { const r = await Nova.api('backup','get-schedule',{params:{account_id:id}}); const s = r?.data || {}; Nova.modal(`Schedule: ${user}`, `
`, ` `); }; window.bkSaveSchedule = async () => { const id = +document.getElementById('bks-acct')?.value; await bkSaveScheduleFor(id); }; window.bkSaveScheduleFor = async (id) => { const r = await Nova.api('backup','schedule',{method:'POST',body:{ account_id: id, frequency: document.getElementById('bks-freq')?.value, type: document.getElementById('bks-type')?.value, retain: +document.getElementById('bks-retain')?.value, }}); document.querySelector('.modal-overlay')?.remove(); Nova.toast(r?.message||(r?.success?'Schedule saved':'Failed'), r?.success?'success':'error'); }; // ── Cloudflare Integration (#16) ────────────────────────────────────────── async function cloudflare() { const acctRes = await Nova.api('accounts','list',{params:{limit:500}}); const accts = acctRes?.data?.accounts || []; window._adminAcctsCF = accts; return `
Account Credentials

Select an account to configure or view its Cloudflare API key.

`; } window.cfLoadAccount = async (id) => { if (!id) { document.getElementById('cf-acct-panel').innerHTML=''; return; } const r = await Nova.api('cloudflare','get-credentials',{params:{account_id:id}}); const c = r?.data || {}; document.getElementById('cf-acct-panel').innerHTML = `
${c.cf_api_key ? `

Key on file: ${Nova.escHtml(c.cf_api_key)}

` : ''}`; document.getElementById('cf-zones-panel').style.display = ''; window._cfCurrentAcct = id; }; window.cfSaveCredentials = async (id) => { const r = await Nova.api('cloudflare','save-credentials',{method:'POST',body:{ account_id: id, api_key: document.getElementById('cf-apikey')?.value, email: document.getElementById('cf-email')?.value, }}); Nova.toast(r?.message||(r?.success?'Saved':'Failed'), r?.success?'success':'error'); }; window.cfTestKey = async (id) => { const r = await Nova.api('cloudflare','test-key',{method:'POST',body:{ account_id: id, api_key: document.getElementById('cf-apikey')?.value, email: document.getElementById('cf-email')?.value, }}); Nova.toast(r?.message||(r?.data?.valid?'API key is valid':'Invalid key'), r?.data?.valid?'success':'error'); }; window.cfRefreshZones = async () => { const id = window._cfCurrentAcct; if (!id) { Nova.toast('Select an account first','error'); return; } const r = await Nova.api('cloudflare','list-zones',{params:{account_id:id}}); const zones = r?.data?.zones || r?.data || []; const body = document.getElementById('cf-zones-body'); if (!body) return; if (!r?.success) { body.innerHTML=`

${Nova.escHtml(r?.message||'Failed to load zones')}

`; return; } if (!zones.length) { body.innerHTML='

No zones found for these credentials.

'; return; } body.innerHTML = ` ${zones.map(z=>``).join('')}
ZoneStatusPlanActions
${Nova.escHtml(z.name)}
${Nova.escHtml(z.id)}
${Nova.badge(z.status,z.status==='active'?'green':'yellow')} ${Nova.escHtml(z.plan?.name||'—')}
`; }; window.cfViewRecords = async (zoneId, domain, acctId) => { const r = await Nova.api('cloudflare','list-records',{method:'POST',body:{zone_id:zoneId,account_id:acctId}}); const records = r?.data?.records || r?.data || []; Nova.modal(`CF DNS: ${domain}`, !records.length ? '

No records.

' : ` ${records.map(rec=>``).join('')}
NameTypeValueProxy
${Nova.escHtml(rec.name)} ${Nova.badge(rec.type,'default')} ${Nova.escHtml(rec.content)}
`); }; window.cfToggleProxy = async (zoneId, recordId, proxied, acctId) => { const r = await Nova.api('cloudflare','toggle-proxy',{method:'POST',body:{zone_id:zoneId,record_id:recordId,proxied,account_id:acctId}}); Nova.toast(r?.message||(r?.success?'Updated':'Failed'), r?.success?'success':'error'); }; window.cfSync = async (zoneId, domain, dir, acctId) => { const action = dir==='to' ? 'sync-to-cf' : 'sync-from-cf'; const label = dir==='to' ? 'Pushing to Cloudflare' : 'Pulling from Cloudflare'; Nova.toast(`${label}…`,'info',10000); const r = await Nova.api('cloudflare',action,{method:'POST',body:{zone_id:zoneId,domain,account_id:acctId}}); Nova.toast(r?.message||(r?.success?'Done':'Failed'), r?.success?'success':'error'); }; window.cfPurge = async (zoneId, acctId) => { Nova.confirm('Purge all Cloudflare cache for this zone?', async () => { const r = await Nova.api('cloudflare','purge-cache',{method:'POST',body:{zone_id:zoneId,account_id:acctId}}); Nova.toast(r?.message||(r?.success?'Cache purged':'Failed'), r?.success?'success':'error'); }); }; // ── TOTP / 2FA Admin (#17) ──────────────────────────────────────────────── async function twofa() { const res = await Nova.api('accounts','list',{params:{limit:500}}); const users = res?.data?.accounts || []; return `
User 2FA Status
${users.map(u=>``).join('')}
UsernameEmailRole2FA StatusActions
${Nova.escHtml(u.username)} ${Nova.escHtml(u.email||'—')} ${Nova.badge(u.role||'user','default')}
`; } window.totpCheckStatus = async (userId) => { const r = await Nova.api('totp','admin-status',{method:'POST',body:{user_id:userId}}); const el = document.getElementById(`totp-status-${userId}`); if (!el) return; const enabled = r?.data?.totp_enabled; el.innerHTML = enabled ? Nova.badge('Enabled','green') : Nova.badge('Disabled','muted'); }; window.totpAdminDisable = (userId, username) => { Nova.confirm(`Force-disable 2FA for ${username}? Use only for account recovery when user cannot log in.`, async () => { const r = await Nova.api('totp','admin-disable',{method:'POST',body:{user_id:userId}}); Nova.toast(r?.message||(r?.success?'2FA disabled':'Failed'), r?.success?'success':'error'); if (r?.success) { const el = document.getElementById(`totp-status-${userId}`); if (el) el.innerHTML = Nova.badge('Disabled','muted'); } }, true); };