Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions public/admin/claims.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CommandLayer Claims Admin</title>
<link rel="stylesheet" href="/css/site.css" />
<style>
*, *::before, *::after { box-sizing: border-box; }
body { margin:0; background:#f8fafc; color:#111827; font-family: Inter, system-ui, sans-serif; }
.container { max-width:1400px; width:min(1400px, calc(100% - 48px)); margin:0 auto; padding:24px 0 48px; }
.panel { background:#fff; border:1px solid #e5e7eb; border-radius:14px; padding:18px; box-shadow:0 1px 2px rgba(0,0,0,.04); min-width:0; }
.auth { margin:14px 0 18px; }
.admin-grid { display:grid; grid-template-columns:420px minmax(0,1fr); gap:20px; align-items:start; }
@media (max-width: 980px) { .admin-grid { grid-template-columns: 1fr; } }
.btn { border:1px solid #d1d5db; border-radius:10px; padding:8px 12px; background:#fff; cursor:pointer; }
.btn-primary { background:#2563eb; border-color:#2563eb; color:#fff; } .btn-secondary { background:#f3f4f6; color:#111827; }
.btn-danger { background:#dc2626; border-color:#dc2626; color:#fff; } .btn-muted { background:#f3f4f6; }
input,textarea { border:1px solid #d1d5db; border-radius:10px; padding:10px; width:100%; min-width:0; }
.row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; min-width:0; }
.muted { color:#6b7280; }
.table-scroll { width:100%; overflow-x:auto; }
table { width:100%; border-collapse:collapse; table-layout:fixed; }
th,td { padding:10px; border-bottom:1px solid #f1f5f9; text-align:left; overflow-wrap:anywhere; word-break:break-word; }
.clickable{cursor:pointer;} .selected{background:#eef2ff;}
.badge{padding:4px 10px; border-radius:999px; font-size:12px; font-weight:700; display:inline-flex; white-space:nowrap; width:auto; max-width:max-content;}
.mono{font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12px;}
.truncate{max-width:220px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:inline-block; vertical-align:bottom;}
.created{background:#e5e7eb;} .approved{background:#dbeafe;color:#1e40af;} .cards_published{background:#ede9fe;color:#5b21b6;}
.failed{background:#fee2e2;color:#991b1b;} .rejected{background:#ffedd5;color:#9a3412;} .live{background:#dcfce7;color:#166534;}
.error-panel{border:1px solid #fecaca;background:#fef2f2;color:#991b1b;padding:10px;border-radius:10px;margin-bottom:12px;}
.card-url-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto; gap:8px; align-items:center; padding:8px 0; border-bottom:1px solid #f1f5f9;}
.claims-list { display:flex; flex-direction:column; gap:8px; }
.claim-item { border:1px solid #e5e7eb; border-radius:12px; padding:10px 12px; background:#fff; cursor:pointer; }
.claim-item.selected { border-color:#2563eb; background:#eef2ff; }
.claim-head, .claim-meta { display:flex; justify-content:space-between; gap:12px; align-items:center; }
.claim-meta { color:#6b7280; font-size:13px; }
.pipeline { display:flex; gap:6px; overflow-x:auto; white-space:nowrap; padding-bottom:2px; }
.detail-panel { min-width:0; overflow:hidden; }
.ens-wrap { overflow-wrap:anywhere; }
.auth-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
.auth-row input { max-width:420px; }

</style>
</head>
<body><div class="container">
<h1>CommandLayer Claims Admin</h1><p class="muted">Internal operator dashboard for claim review and activation pipeline.</p>
<div class="panel auth"><div class="auth-row"><input id="apiKey" type="password" placeholder="Admin API key"><button id="saveKey" class="btn btn-secondary">Save key</button><button id="loadClaims" class="btn btn-primary">Load claims</button><span id="status" class="muted"></span></div></div>
<div class="admin-grid"><div class="panel"><h3>Claims <span id="claimsCount" class="muted"></span></h3><div id="claimsBody" class="claims-list"></div></div><div id="detailPanel" class="panel detail-panel"><p class="muted">Select a claim to review.</p></div></div>
</div>
<script>
(() => {
const s={claims:[],selected:'',detail:null,error:null,checkoutLoading:false,checkoutUrl:null};
const apiKey=document.getElementById('apiKey'); apiKey.value=sessionStorage.getItem('cl_admin_api_key')||localStorage.getItem('cl_admin_api_key')||'';
const headers=()=>({Authorization:`Bearer ${apiKey.value.trim()}`,'Content-Type':'application/json'});
const short=(v,n=10)=>v?`${v.slice(0,n)}…`:''; const dt=(v)=>v?new Date(v).toLocaleString():'-';
const badge=(status)=>`<span class="badge ${status}">${status}</span>`;
const copyBtn=(txt)=>`<button class='btn btn-muted copy-url' data-url='${txt.replaceAll("'", '&#39;')}'>Copy URL</button>`;
function renderClaims(){const b=document.getElementById('claimsBody');b.innerHTML='';document.getElementById('claimsCount').textContent=`(${s.claims.length} loaded)`;for(const c of s.claims){const row=document.createElement('button');row.type='button';row.className=`claim-item ${s.selected===c.claimId?'selected':''}`;row.innerHTML=`<div class='claim-head'><span class='mono'>${short(c.claimId,14)}</span>${badge(c.status)}</div><div class='claim-meta'><span>${c.tenant||'-'}</span><span>${c.packId||'-'} · ${c.agentCount||0} agents</span></div><div class='claim-meta'><span>${dt(c.createdAt)}</span></div>`;row.onclick=()=>loadDetail(c.claimId);b.appendChild(row);}}
async function loadClaims(){status('Loading claims...');const r=await fetch('/api/admin/claims',{headers:{Authorization:headers().Authorization}});const d=await r.json();if(!r.ok||!d.ok){status(`${r.status} ${d.status}`);return;}s.claims=d.claims||[];renderClaims();status(`Loaded ${s.claims.length} claims.`);}
async function loadDetail(id){s.selected=id;renderClaims();const r=await fetch(`/api/admin/claim?claimId=${encodeURIComponent(id)}`,{headers:{Authorization:headers().Authorization}});const d=await r.json();if(!r.ok||!d.ok)return; s.detail=d; renderDetail();}
async function action(action,p={}){const r=await fetch('/api/admin/claim-action',{method:'POST',headers:headers(),body:JSON.stringify({claimId:s.selected,action,actor:'admin',...p})});const d=await r.json();if(!r.ok||!d.ok){s.error=`${d.status}: ${d.error}`;renderDetail();return;}s.error=null;await loadClaims();await loadDetail(s.selected);}
async function publish(){const r=await fetch('/api/admin/publish-agent-cards',{method:'POST',headers:headers(),body:JSON.stringify({claimId:s.selected})});const d=await r.json();if(!r.ok||!d.ok){s.error=`${d.status}: ${d.error}`;renderDetail();return;}s.error=null;await loadClaims();await loadDetail(s.selected);}
async function createCheckoutSession(claimId,forceNew=false){if(!claimId){s.error='400 — claimId is required';renderDetail();return;}s.error=null;s.checkoutUrl=null;s.checkoutLoading=true;renderDetail();try{const r=await fetch('/api/admin/create-checkout-session',{method:'POST',headers:headers(),body:JSON.stringify({claimId,forceNew})});const d=await r.json().catch(()=>({}));if(!r.ok||!d.ok){const detail=d?.debug?.message?`\nDetails: ${d.debug.message}`:'';s.error=`Checkout failed:\n${d.status||r.status} — ${d.error||'Request failed'}${detail}`;return;}s.checkoutUrl=d.checkoutUrl||d.url||null;await loadClaims();await loadDetail(claimId);}catch(e){s.error=`500 — ${e?.message||'Request failed'}`;}finally{s.checkoutLoading=false;renderDetail();}}
function checkoutUrlFromClaim(claim){return s.checkoutUrl || claim?.stripe_checkout_url || claim?.payment_checkout_url || null;}
function openCheckout(claim){const url=checkoutUrlFromClaim(claim);if(url)window.open(url,'_blank');}
function copyCheckout(claim){const url=checkoutUrlFromClaim(claim);if(url)navigator.clipboard.writeText(url);}

function pipeline(status){const steps=['created','approved','cards_published','payment_pending','paid','erc8004','ens_provisioned','live'];return `<div class='pipeline'>${steps.map(x=>`<span class='badge ${status===x|| (x==='created')|| (status==='approved'&&x==='created')|| (status==='cards_published'&&(x==='created'||x==='approved')) ? (['payment_pending','paid','erc8004','ens_provisioned'].includes(x)?'created':x):'created'}'>${x.replaceAll('_',' ')}</span>`).join('')}</div><p class='muted'>Future steps are coming next.</p>`}
function renderDetail(){const el=document.getElementById('detailPanel');if(!s.detail){el.innerHTML='<p class="muted">Select a claim to review.</p>';return;}const {claim,agents=[],events=[],transitions=[],cards=[]}=s.detail;const urls=cards.map(c=>c.card_url).filter(Boolean);const missing=claim.status==='cards_published'&&agents.some(a=>!a.card_url);const firstUrl=urls[0];el.innerHTML=`${s.error?`<div class='error-panel'>${s.error}</div>`:''}<h3>Claim <span class='mono'>${short(claim.claim_id,16)}</span> <button id='copyClaim' class='btn btn-muted'>Copy</button> ${badge(claim.status)}</h3><p class='muted'>Tenant: ${claim.tenant||'-'} · Wallet: <span class='mono'>${claim.authenticated_address||'-'}</span> · Pack: ${claim.pack_id||'-'} · Agents: ${agents.length} · Created: ${dt(claim.created_at)}</p><h4>Pipeline</h4>${pipeline(claim.status)}<h4>Next action</h4><div class='row'><input id='reasonInput' placeholder='Reason'><input id='notesInput' placeholder='Notes'></div><div class='row' id='actions'></div><h4>Agents</h4><div class='table-scroll'><table><thead><tr><th>ENS</th><th>Capability</th><th>Parent</th><th>Card</th><th>Actions</th></tr></thead><tbody>${agents.map(a=>`<tr><td class='mono ens-wrap'>${a.ens||'-'}</td><td>${a.capability||'-'}</td><td>${a.canonical_parent||'-'}</td><td>${a.card_status||'-'}</td><td>${a.card_url?`<a href='${a.card_url}' target='_blank'>Open</a> <button class='btn btn-muted copy-url' data-url='${a.card_url}'>Copy</button>`:'-'}</td></tr>`).join('')}</tbody></table></div>${claim.status==='payment_pending'?`<h4>Payment pending</h4><p class='muted'>Checkout has been created. Open it to complete payment.</p><p class='mono'>${claim.stripe_checkout_session_id||'-'}</p>`:''}${claim.status==='paid'?`<h4>Payment received</h4><p class='muted'>Next: ERC-8004 registration</p>`:''}${claim.status==='cards_published'?`<h4>Agent cards published</h4><p class='muted'>Founding Activation: $20 first year for 10 Trust Verification namespaces.</p><p class='muted'>${urls.length} cards${missing?' · Missing card URLs detected':''}</p>${s.checkoutUrl?`<div class='panel'><strong>Checkout created</strong><div class='row'><a class='btn btn-secondary' href='${s.checkoutUrl}' target='_blank' rel='noopener'>Open checkout</a><button id='copyCheckoutUrl' type='button' class='btn btn-secondary' data-url='${s.checkoutUrl}'>Copy checkout URL</button></div><div class='mono' style='overflow-wrap:anywhere;'>${s.checkoutUrl}</div></div>`:''}<div class='row'><button id='copyUrls' type='button' class='btn btn-secondary'>Copy all URLs</button>${firstUrl?`<button id='openFirst' type='button' class='btn btn-secondary'>Open first card</button>`:''}</div><div>${urls.map(u=>`<div class='card-url-row'><span class='mono truncate' title='${u}'>${u.split('/').pop()?.replace(/\.json$/,'')||u}</span><a href='${u}' target='_blank'>Open</a>${copyBtn(u)}</div>`).join('')}</div>${missing?`<p class='error-panel'>Some cards are missing URLs. Use Repair / Publish cards.</p>`:''}`:''}<h4>Events</h4>${events.map(e=>`<div class='panel' style='margin-bottom:8px'><strong>${e.event_type}</strong><div style='overflow-wrap:anywhere;'>${e.message||''}</div><div class='muted'>${dt(e.created_at)}</div><details><summary>metadata</summary><pre style='white-space:pre-wrap;overflow-wrap:anywhere;'>${JSON.stringify(e.event_json||{},null,2)}</pre></details></div>`).join('')||'<p class="muted">No events.</p>'}<h4>Transitions</h4>${transitions.map(t=>`<div class='panel' style='margin-bottom:8px'>${t.from_status||'-'} → ${t.to_status||'-'} · ${t.action||'-'} · ${t.actor||'-'}<div class='muted'>${dt(t.created_at)} ${t.reason?`· ${t.reason}`:''}</div></div>`).join('')||'<p class="muted">No transitions.</p>'}<details><summary>Show raw JSON</summary><pre style='white-space:pre-wrap;overflow-wrap:anywhere;'>${JSON.stringify(s.detail,null,2)}</pre></details>`;
const a=document.getElementById('actions');const mk=(t,c,cls='btn',opts={})=>{const b=document.createElement('button');b.type='button';b.textContent=t;b.className=cls;b.disabled=Boolean(opts.disabled);if(opts.id)b.id=opts.id;b.onclick=c;a.appendChild(b);};
if(claim.status==='created'){mk('Approve',()=>action('approve',{notes:document.getElementById('notesInput').value}),'btn btn-primary');mk('Reject',()=>action('reject',{reason:document.getElementById('reasonInput').value}),'btn btn-secondary');mk('Mark failed',()=>action('mark_failed',{reason:document.getElementById('reasonInput').value}),'btn btn-danger');}
if(claim.status==='approved'){mk('Publish agent cards',()=>publish(),'btn btn-primary');mk('Mark failed',()=>action('mark_failed',{reason:document.getElementById('reasonInput').value}),'btn btn-danger');mk('Add note',()=>action('add_note',{notes:document.getElementById('notesInput').value,reason:document.getElementById('reasonInput').value}),'btn');}
if(claim.status==='cards_published'){if(missing)mk('Repair / Publish cards',()=>publish(),'btn btn-primary');mk(s.checkoutLoading?'Creating checkout...':'Create $20 checkout',()=>createCheckoutSession(claim.claim_id),'btn btn-primary',{disabled:s.checkoutLoading,id:'createCheckoutBtn'});mk('Add note',()=>action('add_note',{notes:document.getElementById('notesInput').value,reason:document.getElementById('reasonInput').value}),'btn');}
if(claim.status==='payment_pending'){mk('Open checkout',()=>openCheckout(claim),'btn btn-secondary');mk('Copy checkout URL',()=>copyCheckout(claim),'btn btn-secondary');mk(s.checkoutLoading?'Creating checkout...':'Regenerate checkout session',()=>createCheckoutSession(claim.claim_id,true),'btn btn-primary',{disabled:s.checkoutLoading,id:'regenerateCheckoutBtn'});mk('Add note',()=>action('add_note',{notes:document.getElementById('notesInput').value,reason:document.getElementById('reasonInput').value}),'btn');}
if(claim.status==='failed'||claim.status==='rejected'){mk('Add note',()=>action('add_note',{notes:document.getElementById('notesInput').value,reason:document.getElementById('reasonInput').value}),'btn');}
const copy=document.getElementById('copyClaim');if(copy)copy.onclick=()=>navigator.clipboard.writeText(claim.claim_id);const cu=document.getElementById('copyUrls');if(cu)cu.onclick=()=>navigator.clipboard.writeText(urls.join('\n'));const of=document.getElementById('openFirst');if(of)of.onclick=()=>window.open(urls[0],'_blank');const ccu=document.getElementById('copyCheckoutUrl');if(ccu)ccu.onclick=()=>navigator.clipboard.writeText(ccu.dataset.url||'');document.querySelectorAll('.copy-url').forEach((btn)=>{btn.onclick=()=>navigator.clipboard.writeText(btn.dataset.url||'');}); }
const status=(m)=>document.getElementById('status').textContent=m;
document.getElementById('saveKey').onclick=()=>{const v=apiKey.value.trim();sessionStorage.setItem('cl_admin_api_key',v);localStorage.setItem('cl_admin_api_key',v);status('Saved.');};
document.getElementById('loadClaims').onclick=loadClaims;
})();
</script></body></html>
14 changes: 12 additions & 2 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
{
"rewrites": [
{ "source": "/verify/r/:receipt_id", "destination": "/verify/r/index.html" },
{ "source": "/agent-cards/agents/v1.1.0/trust/:ens(.+).json", "destination": "/api/agent-cards/card?ens=:ens" }
{
"source": "/admin/claims",
"destination": "/admin/claims.html"
},
{
"source": "/verify/r/:receipt_id",
"destination": "/verify/r/index.html"
},
{
"source": "/agent-cards/agents/v1.1.0/trust/:ens(.+).json",
"destination": "/api/agent-cards/card?ens=:ens"
}
]
}
Loading