Files
aipackage/cezen-portal/applications.html
T

16 lines
9.5 KiB
HTML

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Applications — Nexus One AI</title><link rel="stylesheet" href="style.css?v=12"><style>
.apps{max-width:1240px;margin:0 auto;padding:34px 36px 70px}.apps-head{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:28px}.apps h1{margin:0 0 7px;font-size:30px;color:var(--text-primary)}.apps-head p{margin:0;color:var(--text-secondary)}.create{background:var(--brand);color:white;text-decoration:none;border-radius:9px;padding:11px 16px;font-size:13px;font-weight:750}.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:22px}.stat,.app-card{background:var(--surface,#fff);border:1px solid var(--border);border-radius:14px}.stat{padding:16px}.stat b{display:block;font-size:23px;color:var(--text-primary)}.stat span{font-size:11px;color:var(--text-tertiary)}.section-head{font-size:13px;font-weight:800;color:var(--text-primary);margin:26px 0 11px}.approval-list{display:grid;gap:10px}.approval-item{display:flex;align-items:center;gap:14px;background:#fffbeb;border:1px solid #fde68a;border-radius:12px;padding:14px}.approval-item div:first-child{flex:1}.approval-item b{display:block;color:#713f12;font-size:13px}.approval-item small{color:#a16207}.approval-item button{background:#15803d;color:white;border:0;border-radius:8px;padding:9px 13px;font-weight:750;cursor:pointer}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.app-card{padding:18px}.app-top{display:flex;justify-content:space-between;gap:10px}.app-icon{width:42px;height:42px;border-radius:10px;background:var(--brand-lt);color:var(--brand);display:grid;place-items:center;font-weight:800}.status{font-size:10px;font-weight:800;color:#15803d;background:#dcfce7;border-radius:99px;padding:4px 8px;height:max-content}.app-card h3{font-size:16px;margin:14px 0 6px;color:var(--text-primary)}.app-card p{font-size:12px;color:var(--text-secondary);line-height:1.5;min-height:54px}.meta{border-top:1px solid var(--border);padding-top:11px;margin-top:12px;font-size:11px;color:var(--text-tertiary);display:flex;justify-content:space-between}.actions{display:flex;gap:8px;margin-top:13px}.actions button,.actions a{flex:1;border:1px solid var(--border);background:white;border-radius:7px;padding:8px;font:inherit;font-size:11px;font-weight:700;cursor:pointer;text-align:center;text-decoration:none;color:var(--text-primary)}.actions a{background:var(--brand);border-color:var(--brand);color:white}.empty{grid-column:1/-1;border:1.5px dashed var(--border);border-radius:14px;text-align:center;padding:55px;color:var(--text-secondary)}@media(max-width:850px){.grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.apps{padding:24px 16px}.grid,.stats{grid-template-columns:1fr}.approval-item{align-items:flex-start;flex-direction:column}}
</style></head><body><header class="topnav"><a href="index.html" class="brand">Nexus One <span>AI</span></a><nav><a href="index.html">Home</a><a href="quickstart.html">Quick Start</a><a href="prompts.html">Prompt Library</a><div class="nav-dropdown"><button class="nav-drop-btn active">AI Tools ▾</button><div class="nav-drop-menu">
<span class="nav-drop-cat">MODEL BUILD /</span>
<a href="dataset-creation.html">Dataset Creation</a>
<a href="training.html">Model Training</a><span class="nav-drop-cat">BUILD /</span><a href="app-studio.html">App Studio</a><a href="applications.html" class="active">Applications</a></div></div></nav><div class="nav-right"><div id="nav-org-logo" class="nav-org-logo"></div><span class="nav-tier-badge" data-brand="tier">Basic Tier</span></div></header><main class="apps"><div class="apps-head"><div><h1>Applications</h1><p>Internal applications created, published and governed through Nexus One App Studio.</p></div><a class="create" href="app-studio.html">+ Create application</a></div><div class="stats"><div class="stat"><b id="total">0</b><span>Published applications</span></div><div class="stat"><b id="running">0</b><span>Running</span></div><div class="stat"><b id="builders">0</b><span>Builders in use</span></div></div><section id="approval-section" hidden><div class="section-head">Publication approvals</div><div class="approval-list" id="approval-list"></div></section><div class="section-head">Published applications</div><div class="grid" id="app-grid"></div></main><script src="branding.js?v=20260721-consolidated"></script><script src="auth.js?v=20260721-consolidated"></script><script>
const grid=document.getElementById('app-grid');const esc=s=>String(s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function render(apps){document.getElementById('total').textContent=apps.length;document.getElementById('running').textContent=apps.filter(a=>String(a.status).toLowerCase()==='running').length;document.getElementById('builders').textContent=new Set(apps.map(a=>a.builder)).size;grid.innerHTML=apps.length?apps.map(a=>`<article class="app-card"><div class="app-top"><div class="app-icon">${esc(a.name).charAt(0)}</div><span class="status">${esc(a.status)}</span></div><h3>${esc(a.name)}</h3><p>${esc(a.description)}</p><div class="meta"><span>${esc(a.builder)}</span><span>v${esc(a.version)}</span></div><div class="actions"><a href="${esc(a.app_url||`/apps/${a.slug}/`)}" target="_blank" rel="noopener">Open app</a><button onclick="manageApplication('${esc(a.id)}')">Releases</button><button onclick="manageAccess('${esc(a.id)}')">Access</button></div></article>`).join(''):`<div class="empty"><h3>No applications published yet</h3><p>Create your first application in App Studio and it will appear here after approval.</p><a class="create" href="app-studio.html">Create application</a></div>`}
async function loadApplications(){try{const response=await fetch('/api/applications',{credentials:'include'});if(!response.ok)throw new Error('API unavailable');const data=await response.json();render(data.applications||[])}catch{render(JSON.parse(localStorage.getItem('nexus_applications')||'[]'))}}
async function loadApprovals(){if(window.cezenUser?.role!=='admin')return;const response=await fetch('/api/app-studio/approvals',{credentials:'include'});if(!response.ok)return;const data=await response.json();const approvals=data.approvals||[];if(!approvals.length)return;document.getElementById('approval-section').hidden=false;document.getElementById('approval-list').innerHTML=approvals.map(p=>`<div class="approval-item"><div><b>${esc(p.name)}</b><small>${esc(p.username)} · ${esc(p.builder)} · preview ready</small></div><button onclick="approveProject('${esc(p.id)}',this)">Approve publication</button></div>`).join('')}
async function approveProject(id,button){button.disabled=true;button.textContent='Publishing…';const response=await fetch(`/api/app-studio/projects/${id}/approve`,{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:'{}'});if(!response.ok){button.disabled=false;button.textContent='Try again';return}button.closest('.approval-item').remove();if(!document.querySelector('.approval-item'))document.getElementById('approval-section').hidden=true;loadApplications()}
async function manageApplication(id){const response=await fetch(`/api/applications/${id}`,{credentials:'include'});if(!response.ok)return alert('Unable to load release history.');const app=await response.json();const releases=app.releases||[];const summary=releases.map(r=>`${r.is_current?'Current':'Available'} · v${r.version} · ${(r.commit||'').slice(0,8)}`).join('\n');if(window.cezenUser?.role!=='admin')return alert(summary||'No releases recorded.');const candidate=(app.available_previews||[])[0];if(candidate&&confirm(`${summary}\n\nPromote verified preview ${candidate.version} (${candidate.commit_sha.slice(0,8)}) to production?`)){const promoted=await fetch(`/api/applications/${id}/promote-preview/${candidate.id}`,{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:'{}'});if(!promoted.ok)return alert('Production promotion failed.');alert('Verified preview promoted to production.');return loadApplications()}const previous=releases.find(r=>!r.is_current);if(!previous)return alert(summary||'No rollback release is available.');if(!confirm(`${summary}\n\nRoll back production to v${previous.version}?`))return;const rollback=await fetch(`/api/applications/${id}/rollback/${previous.id}`,{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:'{}'});if(!rollback.ok)return alert('Production rollback failed.');alert(`Production rolled back to v${previous.version}.`);loadApplications()}
async function manageAccess(id){const response=await fetch(`/api/applications/${id}/members`,{credentials:'include'});if(!response.ok)return alert('Owner access is required to manage members.');const data=await response.json();const summary=(data.members||[]).map(member=>`${member.username} · ${member.role}`).join('\n');const username=prompt(`${summary||'No members'}\n\nNexus username to add or update:`);if(!username)return;const role=prompt('Role: viewer, editor or owner','viewer');if(!role)return;const saved=await fetch(`/api/applications/${id}/members`,{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({username,role})});if(!saved.ok){const body=await saved.json().catch(()=>({}));return alert(body.detail||'Unable to update access.')}alert(`${username} now has ${role} access.`)}
document.addEventListener('cezenAuthReady',()=>{loadApplications();loadApprovals()},{once:true});setTimeout(()=>{if(!grid.children.length)loadApplications()},600);
</script></body></html>