@@ -94,6 +94,30 @@ export interface AnalyticsData {
9494 series : AnalyticsSeriesPoint [ ] ;
9595}
9696
97+ export type AnalyticsScope = 'fleet' | 'x402' ;
98+
99+ export interface X402OperationStat {
100+ operation : string ;
101+ payments : number ;
102+ revenueAtomic : string ;
103+ paidSeconds : number ;
104+ }
105+
106+ export interface X402AnalyticsData {
107+ windowHours : number ;
108+ liveSessions : number ;
109+ revenueAtomic : string ;
110+ settledPayments : number ;
111+ uniquePayers : number ;
112+ paidSeconds : number ;
113+ operations : X402OperationStat [ ] ;
114+ events : Record < string , number > ;
115+ assetName : string ;
116+ assetDecimals : number ;
117+ network : string ;
118+ testnet : boolean ;
119+ }
120+
97121export interface ClientSecretNotice {
98122 clientId : string ;
99123 clientSecret : string ;
@@ -1451,26 +1475,58 @@ const ANALYTICS_STYLE = `
14511475.bar-fill.green { background: linear-gradient(90deg, #17915a, #35c680); }
14521476` ;
14531477
1454- export function AnalyticsView ( { data } : { data : AnalyticsData } ) {
1478+ function AnalyticsScopeToggle ( { scope, windowHours } : { scope : AnalyticsScope ; windowHours : number } ) {
1479+ const options : { value : AnalyticsScope ; label : string } [ ] = [
1480+ { value : 'fleet' , label : 'Fleet' } ,
1481+ { value : 'x402' , label : 'x402' } ,
1482+ ] ;
1483+ return (
1484+ < div class = "an-scope" role = "tablist" aria-label = "Analytics scope" >
1485+ { options . map ( ( option ) => (
1486+ < button
1487+ type = "button"
1488+ role = "tab"
1489+ class = { `an-scope-btn${ option . value === scope ? ' active' : '' } ` }
1490+ aria-selected = { option . value === scope ? 'true' : 'false' }
1491+ hx-get = { `/admin/ui/analytics?scope=${ option . value } &windowHours=${ windowHours } ` }
1492+ hx-target = "#admin-content"
1493+ hx-swap = "innerHTML"
1494+ >
1495+ < span class = { `an-scope-dot ${ option . value } ` } aria-hidden = "true" > </ span >
1496+ < span > { option . label } </ span >
1497+ </ button >
1498+ ) ) }
1499+ </ div >
1500+ ) ;
1501+ }
1502+
1503+ export function AnalyticsView ( { data, x402, scope = 'fleet' } : { data : AnalyticsData ; x402 ?: X402AnalyticsData ; scope ?: AnalyticsScope } ) {
14551504 const { live, window, series } = data ;
14561505 const utilization = percent ( live . allocated , live . capacity ) ;
14571506 const free = Math . max ( 0 , live . capacity - live . allocated - live . ready ) ;
14581507 const hasActivity = series . some ( ( p ) => p . ended > 0 || p . created > 0 ) ;
1508+ // The toggle only exists when x402 is enabled; otherwise always show fleet.
1509+ const activeScope : AnalyticsScope = x402 && scope === 'x402' ? 'x402' : 'fleet' ;
14591510
14601511 return (
14611512 < div class = "workspace analytics-workspace" >
14621513 < header class = "page-header an-topbar" >
14631514 < div class = "an-title" >
14641515 < span class = "eyebrow" > Performance</ span >
14651516 < h1 > Analytics</ h1 >
1466- < p > Fleet allocation and session lifecycle across the { windowLabel ( data . windowHours ) } .</ p >
1517+ < p >
1518+ { activeScope === 'x402'
1519+ ? `Paid x402 sessions across the ${ windowLabel ( data . windowHours ) } .`
1520+ : `Fleet allocation and session lifecycle across the ${ windowLabel ( data . windowHours ) } .` }
1521+ </ p >
14671522 </ div >
14681523 < div class = "an-controls" >
1524+ { x402 ? < AnalyticsScopeToggle scope = { activeScope } windowHours = { data . windowHours } /> : null }
14691525 < select
14701526 class = "an-range"
14711527 name = "windowHours"
14721528 aria-label = "Time range"
1473- hx-get = " /admin/ui/analytics"
1529+ hx-get = { ` /admin/ui/analytics?scope= ${ activeScope } ` }
14741530 hx-target = "#admin-content"
14751531 hx-swap = "innerHTML"
14761532 hx-trigger = "change"
@@ -1482,7 +1538,7 @@ export function AnalyticsView({ data }: { data: AnalyticsData }) {
14821538 < button
14831539 type = "button"
14841540 class = "an-refresh"
1485- hx-get = { `/admin/ui/analytics?windowHours=${ data . windowHours } ` }
1541+ hx-get = { `/admin/ui/analytics?windowHours=${ data . windowHours } &scope= ${ activeScope } ` }
14861542 hx-target = "#admin-content"
14871543 hx-swap = "innerHTML"
14881544 >
@@ -1491,6 +1547,8 @@ export function AnalyticsView({ data }: { data: AnalyticsData }) {
14911547 </ div >
14921548 </ header >
14931549
1550+ { activeScope === 'x402' && x402 ? < X402Section x402 = { x402 } /> : (
1551+ < >
14941552 < div class = "an-sublabel" > Live fleet</ div >
14951553 < div class = "an-kpis" >
14961554 < AnKpi label = "Allocated" value = { String ( live . allocated ) } hint = { `of ${ live . capacity } · ${ utilization } %` } tone = "accent" />
@@ -1569,10 +1627,117 @@ export function AnalyticsView({ data }: { data: AnalyticsData }) {
15691627 </ div >
15701628 </ div >
15711629 ) }
1630+ </ >
1631+ ) }
15721632 </ div >
15731633 ) ;
15741634}
15751635
1636+ // USDC and most x402 settlement assets use 6 decimals; the client flow assumes
1637+ // the same. Format the atomic ledger amount into a human token value.
1638+ function formatAtomicAmount ( atomic : string , decimals : number ) {
1639+ const digits = String ( atomic || '0' ) . replace ( / [ ^ 0 - 9 ] / g, '' ) || '0' ;
1640+ if ( decimals <= 0 ) return digits ;
1641+ const padded = digits . padStart ( decimals + 1 , '0' ) ;
1642+ const whole = padded . slice ( 0 , padded . length - decimals ) ;
1643+ const fraction = padded . slice ( padded . length - decimals ) . replace ( / 0 + $ / , '' ) ;
1644+ return fraction ? `${ whole } .${ fraction } ` : whole ;
1645+ }
1646+
1647+ function x402EventLabel ( eventType : string ) {
1648+ return eventType
1649+ . replace ( / ^ x 4 0 2 \. / , '' )
1650+ . replace ( / _ / g, ' ' )
1651+ . replace ( / ^ \w / , ( ch ) => ch . toUpperCase ( ) ) ;
1652+ }
1653+
1654+ function X402Section ( { x402 } : { x402 : X402AnalyticsData } ) {
1655+ const revenue = formatAtomicAmount ( x402 . revenueAtomic , x402 . assetDecimals ) ;
1656+ const events = Object . entries ( x402 . events ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) ;
1657+ const created = x402 . events [ 'x402.session_created' ] || 0 ;
1658+ const challenges = x402 . events [ 'x402.challenge_issued' ] || 0 ;
1659+ const conversion = challenges > 0 ? Math . round ( ( created / challenges ) * 100 ) : 0 ;
1660+
1661+ return (
1662+ < >
1663+ < div class = "an-sublabel" >
1664+ x402 paid sessions · { windowLabel ( x402 . windowHours ) }
1665+ { x402 . testnet ? ' · testnet' : '' } · { x402 . network }
1666+ </ div >
1667+ < div class = "an-kpis" >
1668+ < AnKpi
1669+ label = "Live sessions"
1670+ value = { String ( x402 . liveSessions ) }
1671+ hint = "paid access active now"
1672+ tone = { x402 . liveSessions > 0 ? 'success' : undefined }
1673+ />
1674+ < AnKpi
1675+ label = "Revenue"
1676+ value = { `${ revenue } ${ x402 . assetName } ` }
1677+ hint = { `${ x402 . settledPayments } settled payment${ x402 . settledPayments === 1 ? '' : 's' } ` }
1678+ tone = "accent"
1679+ />
1680+ < AnKpi label = "Unique payers" value = { String ( x402 . uniquePayers ) } hint = "distinct wallets" />
1681+ < AnKpi label = "Paid time sold" value = { formatDuration ( x402 . paidSeconds ) } />
1682+ < AnKpi
1683+ label = "Challenge → session"
1684+ value = { challenges > 0 ? `${ conversion } %` : '—' }
1685+ hint = { challenges > 0 ? `${ created } of ${ challenges } challenges` : 'No challenges issued' }
1686+ tone = { challenges > 0 && conversion >= 50 ? 'success' : undefined }
1687+ />
1688+ </ div >
1689+
1690+ < div class = "an-charts" >
1691+ < div class = "an-card" >
1692+ < div class = "an-card-head" > < h3 > Revenue by operation</ h3 > < span class = "an-card-sub" > create vs extend</ span > </ div >
1693+ { x402 . operations . length ? (
1694+ < div class = "bar-list" >
1695+ { x402 . operations . map ( ( op ) => (
1696+ < div class = "bar-row" >
1697+ < div class = "bar-row-head" >
1698+ < span class = "bar-row-label" > { op . operation } </ span >
1699+ < span class = "bar-row-value" >
1700+ { formatAtomicAmount ( op . revenueAtomic , x402 . assetDecimals ) } { x402 . assetName } · { op . payments } payment{ op . payments === 1 ? '' : 's' } · { formatDuration ( op . paidSeconds ) }
1701+ </ span >
1702+ </ div >
1703+ < div class = "bar-track" >
1704+ < div
1705+ class = "bar-fill blue"
1706+ style = { `width:${ percent ( op . payments , x402 . settledPayments ) } %` }
1707+ > </ div >
1708+ </ div >
1709+ </ div >
1710+ ) ) }
1711+ </ div >
1712+ ) : (
1713+ < div class = "viz-empty" > No settled payments in this range.</ div >
1714+ ) }
1715+ </ div >
1716+ < div class = "an-card" >
1717+ < div class = "an-card-head" > < h3 > Lifecycle events</ h3 > < span class = "an-card-sub" > count by type</ span > </ div >
1718+ { events . length ? (
1719+ < div class = "bar-list" >
1720+ { events . map ( ( [ type , count ] ) => (
1721+ < div class = "bar-row" >
1722+ < div class = "bar-row-head" >
1723+ < span class = "bar-row-label" > { x402EventLabel ( type ) } </ span >
1724+ < span class = "bar-row-value" > { count } </ span >
1725+ </ div >
1726+ < div class = "bar-track" >
1727+ < div class = "bar-fill blue" style = { `width:${ percent ( count , events [ 0 ] [ 1 ] ) } %` } > </ div >
1728+ </ div >
1729+ </ div >
1730+ ) ) }
1731+ </ div >
1732+ ) : (
1733+ < div class = "viz-empty" > No x402 events in this range.</ div >
1734+ ) }
1735+ </ div >
1736+ </ div >
1737+ </ >
1738+ ) ;
1739+ }
1740+
15761741function SelectedRegionCard ( { region, selectedRegion, totals } : {
15771742 region : AdminRegion | null ;
15781743 selectedRegion : string ;
0 commit comments