@@ -126,17 +126,15 @@ const CP = (() => {
126126 async function loadDashboard ( ) {
127127 await Promise . all ( [
128128 loadDashboardStats ( ) ,
129- loadDashboardServices ( ) ,
129+ loadServicesTable ( ) ,
130130 loadEarningsChart ( '7' ) ,
131- loadEarningsBreakdown ( ) ,
132131 ] ) ;
133132
134133 // Auto-refresh every 60 seconds
135134 if ( refreshTimer ) clearInterval ( refreshTimer ) ;
136135 refreshTimer = setInterval ( ( ) => {
137136 loadDashboardStats ( ) ;
138- loadDashboardServices ( ) ;
139- loadEarningsBreakdown ( ) ;
137+ loadServicesTable ( ) ;
140138 } , 60000 ) ;
141139 }
142140
@@ -168,93 +166,125 @@ const CP = (() => {
168166 }
169167 }
170168
171- async function loadDashboardServices ( ) {
172- const container = document . getElementById ( 'services-list ' ) ;
169+ async function loadServicesTable ( ) {
170+ const container = document . getElementById ( 'services-table-container ' ) ;
173171 if ( ! container ) return ;
174172
175173 try {
176- const services = await api ( '/api/services/deployed' ) ;
174+ const [ services , breakdown ] = await Promise . all ( [
175+ api ( '/api/services/deployed' ) ,
176+ api ( '/api/earnings/breakdown' ) . catch ( ( ) => [ ] ) ,
177+ ] ) ;
178+
177179 if ( ! services || services . length === 0 ) {
178180 container . innerHTML = `
179- <div class="empty-state">
180- <div class="empty-state-icon">🚀</div>
181+ <div class="empty-state" style="padding:32px 0; text-align:center;">
181182 <div class="empty-state-title">No services deployed yet</div>
182183 <div class="empty-state-text">Get started by deploying your first passive income service.</div>
183- <a href="/setup" class="btn btn-primary btn-lg ">Setup Wizard</a>
184+ <a href="/setup" class="btn btn-primary" style="margin-top:12px; ">Setup Wizard</a>
184185 </div>` ;
185186 return ;
186187 }
187- container . innerHTML = services . map ( renderServiceCard ) . join ( '' ) ;
188- } catch ( err ) {
188+
189+ // Merge breakdown data into services by slug
190+ const breakdownMap = { } ;
191+ ( breakdown || [ ] ) . forEach ( b => { breakdownMap [ b . platform ] = b ; } ) ;
192+
193+ const rows = services . map ( svc => renderServiceRow ( svc , breakdownMap [ svc . slug ] ) ) . join ( '' ) ;
189194 container . innerHTML = `
190- <div class="empty-state">
191- <div class="empty-state-icon">🚀</div>
192- <div class="empty-state-title">No services deployed yet</div>
193- <div class="empty-state-text">Get started by deploying your first passive income service.</div>
194- <a href="/setup" class="btn btn-primary btn-lg">Setup Wizard</a>
195- </div>` ;
195+ <table class="breakdown-table">
196+ <thead>
197+ <tr>
198+ <th>Service</th>
199+ <th style="text-align:center;">Status</th>
200+ <th style="text-align:center;">Health</th>
201+ <th style="text-align:right;">Balance</th>
202+ <th style="text-align:right;">Change</th>
203+ <th style="text-align:right;">CPU</th>
204+ <th style="text-align:right;">Memory</th>
205+ <th style="text-align:center;">Payout</th>
206+ <th style="text-align:center;">Actions</th>
207+ </tr>
208+ </thead>
209+ <tbody>${ rows } </tbody>
210+ </table>` ;
211+ } catch ( err ) {
212+ container . innerHTML = `<div class="empty-state-text" style="padding:24px 0; text-align:center; color:var(--text-muted);">Failed to load services.</div>` ;
196213 }
197214 }
198215
199- function renderServiceCard ( svc ) {
216+ function renderServiceRow ( svc , bk ) {
200217 const statusClass = ( svc . container_status || 'stopped' ) . toLowerCase ( ) ;
201218 const statusLabel = statusClass . charAt ( 0 ) . toUpperCase ( ) + statusClass . slice ( 1 ) ;
202- const initial = ( svc . name || svc . slug || '?' ) [ 0 ] . toUpperCase ( ) ;
203219
204- // Health score badge
205- let healthBadge = '' ;
220+ // Service name — linked to referral URL if available
221+ const name = escapeHtml ( svc . name ) ;
222+ const nameHtml = svc . referral_url
223+ ? `<a href="${ escapeHtml ( svc . referral_url ) } " target="_blank" rel="noopener" title="Referral link" style="color:var(--accent); text-decoration:none; font-weight:600;">${ name } </a>`
224+ : `<span style="font-weight:600;">${ name } </span>` ;
225+
226+ // Health badge
227+ let healthBadge = '<span style="color:var(--text-muted);">--</span>' ;
206228 if ( svc . health_score !== null && svc . health_score !== undefined ) {
207229 const score = svc . health_score ;
208230 const hClass = score >= 80 ? 'badge-running' : score >= 50 ? 'badge-error' : 'badge-stopped' ;
209- const uptimeStr = svc . uptime_pct !== null ? ` · ${ svc . uptime_pct } % up` : '' ;
210- healthBadge = `<span class="badge ${ hClass } " title="Health ${ score } /100${ uptimeStr } " style="margin-left:6px;">${ score } </span>` ;
231+ healthBadge = `<span class="badge ${ hClass } " title="Health ${ score } /100">${ score } </span>` ;
211232 }
212233
213- // Claim button instead of direct cashout link
214- let claimBtn = '' ;
215- if ( svc . cashout && svc . cashout . dashboard_url ) {
216- const minAmount = parseFloat ( svc . cashout . min_amount ) || 0 ;
217- const balance = parseFloat ( svc . balance ) || 0 ;
218- const canCashout = balance >= minAmount && minAmount > 0 ;
219- claimBtn = `
220- <button class="btn btn-sm ${ canCashout ? 'btn-success' : 'btn-ghost' } "
221- onclick="CP.openClaimModal('${ svc . slug } ')"
222- title="${ canCashout ? 'Eligible for payout' : `Min $${ minAmount } ` } "
223- style="font-size:0.75rem; padding:4px 8px;">
224- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>
225- Claim
226- </button>` ;
227- }
234+ // Balance + delta from breakdown
235+ const balance = ( bk && bk . balance ) || svc . balance || 0 ;
236+ const delta = bk ? bk . delta : 0 ;
237+ const deltaSign = delta > 0 ? '+' : '' ;
238+ const deltaClass = delta > 0 ? 'positive' : delta < 0 ? 'negative' : '' ;
239+ const deltaStr = delta !== 0 ? `${ deltaSign } ${ formatCurrency ( delta ) } ` : '--' ;
228240
229- return `
230- <div class="service-card" data-slug="${ escapeHtml ( svc . slug ) } ">
231- <div class="service-card-header">
232- <div class="service-icon">${ initial } </div>
233- <div>
234- <div class="service-name">${ escapeHtml ( svc . name ) } ${ healthBadge } </div>
235- <span class="badge badge-${ statusClass } "><span class="status-dot ${ statusClass } "></span> ${ statusLabel } </span>
236- </div>
237- </div>
238- <div class="service-stats">
239- <div class="service-balance">${ formatCurrency ( svc . balance || 0 ) } </div>
240- <div class="service-usage">
241- <span>CPU ${ svc . cpu || '0' } %</span>
242- <span>MEM ${ svc . memory || '0 MB' } </span>
243- </div>
241+ // Payout progress
242+ const co = svc . cashout || { } ;
243+ const minAmount = co . min_amount || 0 ;
244+ const eligible = minAmount > 0 && balance >= minAmount ;
245+ const pctToMin = minAmount > 0 ? Math . min ( 100 , ( balance / minAmount ) * 100 ) : 0 ;
246+ const progressBar = minAmount > 0 ? `
247+ <div class="payout-progress" title="${ formatCurrency ( balance ) } / ${ formatCurrency ( minAmount ) } " style="min-width:60px;">
248+ <div class="payout-progress-bar ${ eligible ? 'eligible' : '' } " style="width:${ pctToMin . toFixed ( 0 ) } %"></div>
244249 </div>
245- <div class="service-actions">
250+ <span class="payout-label">${ pctToMin . toFixed ( 0 ) } %</span>
251+ ` : '<span style="color:var(--text-muted);">--</span>' ;
252+
253+ // Action buttons
254+ const claimBtn = co . dashboard_url
255+ ? `<button class="btn btn-sm ${ eligible ? 'btn-success' : 'btn-ghost' } btn-icon" onclick="CP.openClaimModal('${ svc . slug } ')" title="${ eligible ? 'Claim' : 'Details' } ">
256+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>
257+ </button>`
258+ : '' ;
259+
260+ return `
261+ <tr class="breakdown-row" data-slug="${ escapeHtml ( svc . slug ) } ">
262+ <td>${ nameHtml } <div style="font-size:0.7rem; color:var(--text-muted);">${ escapeHtml ( svc . image || '' ) } </div></td>
263+ <td style="text-align:center;"><span class="badge badge-${ statusClass } "><span class="status-dot ${ statusClass } "></span> ${ statusLabel } </span></td>
264+ <td style="text-align:center;">${ healthBadge } </td>
265+ <td style="text-align:right; font-weight:600;">${ formatCurrency ( balance ) } </td>
266+ <td style="text-align:right;"><span class="stat-change ${ deltaClass } ">${ deltaStr } </span></td>
267+ <td style="text-align:right;">${ svc . cpu || '0' } %</td>
268+ <td style="text-align:right;">${ svc . memory || '0 MB' } </td>
269+ <td style="text-align:center;">${ progressBar } </td>
270+ <td style="text-align:center; white-space:nowrap;">
246271 ${ claimBtn }
247272 <button class="btn btn-ghost btn-sm btn-icon" onclick="CP.restartService('${ svc . slug } ')" title="Restart">
248- <svg width="14 " height="14 " viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
273+ <svg width="12 " height="12 " viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
249274 </button>
250275 <button class="btn btn-ghost btn-sm btn-icon" onclick="CP.stopService('${ svc . slug } ')" title="Stop">
251- <svg width="14 " height="14 " viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="6" width="12" height="12" rx="1"/></svg>
276+ <svg width="12 " height="12 " viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="6" width="12" height="12" rx="1"/></svg>
252277 </button>
253278 <button class="btn btn-ghost btn-sm btn-icon" onclick="CP.viewLogs('${ svc . slug } ')" title="Logs">
254- <svg width="14 " height="14 " viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
279+ <svg width="12 " height="12 " viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
255280 </button>
256- </div>
257- </div>` ;
281+ </td>
282+ </tr>` ;
283+ }
284+
285+ function refreshServices ( ) {
286+ loadServicesTable ( ) ;
287+ toast ( 'Services refreshed' , 'info' ) ;
258288 }
259289
260290 async function loadEarningsChart ( days ) {
@@ -345,82 +375,7 @@ const CP = (() => {
345375 // -----------------------------------------------------------
346376 // Earnings Breakdown
347377 // -----------------------------------------------------------
348- async function loadEarningsBreakdown ( ) {
349- const container = document . getElementById ( 'earnings-breakdown' ) ;
350- if ( ! container ) return ;
351-
352- try {
353- const data = await api ( '/api/earnings/breakdown' ) ;
354- if ( ! data || data . length === 0 ) {
355- container . innerHTML = '<div class="empty-state-text" style="padding: 16px 0; text-align: center;">No earnings data yet. Configure collectors in Settings.</div>' ;
356- return ;
357- }
358- const rows = data . map ( renderBreakdownRow ) . join ( '' ) ;
359- container . innerHTML = `
360- <table class="breakdown-table">
361- <thead>
362- <tr>
363- <th>Service</th>
364- <th style="text-align:right;">Balance</th>
365- <th style="text-align:right;">Change</th>
366- <th style="text-align:center;">Payout</th>
367- <th style="text-align:center;">Action</th>
368- </tr>
369- </thead>
370- <tbody>${ rows } </tbody>
371- </table>` ;
372- } catch ( err ) {
373- container . innerHTML = '<div class="empty-state-text" style="padding: 16px 0; text-align: center;">Could not load breakdown.</div>' ;
374- }
375- }
376-
377- function renderBreakdownRow ( svc ) {
378- const initial = ( svc . name || svc . platform || '?' ) [ 0 ] . toUpperCase ( ) ;
379- const deltaSign = svc . delta > 0 ? '+' : '' ;
380- const deltaClass = svc . delta > 0 ? 'positive' : svc . delta < 0 ? 'negative' : '' ;
381- const deltaStr = svc . delta !== 0 ? `${ deltaSign } ${ formatCurrency ( svc . delta ) } ` : '--' ;
382-
383- const co = svc . cashout || { } ;
384- const eligible = co . eligible ;
385- const minAmount = co . min_amount || 0 ;
386- const pctToMin = minAmount > 0 ? Math . min ( 100 , ( svc . balance / minAmount ) * 100 ) : 0 ;
387-
388- // Progress bar toward minimum payout
389- const progressBar = minAmount > 0 ? `
390- <div class="payout-progress" title="${ formatCurrency ( svc . balance ) } / ${ formatCurrency ( minAmount ) } ">
391- <div class="payout-progress-bar ${ eligible ? 'eligible' : '' } " style="width:${ pctToMin . toFixed ( 0 ) } %"></div>
392- </div>
393- <span class="payout-label">${ formatCurrency ( svc . balance ) } / ${ formatCurrency ( minAmount ) } </span>
394- ` : '<span class="payout-label" style="color:var(--text-muted);">N/A</span>' ;
395-
396- const claimBtn = co . dashboard_url
397- ? `<button class="btn btn-sm ${ eligible ? 'btn-success' : 'btn-ghost' } " onclick="CP.openClaimModal('${ escapeHtml ( svc . platform ) } ')" ${ ! eligible ? 'title="Below minimum payout"' : '' } >
398- ${ eligible ? 'Claim' : 'Details' }
399- </button>`
400- : '' ;
401-
402- return `
403- <tr class="breakdown-row">
404- <td>
405- <div style="display:flex; align-items:center; gap:10px;">
406- <div class="service-icon" style="width:32px;height:32px;font-size:0.9rem;">${ initial } </div>
407- <div>
408- <div style="font-weight:600; font-size:0.9rem;">${ escapeHtml ( svc . name ) } </div>
409- <div style="font-size:0.75rem; color:var(--text-muted);">${ escapeHtml ( svc . currency ) } </div>
410- </div>
411- </div>
412- </td>
413- <td style="text-align:right; font-weight:600; font-size:0.95rem;">${ formatCurrency ( svc . balance ) } </td>
414- <td style="text-align:right;"><span class="stat-change ${ deltaClass } ">${ deltaStr } </span></td>
415- <td style="text-align:center;">${ progressBar } </td>
416- <td style="text-align:center;">${ claimBtn } </td>
417- </tr>` ;
418- }
419-
420- function refreshBreakdown ( ) {
421- loadEarningsBreakdown ( ) ;
422- toast ( 'Breakdown refreshed' , 'info' ) ;
423- }
378+ // loadEarningsBreakdown merged into loadServicesTable above
424379
425380 // -----------------------------------------------------------
426381 // Claim Modal
@@ -519,7 +474,7 @@ const CP = (() => {
519474 try {
520475 await api ( `/api/services/${ slug } /restart` , { method : 'POST' } ) ;
521476 toast ( `${ slug } restarting...` , 'success' ) ;
522- loadDashboardServices ( ) ;
477+ loadServicesTable ( ) ;
523478 } catch ( err ) {
524479 toast ( err . message , 'error' ) ;
525480 }
@@ -529,7 +484,7 @@ const CP = (() => {
529484 try {
530485 await api ( `/api/services/${ slug } /stop` , { method : 'POST' } ) ;
531486 toast ( `${ slug } stopped` , 'success' ) ;
532- loadDashboardServices ( ) ;
487+ loadServicesTable ( ) ;
533488 } catch ( err ) {
534489 toast ( err . message , 'error' ) ;
535490 }
@@ -539,7 +494,7 @@ const CP = (() => {
539494 try {
540495 await api ( `/api/services/${ slug } /start` , { method : 'POST' } ) ;
541496 toast ( `${ slug } starting...` , 'success' ) ;
542- loadDashboardServices ( ) ;
497+ loadServicesTable ( ) ;
543498 } catch ( err ) {
544499 toast ( err . message , 'error' ) ;
545500 }
@@ -550,7 +505,7 @@ const CP = (() => {
550505 try {
551506 await api ( `/api/services/${ slug } ` , { method : 'DELETE' } ) ;
552507 toast ( `${ slug } removed` , 'success' ) ;
553- loadDashboardServices ( ) ;
508+ loadServicesTable ( ) ;
554509 } catch ( err ) {
555510 toast ( err . message , 'error' ) ;
556511 }
@@ -1310,7 +1265,7 @@ const CP = (() => {
13101265 testCollectors,
13111266 editCredentials,
13121267 filterCatalog,
1313- refreshBreakdown ,
1268+ refreshServices ,
13141269 openClaimModal,
13151270 } ;
13161271} ) ( ) ;
0 commit comments