@@ -49,14 +49,15 @@ function navigate(page) {
4949 if ( nav ) nav . classList . add ( 'active' ) ;
5050
5151 // Update page title
52- const titles = { overview :'Overview' , logs :'Live Terminal' , llm :'LLM & Costs' , github :'GitHub Traffic' , users :'Users' } ;
52+ const titles = { overview :'Overview' , logs :'Live Terminal' , llm :'LLM & Costs' , github :'GitHub Traffic' , users :'Users' , scanner : 'Scanner Analytics' } ;
5353 const titleEl = document . getElementById ( 'page-title' ) ;
5454 if ( titleEl ) titleEl . textContent = titles [ page ] || page ;
5555
5656 if ( page === 'overview' ) loadOverview ( ) ;
5757 if ( page === 'llm' ) loadLLM ( ) ;
5858 if ( page === 'github' ) loadGitHub ( ) ;
5959 if ( page === 'users' ) loadUsers ( ) ;
60+ if ( page === 'scanner' ) loadScanner ( ) ;
6061 if ( page === 'logs' && ! _sseSource ) connectSystemLogs ( ) ;
6162}
6263
@@ -75,11 +76,13 @@ async function loadAll() {
7576 loadOverview ( ) ;
7677 loadLLM ( ) ;
7778 loadGitHub ( ) ;
79+ loadScanner ( ) ;
7880 // Auto-refresh every 30s
7981 setInterval ( ( ) => {
8082 if ( currentPage === 'overview' ) loadOverview ( ) ;
8183 if ( currentPage === 'llm' ) loadLLM ( ) ;
8284 if ( currentPage === 'users' ) loadUsers ( ) ;
85+ if ( currentPage === 'scanner' ) loadScanner ( ) ;
8386 } , 30000 ) ;
8487}
8588
@@ -362,6 +365,100 @@ async function loadGitHub() {
362365 document . getElementById ( 'gh-paths-body' ) . innerHTML = pathRows || '<tr><td colspan="3" style="color:var(--text2)">No data</td></tr>' ;
363366}
364367
368+ // ── Scanner page ──────────────────────────────────────────────────
369+ async function loadScanner ( ) {
370+ const data = await apiFetch ( '/scanner/analytics' ) ;
371+ if ( ! data ) return ;
372+
373+ if ( data . error ) {
374+ console . error ( 'Scanner analytics error:' , data . error ) ;
375+ return ;
376+ }
377+
378+ // Update summary cards
379+ const jobs = data . scanner_jobs || { } ;
380+ const repos = data . user_repos || { } ;
381+ const runs = data . scan_runs || { } ;
382+ const stars = data . community_stars || { } ;
383+ const visibility = data . index_visibility || { } ;
384+
385+ document . getElementById ( 'scanner-jobs-total' ) . textContent = formatNum ( jobs . total || 0 ) ;
386+ document . getElementById ( 'scanner-repos-total' ) . textContent = formatNum ( repos . total || 0 ) ;
387+ document . getElementById ( 'scanner-runs-total' ) . textContent = formatNum ( runs . total || 0 ) ;
388+ document . getElementById ( 'scanner-stars-total' ) . textContent = formatNum ( stars . total_stars || 0 ) ;
389+ document . getElementById ( 'scanner-visibility-total' ) . textContent = formatNum ( visibility . total || 0 ) ;
390+ document . getElementById ( 'scanner-stargazers' ) . textContent = formatNum ( stars . unique_users || 0 ) ;
391+
392+ // Recent scanner jobs table
393+ const jobsRows = ( jobs . recent || [ ] ) . map ( j => {
394+ const statusBadge = ( status ) => {
395+ const color = status === 'complete' ? 'var(--green)' : status === 'failed' ? 'var(--red)' : status === 'running' ? 'var(--amber)' : 'var(--text2)' ;
396+ return `<span style="color:${ color } ;font-weight:600">${ status || 'N/A' } </span>` ;
397+ } ;
398+ const updated = j . updated_at ? formatDateShort ( j . updated_at ) : '—' ;
399+ return `<tr>
400+ <td style="font-family:'JetBrains Mono',monospace;font-size:11px">${ escHtml ( j . job_id || 'N/A' ) } </td>
401+ <td>${ escHtml ( j . username || 'N/A' ) } </td>
402+ <td>${ escHtml ( j . org || '' ) } /${ escHtml ( j . repo || '' ) } </td>
403+ <td>${ statusBadge ( j . phase1_status ) } </td>
404+ <td>${ statusBadge ( j . phase2_status ) } </td>
405+ <td>${ updated } </td>
406+ </tr>` ;
407+ } ) . join ( '' ) ;
408+ document . getElementById ( 'scanner-jobs-body' ) . innerHTML = jobsRows || '<tr><td colspan="6" style="color:var(--text2)">No scanner jobs found</td></tr>' ;
409+
410+ // Recent scan runs table
411+ const runsRows = ( runs . latest || [ ] ) . map ( r => {
412+ const status = r . status || 'unknown' ;
413+ const statusColor = status === 'success' ? 'var(--green)' : status === 'failed' ? 'var(--red)' : 'var(--text2)' ;
414+ const scanned = r . last_scanned_at ? formatDateShort ( r . last_scanned_at ) : '—' ;
415+ return `<tr>
416+ <td>${ escHtml ( r . org_id || 'N/A' ) } </td>
417+ <td>${ escHtml ( r . repo || 'N/A' ) } </td>
418+ <td style="color:${ statusColor } ;font-weight:600">${ status } </td>
419+ <td>${ scanned } </td>
420+ </tr>` ;
421+ } ) . join ( '' ) ;
422+ document . getElementById ( 'scanner-runs-body' ) . innerHTML = runsRows || '<tr><td colspan="4" style="color:var(--text2)">No scan runs found</td></tr>' ;
423+
424+ // Top starred repos table
425+ const starsRows = ( stars . top_repos || [ ] ) . map ( s => {
426+ const org = ( s . _id && s . _id . org ) || 'N/A' ;
427+ const repo = ( s . _id && s . _id . repo ) || 'N/A' ;
428+ return `<tr>
429+ <td>${ escHtml ( org ) } </td>
430+ <td>${ escHtml ( repo ) } </td>
431+ <td style="color:#fbbf24;font-weight:600">${ s . star_count || 0 } </td>
432+ </tr>` ;
433+ } ) . join ( '' ) ;
434+ document . getElementById ( 'scanner-stars-body' ) . innerHTML = starsRows || '<tr><td colspan="3" style="color:var(--text2)">No stars data found</td></tr>' ;
435+
436+ // Repos per user table
437+ const reposPerUserRows = ( repos . per_user || [ ] ) . map ( u =>
438+ `<tr><td>${ escHtml ( u . _id || 'N/A' ) } </td><td>${ u . repo_count || 0 } </td></tr>`
439+ ) . join ( '' ) ;
440+ document . getElementById ( 'scanner-repos-per-user-body' ) . innerHTML = reposPerUserRows || '<tr><td colspan="2" style="color:var(--text2)">No user repos found</td></tr>' ;
441+
442+ // Recent user repos table
443+ const recentReposRows = ( repos . recent || [ ] ) . map ( r =>
444+ `<tr>
445+ <td>${ escHtml ( r . username || 'N/A' ) } </td>
446+ <td>${ escHtml ( r . github_org || 'N/A' ) } </td>
447+ <td>${ escHtml ( r . repo || 'N/A' ) } </td>
448+ <td>${ escHtml ( r . branch || 'main' ) } </td>
449+ </tr>`
450+ ) . join ( '' ) ;
451+ document . getElementById ( 'scanner-recent-repos-body' ) . innerHTML = recentReposRows || '<tr><td colspan="4" style="color:var(--text2)">No recent repos found</td></tr>' ;
452+
453+ // Job status breakdown table
454+ const statusRows = ( jobs . by_status || [ ] ) . map ( s => {
455+ const p1 = ( s . _id && s . _id . phase1 ) || 'N/A' ;
456+ const p2 = ( s . _id && s . _id . phase2 ) || 'N/A' ;
457+ return `<tr><td>${ escHtml ( p1 ) } </td><td>${ escHtml ( p2 ) } </td><td>${ s . count || 0 } </td></tr>` ;
458+ } ) . join ( '' ) ;
459+ document . getElementById ( 'scanner-job-status-body' ) . innerHTML = statusRows || '<tr><td colspan="3" style="color:var(--text2)">No status data found</td></tr>' ;
460+ }
461+
365462
366463// ═════════════════════════════════════════════════════════════════
367464// Live Logs — SSE (Server-Sent Events) + journalctl subprocess
0 commit comments