refactor: reorganize Reports and Downloads tabs, update README

Move report history/status table to Reports tab. Simplify Downloads tab
to show only completed reports with expandable file browser grouped by
category. Add CIS Level description tooltip and fix log expand persisting
during report generation polling.
This commit is contained in:
nogueiraguh
2026-03-05 11:55:05 -03:00
parent 3cef21b6dd
commit 1d52234b1a
2 changed files with 43 additions and 22 deletions

View File

@@ -218,7 +218,7 @@ const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],g
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
rptOciOpen:false,rptOciFilter:'',rptOciVal:'',rptRselOpen:false,rptRselFilter:'',rptRselVal:'',ociFormRegOpen:false,ociFormRegFilter:'',ociFormRegVal:'',
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',
cisVer:null,cisCheckResult:null,cisUpdating:false};
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:''};
const API='/api';
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
@@ -406,7 +406,7 @@ function rReport(){const comp=S.reports.filter(r=>r.status==='completed');
const m=last.match(/\[(\d+)\/(\d+)\]/);const pct=m?Math.round((parseInt(m[1])/parseInt(m[2]))*100):0;
progressHtml=`<div id="rpt-progress">${rProgressCard('running',last,pct,lines,S.trackingReportId)}</div>`;
if(!_rptPollTimer)startRptPoll()}
if(!comp.length)return`${progressHtml}<div class="cd"><div class="ct">📊 Compliance Reports</div><div class="emp"><div class="eic">📄</div><p>Nenhum relatório gerado ainda.</p></div></div>${rRunRpt()}`;
if(!comp.length)return`${progressHtml}<div class="cd"><div class="ct">📊 Compliance Reports</div><div class="emp"><div class="eic">📄</div><p>Nenhum relatório gerado ainda.</p></div></div>${rRunRpt()}${rReportHistory()}`;
const lt=comp[0];
const selRpt=S.rptRselVal?comp.find(r=>r.id===S.rptRselVal):lt;
if(!S.rptRselVal)S.rptRselVal=lt.id;
@@ -421,7 +421,7 @@ ${S.rptRselOpen?`<div class="mdrop-list" style="position:absolute;top:100%;left:
${rselDdItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhum relatório encontrado</div>'}
</div>`:''}
</div></div>
<iframe class="rif" id="rfr" src="${API}/reports/${(selRpt||lt).id}/html"></iframe>${rRunRpt()}`}
<iframe class="rif" id="rfr" src="${API}/reports/${(selRpt||lt).id}/html"></iframe>${rRunRpt()}${rReportHistory()}`}
function rRunRpt(){if(S.user?.role==='viewer')return'';
const selTags=S.rptSelRegions.map(r=>`<span class="tag" style="background:var(--b2);font-size:.62rem;display:inline-flex;align-items:center;gap:.2rem">${r}<span onclick="rptRemoveRegion('${r}')" style="cursor:pointer;font-weight:700;color:var(--err)">&times;</span></span>`).join(' ');
const f=S.rptRegionFilter.toLowerCase();
@@ -509,7 +509,9 @@ async function pollRptProgress(){if(!S.trackingReportId)return stopRptPoll();
if(ft.includes('writing cis reports'))pct=75;
if(ft.includes('best practices'))pct=85;
if(ft.includes('finished'))pct=100;
el.innerHTML=rProgressCard(p.status,last,pct,lines,S.trackingReportId)}
const wasOpen=el.querySelector('details[open]')!==null;
el.innerHTML=rProgressCard(p.status,last,pct,lines,S.trackingReportId);
if(wasOpen){const d=el.querySelector('details');if(d)d.open=true}}
if(p.status==='completed'||p.status==='failed'||p.status==='cancelled'){stopRptPoll();S.trackingReportId=null;S.reports=await $api('/reports');R()}}catch(e){}}
async function cancelRpt(rid){if(!confirm('Cancelar este relatório?'))return;
try{await $api('/reports/'+rid+'/cancel',{method:'POST'});stopRptPoll();S.trackingReportId=null;S.reports=await $api('/reports');R()}catch(e){alert('Erro: '+e.message)}}
@@ -525,33 +527,52 @@ function rProgressCard(status,lastStep,pct,lines,rid){
${lines.length>1?`<details style="margin-top:.35rem"><summary style="font-size:.68rem;color:var(--t4);cursor:pointer">Ver log completo</summary><pre style="font-size:.62rem;color:var(--t4);margin-top:.25rem;max-height:120px;overflow:auto;white-space:pre-wrap">${lines.join('\n')}</pre></details>`:''}
</div></div>`}
/* ── Downloads ── */
function rDl(){
/* ── Report History (inside Reports tab) ── */
function rReportHistory(){
const tenancies=[...new Set(S.reports.map(r=>r.tenancy_name))].sort();
const filtered=S.dlTenancyFilter?S.reports.filter(r=>r.tenancy_name===S.dlTenancyFilter):S.reports;
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.55rem;gap:.5rem;flex-wrap:wrap">
<div style="display:flex;align-items:center;gap:.5rem">
<select style="font-size:.76rem;min-width:180px" onchange="S.dlTenancyFilter=this.value;S.dlExpandedRid=null;R()">
const filtered=S.rptHistFilter?S.reports.filter(r=>r.tenancy_name===S.rptHistFilter):S.reports;
if(!filtered.length&&!S.reports.length)return'';
return`<div class="cd" style="margin-top:.85rem"><div class="ct">📋 Histórico de Relatórios</div>
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem">
<select style="font-size:.76rem;min-width:180px" onchange="S.rptHistFilter=this.value;R()">
<option value="">Todas as Tenancies (${S.reports.length})</option>
${tenancies.map(t=>`<option value="${t}" ${S.dlTenancyFilter===t?'selected':''}>${t} (${S.reports.filter(r=>r.tenancy_name===t).length})</option>`).join('')}
${tenancies.map(t=>`<option value="${t}" ${S.rptHistFilter===t?'selected':''}>${t} (${S.reports.filter(r=>r.tenancy_name===t).length})</option>`).join('')}
</select>
<span style="font-size:.72rem;color:var(--t4)">${filtered.length} report(s)</span></div>
<button class="btn bs bsm" onclick="refreshDl()">🔄 Atualizar</button></div>
<div class="cd"><table><thead><tr><th>Tenancy</th><th>Level</th><th>Opções</th><th>Status</th><th>Executado</th><th>Concluído</th><th>Arquivos</th><th>Ações</th></tr></thead>
<table><thead><tr><th>Tenancy</th><th>Level</th><th>Opções</th><th>Status</th><th>Executado</th><th>Concluído</th><th>Ações</th></tr></thead>
<tbody>${filtered.map(r=>{
const opts=[];if(r.obp_checks)opts.push('OBP');if(r.raw_data)opts.push('Raw');if(r.redact_output)opts.push('Redact');
const isExp=S.dlExpandedRid===r.id;
const fHtml=isExp&&S.reportFiles[r.id]?renderFileList(r.id,S.reportFiles[r.id]):'';
return`<tr><td><strong>${r.tenancy_name}</strong></td>
<td style="font-size:.72rem">${r.level||2}</td>
<td style="font-size:.68rem">${opts.join(', ')||'—'}</td>
<td><span class="badge ${r.status==='completed'?'b-pass':r.status==='failed'||r.status==='cancelled'?'b-fail':'b-pend'}">${r.status==='cancelled'?'cancelado':r.status}</span></td>
<td style="font-size:.72rem">${r.created_at||'—'}</td>
<td style="font-size:.72rem">${r.completed_at||'—'}</td>
<td>${r.status==='completed'?`<button class="btn bs bsm" style="font-size:.68rem" onclick="toggleFiles('${r.id}')">${isExp?'Ocultar':'Ver'} Arquivos</button>`:'—'}</td>
<td>${r.status==='completed'?`<button class="btn bs bsm" onclick="dlRpt('${r.id}','html')">HTML</button> <button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button>`:r.status==='running'?`<button class="btn bs bsm" style="color:var(--err);border-color:var(--err)" onclick="cancelRpt('${r.id}')">Cancelar</button>`:''}</td></tr>
${isExp?`<tr><td colspan="8" style="padding:0">${fHtml}</td></tr>`:''}`}).join('')}</tbody></table>
${!filtered.length?'<div class="emp"><div class="eic">📂</div><p>Nenhum relatório.</p></div>':''}</div>`}
<td>${r.status==='completed'?`<button class="btn bs bsm" onclick="dlRpt('${r.id}','html')">HTML</button> <button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button>`:r.status==='running'?`<button class="btn bs bsm" style="color:var(--err);border-color:var(--err)" onclick="cancelRpt('${r.id}')">Cancelar</button>`:'—'}</td></tr>`}).join('')}</tbody></table>
${!filtered.length?'<div class="emp" style="padding:.5rem"><p style="font-size:.72rem;color:var(--t4)">Nenhum relatório.</p></div>':''}</div>`}
/* ── Downloads (file browser only) ── */
function rDl(){
const comp=S.reports.filter(r=>r.status==='completed');
const tenancies=[...new Set(comp.map(r=>r.tenancy_name))].sort();
const filtered=S.dlTenancyFilter?comp.filter(r=>r.tenancy_name===S.dlTenancyFilter):comp;
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.55rem;gap:.5rem;flex-wrap:wrap">
<div style="display:flex;align-items:center;gap:.5rem">
<select style="font-size:.76rem;min-width:180px" onchange="S.dlTenancyFilter=this.value;S.dlExpandedRid=null;R()">
<option value="">Todas as Tenancies (${comp.length})</option>
${tenancies.map(t=>`<option value="${t}" ${S.dlTenancyFilter===t?'selected':''}>${t} (${comp.filter(r=>r.tenancy_name===t).length})</option>`).join('')}
</select>
<span style="font-size:.72rem;color:var(--t4)">${filtered.length} report(s)</span></div>
<button class="btn bs bsm" onclick="refreshDl()">🔄 Atualizar</button></div>
${!filtered.length?'<div class="cd"><div class="emp"><div class="eic">📂</div><p>Nenhum relatório concluído.</p></div></div>':`
<div style="display:flex;flex-direction:column;gap:.5rem">${filtered.map(r=>{
const isExp=S.dlExpandedRid===r.id;
const fHtml=isExp&&S.reportFiles[r.id]?renderFileList(r.id,S.reportFiles[r.id]):'';
return`<div class="cd" style="padding:0;overflow:hidden">
<div style="display:flex;justify-content:space-between;align-items:center;padding:.6rem .8rem;cursor:pointer" onclick="toggleFiles('${r.id}')">
<div><strong style="font-size:.8rem">${r.tenancy_name}</strong><span style="font-size:.7rem;color:var(--t4);margin-left:.5rem">Level ${r.level||2}${r.created_at}</span></div>
<span style="font-size:.7rem;color:var(--ac)">${isExp?'▲ Ocultar':'▼ Ver Arquivos'}</span></div>
${isExp?fHtml:''}</div>`}).join('')}</div>`}`}
async function toggleFiles(rid){if(S.dlExpandedRid===rid){S.dlExpandedRid=null;R();return}
S.dlExpandedRid=rid;if(!S.reportFiles[rid]){try{S.reportFiles[rid]=await $api('/reports/'+rid+'/files')}catch(e){S.reportFiles[rid]=[]}}R()}
function renderFileList(rid,files){if(!files.length)return'<div style="padding:.6rem;color:var(--t4);font-size:.72rem">Nenhum arquivo encontrado</div>';