@@ -3659,9 +3659,12 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId) {
36593659 <table class="dashboard-table" style="margin:0;table-layout:fixed;width:100%">
36603660 <thead style="position:sticky;top:0;z-index:2;background:rgba(2,6,23,.97);backdrop-filter:blur(4px)">
36613661 <tr>
3662- <th style="width:32%">TARGET</th>
3662+ <th style="width:36px;text-align:center;padding-left:10px">
3663+ <input type="checkbox" id="findings-select-all" title="Select all" style="width:14px;height:14px;accent-color:var(--accent-cyan);cursor:pointer">
3664+ </th>
3665+ <th style="width:31%">TARGET</th>
36633666 <th style="width:8%;text-align:center">SEV</th>
3664- <th style="width:44 %">VULNERABILITY TYPE</th>
3667+ <th style="width:43 %">VULNERABILITY TYPE</th>
36653668 <th style="width:16%">MODULE</th>
36663669 </tr>
36673670 </thead>
@@ -3742,7 +3745,10 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId) {
37423745 href = 'https://' + target ;
37433746 }
37443747
3745- return `<tr class="findings-row" data-target="${ escAttr ( target ) } " data-finding="${ escAttr ( vulnType ) } " data-severity="${ escAttr ( sev ) } " data-module="${ escAttr ( r . module || '' ) } " data-href="${ escAttr ( href ) } " style="${ idx % 2 ? 'background:rgba(255,255,255,.012)' : '' } " title="Right-click for options">
3748+ return `<tr class="findings-row" data-target="${ escAttr ( target ) } " data-finding="${ escAttr ( vulnType ) } " data-severity="${ escAttr ( sev ) } " data-module="${ escAttr ( r . module || '' ) } " data-href="${ escAttr ( href ) } " style="cursor:pointer;${ idx % 2 ? 'background:rgba(255,255,255,.012)' : '' } ">
3749+ <td style="padding:7px 10px;width:36px;text-align:center">
3750+ <input type="checkbox" class="finding-chk" style="width:14px;height:14px;accent-color:var(--accent-cyan);cursor:pointer" onclick="event.stopPropagation()">
3751+ </td>
37463752 <td style="padding:7px 10px;max-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
37473753 <a href="${ esc ( href ) } " target="_blank" rel="noopener"
37483754 onclick="event.stopPropagation()"
@@ -3780,7 +3786,7 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId) {
37803786 <span style="color:${ modInfo . color } ;font-size:11px;font-weight:500">${ modInfo . icon } ${ esc ( modInfo . name ) } </span>
37813787 </td>
37823788 </tr>` ;
3783- } ) . join ( '' ) : '<tr><td colspan="4 " style="text-align:center;padding:28px;color:var(--text-muted);font-size:13px">No findings match the current filter.</td></tr>' ;
3789+ } ) . join ( '' ) : '<tr><td colspan="5 " style="text-align:center;padding:28px;color:var(--text-muted);font-size:13px">No findings match the current filter.</td></tr>' ;
37843790 }
37853791 if ( cap ) {
37863792 cap . style . display = filtered . length > maxRows ? 'block' : 'none' ;
@@ -3849,70 +3855,123 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId) {
38493855 if ( titleInput ) titleInput . addEventListener ( 'input' , applyFiltersDebounced ) ;
38503856 if ( severitySel ) severitySel . addEventListener ( 'change' , applyFilters ) ;
38513857
3852- // ── Right-click context menu on findings rows ──────────────────────────────
3853- let _ctxMenu = document . getElementById ( 'findings-ctx-menu' ) ;
3854- if ( ! _ctxMenu ) {
3855- _ctxMenu = document . createElement ( 'div' ) ;
3856- _ctxMenu . id = 'findings-ctx-menu' ;
3857- _ctxMenu . style . cssText = 'position:fixed;z-index:9999;background:var(--bg-card,#1e293b);border:1px solid var(--border,#334155);border-radius:10px;padding:4px 0;min-width:190px;box-shadow:0 8px 32px rgba(0,0,0,.6);display:none' ;
3858- _ctxMenu . innerHTML = `
3859- <div class="ctx-item" id="ctx-copy-target" style="padding:9px 16px;font-size:12px;cursor:pointer;color:var(--text-primary);display:flex;align-items:center;gap:8px">📋 Copy Target</div>
3860- <div class="ctx-item" id="ctx-copy-finding" style="padding:9px 16px;font-size:12px;cursor:pointer;color:var(--text-primary);display:flex;align-items:center;gap:8px">📝 Copy Finding</div>
3861- <div class="ctx-item" id="ctx-open-url" style="padding:9px 16px;font-size:12px;cursor:pointer;color:var(--accent-cyan);display:flex;align-items:center;gap:8px">🌐 Open in Browser</div>
3862- <div style="border-top:1px solid var(--border);margin:4px 0"></div>
3863- <div class="ctx-item" id="ctx-validate-ai" style="padding:9px 16px;font-size:12px;cursor:pointer;color:#a78bfa;display:flex;align-items:center;gap:8px">🤖 Validate with AI</div>` ;
3864- document . body . appendChild ( _ctxMenu ) ;
3865- // Hover styles
3866- _ctxMenu . querySelectorAll ( '.ctx-item' ) . forEach ( el => {
3867- el . addEventListener ( 'mouseenter' , ( ) => el . style . background = 'rgba(255,255,255,.06)' ) ;
3868- el . addEventListener ( 'mouseleave' , ( ) => el . style . background = '' ) ;
3858+ // ── Selection toolbar ─────────────────────────────────────────────────────
3859+ // Build the floating toolbar (once, outside the root so it stays on DOM)
3860+ let _selToolbar = document . getElementById ( 'findings-sel-toolbar' ) ;
3861+ if ( ! _selToolbar ) {
3862+ _selToolbar = document . createElement ( 'div' ) ;
3863+ _selToolbar . id = 'findings-sel-toolbar' ;
3864+ _selToolbar . style . cssText = [
3865+ 'position:fixed;bottom:28px;left:50%;transform:translateX(-50%) translateY(80px)' ,
3866+ 'z-index:9000;background:var(--bg-card,#1e293b)' ,
3867+ 'border:1px solid var(--accent-cyan,#22d3ee)44' ,
3868+ 'border-radius:14px;padding:10px 18px' ,
3869+ 'display:flex;align-items:center;gap:12px' ,
3870+ 'box-shadow:0 8px 40px rgba(0,0,0,.7)' ,
3871+ 'transition:transform .25s cubic-bezier(.34,1.56,.64,1),opacity .2s' ,
3872+ 'opacity:0;pointer-events:none;white-space:nowrap' ,
3873+ ] . join ( ';' ) ;
3874+ _selToolbar . innerHTML = `
3875+ <span id="sel-count-badge" style="background:var(--accent-cyan,#22d3ee);color:#0f172a;font-size:11px;font-weight:700;padding:3px 9px;border-radius:20px;min-width:22px;text-align:center">0</span>
3876+ <span style="font-size:12px;color:var(--text-secondary)">selected</span>
3877+ <div style="width:1px;height:20px;background:var(--border)"></div>
3878+ <button id="sel-copy-targets" title="Copy all selected targets" style="background:transparent;border:1px solid var(--border);border-radius:8px;padding:5px 12px;font-size:12px;color:var(--text-primary);cursor:pointer">📋 Copy Targets</button>
3879+ <button id="sel-copy-findings" title="Copy all selected finding types" style="background:transparent;border:1px solid var(--border);border-radius:8px;padding:5px 12px;font-size:12px;color:var(--text-primary);cursor:pointer">📝 Copy Findings</button>
3880+ <button id="sel-open-urls" title="Open all selected targets in new tabs" style="background:transparent;border:1px solid var(--border);border-radius:8px;padding:5px 12px;font-size:12px;color:var(--accent-cyan,#22d3ee);cursor:pointer">🌐 Open URLs</button>
3881+ <button id="sel-validate-ai" title="AI-validate the first selected finding" style="background:rgba(167,139,250,.15);border:1px solid #a78bfa44;border-radius:8px;padding:5px 12px;font-size:12px;color:#a78bfa;cursor:pointer">🤖 Validate with AI</button>
3882+ <button id="sel-clear" title="Clear selection" style="background:transparent;border:none;font-size:17px;color:var(--text-muted);cursor:pointer;padding:0 2px;line-height:1">✕</button>` ;
3883+ document . body . appendChild ( _selToolbar ) ;
3884+
3885+ // Hover effect on toolbar buttons
3886+ _selToolbar . querySelectorAll ( 'button' ) . forEach ( b => {
3887+ if ( b . id === 'sel-clear' ) return ;
3888+ b . addEventListener ( 'mouseenter' , ( ) => b . style . opacity = '0.8' ) ;
3889+ b . addEventListener ( 'mouseleave' , ( ) => b . style . opacity = '1' ) ;
38693890 } ) ;
38703891 }
3871- let _ctxRow = null ;
3872- const closeCtx = ( ) => { _ctxMenu . style . display = 'none' ; } ;
3873- document . addEventListener ( 'click' , e => {
3874- if ( ! _ctxMenu . contains ( e . target ) ) {
3875- closeCtx ( ) ;
3876- _ctxRow = null ;
3892+
3893+ const _getSelectedRows = ( ) => Array . from ( root . querySelectorAll ( '.finding-chk:checked' ) ) . map ( cb => cb . closest ( '.findings-row' ) ) . filter ( Boolean ) ;
3894+
3895+ const _updateToolbar = ( ) => {
3896+ const rows = _getSelectedRows ( ) ;
3897+ const n = rows . length ;
3898+ const badge = _selToolbar . querySelector ( '#sel-count-badge' ) ;
3899+ if ( badge ) badge . textContent = String ( n ) ;
3900+ if ( n > 0 ) {
3901+ _selToolbar . style . opacity = '1' ;
3902+ _selToolbar . style . pointerEvents = 'auto' ;
3903+ _selToolbar . style . transform = 'translateX(-50%) translateY(0)' ;
3904+ } else {
3905+ _selToolbar . style . opacity = '0' ;
3906+ _selToolbar . style . pointerEvents = 'none' ;
3907+ _selToolbar . style . transform = 'translateX(-50%) translateY(80px)' ;
3908+ }
3909+ } ;
3910+
3911+ // Delegate checkbox change events from the findings table
3912+ root . addEventListener ( 'change' , e => {
3913+ if ( e . target . classList . contains ( 'finding-chk' ) || e . target . id === 'findings-select-all' ) {
3914+ if ( e . target . id === 'findings-select-all' ) {
3915+ root . querySelectorAll ( '.finding-chk' ) . forEach ( cb => cb . checked = e . target . checked ) ;
3916+ }
3917+ _updateToolbar ( ) ;
38773918 }
38783919 } ) ;
3879- document . addEventListener ( 'keydown' , e => { if ( e . key === 'Escape' ) { closeCtx ( ) ; _ctxRow = null ; } } ) ;
38803920
3881- root . addEventListener ( 'contextmenu' , e => {
3921+ // Clicking a row (not a link/checkbox) toggles its checkbox
3922+ root . addEventListener ( 'click' , e => {
38823923 const row = e . target . closest ( '.findings-row' ) ;
3883- if ( ! row || ! root . contains ( row ) ) return ;
3884- e . preventDefault ( ) ;
3885- _ctxRow = row ;
3886- // Position menu
3887- const x = Math . min ( e . clientX , window . innerWidth - 210 ) ;
3888- const y = Math . min ( e . clientY , window . innerHeight - 160 ) ;
3889- _ctxMenu . style . left = x + 'px' ;
3890- _ctxMenu . style . top = y + 'px' ;
3891- _ctxMenu . style . display = 'block' ;
3924+ if ( ! row ) return ;
3925+ if ( e . target . tagName === 'A' || e . target . tagName === 'INPUT' ) return ;
3926+ const chk = row . querySelector ( '.finding-chk' ) ;
3927+ if ( chk ) { chk . checked = ! chk . checked ; _updateToolbar ( ) ; }
3928+ } ) ;
3929+
3930+ // Toolbar actions
3931+ _selToolbar . querySelector ( '#sel-copy-targets' ) . addEventListener ( 'click' , async ( ) => {
3932+ const rows = _getSelectedRows ( ) ;
3933+ if ( ! rows . length ) return ;
3934+ const text = rows . map ( r => r . dataset . target || '' ) . filter ( Boolean ) . join ( '\n' ) ;
3935+ await copyToClipboard ( text ) . catch ( ( ) => { } ) ;
3936+ showToast ( 'success' , 'Copied' , `${ rows . length } target(s) copied` ) ;
38923937 } ) ;
38933938
3894- _ctxMenu . querySelector ( '#ctx-copy-target' ) ?. addEventListener ( 'click' , async ( ) => {
3895- if ( ! _ctxRow ) return ; closeCtx ( ) ;
3896- await copyToClipboard ( _ctxRow . dataset . target || '' ) . catch ( ( ) => { } ) ;
3897- showToast ( 'success' , 'Copied' , _ctxRow . dataset . target || '' ) ;
3939+ _selToolbar . querySelector ( '#sel-copy-findings' ) . addEventListener ( 'click' , async ( ) => {
3940+ const rows = _getSelectedRows ( ) ;
3941+ if ( ! rows . length ) return ;
3942+ const text = rows . map ( r => r . dataset . finding || '' ) . filter ( Boolean ) . join ( '\n' ) ;
3943+ await copyToClipboard ( text ) . catch ( ( ) => { } ) ;
3944+ showToast ( 'success' , 'Copied' , `${ rows . length } finding type(s) copied` ) ;
38983945 } ) ;
3899- _ctxMenu . querySelector ( '#ctx-copy-finding' ) ?. addEventListener ( 'click' , async ( ) => {
3900- if ( ! _ctxRow ) return ; closeCtx ( ) ;
3901- await copyToClipboard ( _ctxRow . dataset . finding || '' ) . catch ( ( ) => { } ) ;
3902- showToast ( 'success' , 'Copied' , _ctxRow . dataset . finding || '' ) ;
3946+
3947+ _selToolbar . querySelector ( '#sel-open-urls' ) . addEventListener ( 'click' , ( ) => {
3948+ const rows = _getSelectedRows ( ) ;
3949+ if ( ! rows . length ) return ;
3950+ let opened = 0 ;
3951+ rows . forEach ( r => {
3952+ const href = r . dataset . href ;
3953+ if ( href && href !== '#' ) { window . open ( href , '_blank' , 'noopener' ) ; opened ++ ; }
3954+ } ) ;
3955+ if ( ! opened ) showToast ( 'error' , 'No URLs' , 'None of the selected rows have valid URLs.' ) ;
3956+ else showToast ( 'success' , 'Opened' , `${ opened } URL(s) opened in new tabs` ) ;
39033957 } ) ;
3904- _ctxMenu . querySelector ( '#ctx-open-url' ) ?. addEventListener ( 'click' , ( ) => {
3905- if ( ! _ctxRow ) return ; closeCtx ( ) ;
3906- const href = _ctxRow . dataset . href ;
3907- if ( href && href !== '#' ) window . open ( href , '_blank' , 'noopener' ) ;
3908- else showToast ( 'error' , 'No URL' , 'Target has no valid URL.' ) ;
3958+
3959+ _selToolbar . querySelector ( '#sel-validate-ai' ) . addEventListener ( 'click' , ( ) => {
3960+ const rows = _getSelectedRows ( ) ;
3961+ if ( ! rows . length ) return ;
3962+ const r = rows [ 0 ] ; // validate the first selected row
3963+ openValidateModal ( r . dataset . target , r . dataset . finding , r . dataset . severity , r . dataset . module ) ;
39093964 } ) ;
3910- _ctxMenu . querySelector ( '#ctx-validate-ai' ) ?. addEventListener ( 'click' , ( ) => {
3911- if ( ! _ctxRow ) return ; closeCtx ( ) ;
3912- openValidateModal ( _ctxRow . dataset . target , _ctxRow . dataset . finding , _ctxRow . dataset . severity , _ctxRow . dataset . module ) ;
3965+
3966+ _selToolbar . querySelector ( '#sel-clear' ) . addEventListener ( 'click' , ( ) => {
3967+ root . querySelectorAll ( '.finding-chk' ) . forEach ( cb => cb . checked = false ) ;
3968+ const sa = root . querySelector ( '#findings-select-all' ) ;
3969+ if ( sa ) sa . checked = false ;
3970+ _updateToolbar ( ) ;
39133971 } ) ;
39143972}
39153973
3974+
39163975// ── AI Validate Finding Modal ─────────────────────────────────────────────────
39173976function openValidateModal ( target , findingType , severity , module_ ) {
39183977 let modal = document . getElementById ( 'validate-finding-modal' ) ;
0 commit comments