3131 .error-panel {border : 1px solid # fecaca ;background : # fef2f2 ;color : # 991b1b ;padding : 10px ;border-radius : 10px ;margin-bottom : 12px ;}
3232 .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 ;}
3333 .claims-list { display : flex; flex-direction : column; gap : 8px ; }
34- .claim-item { border : 1px solid # e5e7eb ; border-radius : 12px ; padding : 10px 12px ; background : # fff ; cursor : pointer; }
34+ .claim-item { border : 1px solid # e5e7eb ; border-radius : 12px ; padding : 10px 12px ; background : # fff ; cursor : pointer; transition : background .15s ease, border-color .15s ease; }
35+ .claim-item : hover { border-color : # 93c5fd ; background : # f8fbff ; }
3536 .claim-item .selected { border-color : # 2563eb ; background : # eef2ff ; }
3637 .claim-head , .claim-meta { display : flex; justify-content : space-between; gap : 12px ; align-items : center; }
3738 .claim-meta { color : # 6b7280 ; font-size : 13px ; }
@@ -52,13 +53,13 @@ <h1>CommandLayer Claims Admin</h1><p class="muted">Internal operator dashboard f
5253( ( ) => {
5354const s = { claims :[ ] , selected :'' , detail :null , error :null , checkoutLoading :false , checkoutUrl :null } ;
5455const apiKey = document . getElementById ( 'apiKey' ) ; apiKey . value = sessionStorage . getItem ( 'cl_admin_api_key' ) || localStorage . getItem ( 'cl_admin_api_key' ) || '' ;
55- const headers = ( ) => ( { Authorization : `Bearer ${ apiKey . value . trim ( ) } `, 'Content-Type' :'application/json' } ) ;
56+ const headers = ( ) => { const key = apiKey . value . trim ( ) ; return { Authorization : `Bearer ${ key } `, 'x-admin-api-key' : key , ' Content-Type' :'application/json' } ; } ;
5657const short = ( v , n = 10 ) => v ?`${ v . slice ( 0 , n ) } …` :'' ; const dt = ( v ) => v ?new Date ( v ) . toLocaleString ( ) :'-' ;
5758const badge = ( status ) => `<span class="badge ${ status } ">${ status } </span>` ;
5859const copyBtn = ( txt ) => `<button class='btn btn-muted copy-url' data-url='${ txt . replaceAll ( "'" , ''' ) } '>Copy URL</button>` ;
5960function 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 ) ; } }
60- 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.` ) ; }
61- 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 ( ) ; }
61+ async function loadClaims ( ) { status ( 'Loading claims...' ) ; const r = await fetch ( '/api/admin/claims' , { headers :{ Authorization :headers ( ) . Authorization , 'x-admin-api-key' : headers ( ) [ 'x-admin-api-key' ] } } ) ; const d = await r . json ( ) ; if ( ! r . ok || d ? .ok === false ) { status ( `${ r . status } ${ d . status || d . error || 'error' } ` ) ; return ; } s . claims = d . claims || [ ] ; renderClaims ( ) ; status ( `Loaded ${ s . claims . length } claims.` ) ; }
62+ async function loadDetail ( id ) { s . selected = id ; s . detail = null ; s . error = null ; renderClaims ( ) ; renderDetail ( 'Loading claim detail...' ) ; const endpoint = `/api/admin/claim?claimId=${ encodeURIComponent ( id ) } ` ; console . debug ( '[claims-admin] selected claimId:' , id ) ; console . debug ( '[claims-admin] detail endpoint:' , endpoint ) ; const r = await fetch ( endpoint , { headers :{ Authorization :headers ( ) . Authorization , 'x-admin-api-key' : headers ( ) [ 'x-admin-api-key' ] } } ) ; console . debug ( '[claims-admin] detail HTTP status:' , r . status ) ; const d = await r . json ( ) . catch ( ( ) => ( { } ) ) ; console . debug ( '[claims-admin] detail response status/error:' , d ?. status || null , d ?. error || null ) ; if ( ! r . ok || d ? .ok === false || ! d ?. claim ) { s . error = `Claim detail failed: ${ r . status } — ${ d ?. error || d ?. status || 'Unknown error' } ` ; renderDetail ( ) ; return ; } s . detail = d ; renderDetail ( ) ; }
6263async 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 ) ; }
6364async 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 ) ; }
6465async 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 ( ) ; } }
@@ -67,7 +68,7 @@ <h1>CommandLayer Claims Admin</h1><p class="muted">Internal operator dashboard f
6768function copyCheckout ( claim ) { const url = checkoutUrlFromClaim ( claim ) ; if ( url ) navigator . clipboard . writeText ( url ) ; }
6869
6970function 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>` }
70- 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 ( / \. j s o n $ / , '' ) || 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>` ;
71+ function renderDetail ( loadingMsg = '' ) { const el = document . getElementById ( 'detailPanel' ) ; if ( loadingMsg ) { el . innerHTML = `<p class="muted">${ loadingMsg } </p>` ; return ; } if ( ! s . detail ) { el . innerHTML = `<p class="muted">${ s . error || 'Select a claim to review.' } </p>` ; return ; } const payload = s . detail || { } ; const claim = payload . claim || { } ; const agents = payload . agents || [ ] ; const events = payload . events || [ ] ; const transitions = payload . transitions || [ ] ; const cards = payload . cards || [ ] ; 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> · Status: ${ claim . status || '-' } · Payment: ${ claim . payment_status || '-' } · 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 ( / \. j s o n $ / , '' ) || 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>` ;
7172const 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 ) ; } ;
7273if ( 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' ) ; }
7374if ( 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' ) ; }
0 commit comments