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:
@@ -55,8 +55,8 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
|
||||
- **Level 1**: Essential security controls that can be implemented with minimal impact on operations. Recommended as baseline for all organizations.
|
||||
- **Level 2**: Advanced security controls that may restrict functionality or require more effort to implement. Recommended for high-security environments.
|
||||
- **Multiple output formats**: HTML summary, CSV per section/finding, JSON summary, optional XLSX
|
||||
- **File browser**: all generated files stored per report, browsable and downloadable individually
|
||||
- **Tenancy filter**: filter reports by tenancy in the Downloads tab
|
||||
- **Report history**: full execution history with status, tenancy filter, and download actions (HTML/JSON) in the Reports tab
|
||||
- **File browser**: dedicated Downloads tab with expandable cards per completed report, files grouped by category (Summary, CIS Findings, OBP, Raw Data, etc.)
|
||||
- Region filtering with multi-select
|
||||
- Real-time progress tracking with phase-based progress bar
|
||||
- **CIS Engine auto-update**: check for new versions of `cis_reports.py` from Oracle's GitHub repository and update with one click (admin only). Custom patches are automatically reapplied after update
|
||||
@@ -576,7 +576,7 @@ All models include OCID mapping for `us-ashburn-1`. For other regions, use the "
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| **v1.8** | 2026-03 | CIS Engine auto-update from Oracle GitHub with automatic patch reapplication, version check UI card (admin), new `/api/cis-engine/*` endpoints, report file listing and individual download endpoints |
|
||||
| **v1.8** | 2026-03 | CIS Engine auto-update from Oracle GitHub with automatic patch reapplication, version check UI card (admin), new `/api/cis-engine/*` endpoints, report file listing and individual download endpoints, reorganized Reports tab (execution history + status) and Downloads tab (file browser only with expandable cards per report), CIS Level description tooltip, persistent log expand during report generation |
|
||||
| **v1.7** | 2026-03 | Oracle official CIS report engine (replaces lightweight checker), granular report parameters (Level, OBP, Raw Data, Redact), per-report file storage with category browser, tenancy filter in Downloads, individual file download |
|
||||
| **v1.6** | 2026-03 | Granular CIS MCP server (12 per-section scan tools: IAM, Networking, Compute, Logging/Monitoring, Storage, Asset Management), chat memory compaction with LLM-based summarization, GenAI tool use loop fix (accumulated conversation), chat thinking indicator, auto-registered CIS MCP server |
|
||||
| **v1.5** | 2026-03 | MCP Tool Use in Chat (GenAI function calling with auto tool discovery + execution via MCP SDK), multi-table ADB vector search, preview chunks before embedding, enriched embeddings with tenancy/regions/compartments, searchable dropdowns, editable vector tables, orphaned report cleanup on restart |
|
||||
|
||||
@@ -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)">×</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>';
|
||||
|
||||
Reference in New Issue
Block a user