1/*! 2 * ET Ducky â Smart Reports Module 3 * Natural language query bar â structured visualization 4 * Follow-up analysis via SSE streaming with correlated data 5 * Uses: Chart.js (self-hosted), Clerk auth, existing app module system 6 */ 7(function(){'use strict';const COLORS=['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#06b6d4','#ec4899','#84cc16','#f97316','#6366f1',];window.etduckySmartReports=new class{constructor(){this.chart=null;this.suggestions=[];this.recentQueries=[];this.isLoading=false;this.isAnalyzing=false;this.lastResponse=null;this.analysisAbortController=null;} 8async init(){this.bindEvents();await Promise.all([this.loadSuggestions(),this.loadRecentQueries()]);} 9async getToken(){const s=window.app?.clerk?.session||window.Clerk?.session;if(s&&typeof s.getToken==='function')return s.getToken();for(let i=0;i<20;i++){await new Promise(r=>setTimeout(r,150));const x=window.app?.clerk?.session||window.Clerk?.session;if(x&&typeof x.getToken==='function')return x.getToken();}return'';} 10async apiFetch(endpoint,opts={}){const token=await this.getToken();const res=await fetch((window.ETDuckyConfig?.API_BASE_URL||'')+endpoint,{...opts,headers:{Authorization:'Bearer '+token,'Content-Type':'application/json',...(opts.headers||{})},});if(!res.ok){const body=await res.json().catch(()=>({}));throw new Error(body.error||body.message||'API error '+res.status);} 11return res.json();} 12bindEvents(){const recentSelect=document.getElementById('sr-recent-select');document.getElementById('sr-send-btn')?.addEventListener('click',()=>this.submitQuestion());document.getElementById('sr-input')?.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();this.submitQuestion();}});document.querySelectorAll('.sr-chart-toggle-btn').forEach(b=>b.addEventListener('click',()=>this.switchChartType(b.dataset.chartType)));document.getElementById('sr-toggle-table')?.addEventListener('click',()=>this.toggleTable());document.getElementById('sr-toggle-sql')?.addEventListener('click',()=>this.toggleSql());document.getElementById('sr-analyze-btn')?.addEventListener('click',()=>this.submitAnalysis());document.getElementById('sr-analyze-input')?.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();this.submitAnalysis();}});document.getElementById('sr-analyze-cancel')?.addEventListener('click',()=>this.cancelAnalysis());recentSelect?.addEventListener('change',()=>{const q=recentSelect.value?.trim();if(!q)return;this.askQuestion(q);recentSelect.value='';});} 13submitQuestion(){const q=document.getElementById('sr-input')?.value?.trim();if(q&&!this.isLoading)this.askQuestion(q);} 14async askQuestion(question){if(this.isLoading)return;const input=document.getElementById('sr-input');if(input)input.value=question;this.setLoading(true);this.showResultsArea();this.clearResults();try{const resp=await this.apiFetch('/reports/smart-query',{method:'POST',body:JSON.stringify({question,viewerTimeZone:Intl.DateTimeFormat().resolvedOptions().timeZone}),});this.lastResponse=resp;if(resp.error){this.showError(resp.error);return;} 15this.renderResults(resp);this.loadRecentQueries();}catch(err){console.error('Smart query error:',err);this.showError(err.message||'Query failed. Please try again.');}finally{this.setLoading(false);}} 16renderResults(resp){const titleEl=document.getElementById('sr-result-title');if(titleEl)titleEl.textContent=resp.title||'Results';const metaEl=document.getElementById('sr-result-meta');if(metaEl){const rows=resp.result?.totalRows??0;const ms=resp.executionTimeMs??0;const qt=resp.questionType||'';metaEl.innerHTML=(resp.questionType==='catalog'?'':'<span>'+rows+' row'+(rows!==1?'s':'')+'</span>')+ 17'<span>'+ms+'ms</span>'+ 18'<span class="sr-question-type">'+this.esc(qt)+'</span>';} 19// Catalog short-circuit: server returns the schema directly when the user 20// asks "what metrics are available". Render as a browsable list of tables 21// + columns; clicking a column drops a starter prompt into the input box. 22if(resp.questionType==='catalog'&&resp.catalog){this.renderCatalog(resp.catalog);const fu=resp.suggestions||[];if(fu.length>0)this.renderFollowUps(fu);return;} 23const chart=resp.chart;if(chart?.isStatCard)this.renderStatCard(chart);else if(chart?.isTableOnly||!chart)this.renderTableOnly(resp);else{this.renderChart(chart);this.showChartToggles(chart.type);} 24this.renderExportButtons();if(resp.result?.rows?.length>0){this.renderDataTable(resp.result);document.getElementById('sr-data-table-card').style.display='block';} 25if(resp.sql){document.getElementById('sr-sql-text').textContent=resp.sql;document.getElementById('sr-sql-card').style.display='block';} 26const followUps=this.buildContextualFollowUps(resp);if(followUps.length>0)this.renderFollowUps(followUps);this.showAnalysisPanel();} 27renderCatalog(catalog){const area=document.getElementById('sr-chart-area');const chartCard=document.getElementById('sr-chart-card');
27if(!area||!chartCard)return;chartCard.style.display='block';const tables=catalog.tables||[];const tableNamesPriority=['AgentHealthMetrics','HardwareHealthMetrics','Events','Agents','LiveSessions','LiveSessionQueries'];tables.sort((a,b)=>{const ai=tableNamesPriority.indexOf(a.name);const bi=tableNamesPriority.indexOf(b.name);if(ai!==-1&&bi!==-1)return ai-bi;if(ai!==-1)return-1;if(bi!==-1)return 1;return(a.displayName||a.name).localeCompare(b.displayName||b.name);});const html=['<div class="sr-catalog">'];for(const t of tables){const cols=(t.columns||[]).slice().sort((a,b)=>(a.displayName||a.name).localeCompare(b.displayName||b.name));html.push('<details class="sr-catalog-table" '+(tableNamesPriority.includes(t.name)?'open':'')+'>');html.push('<summary><strong>'+this.esc(t.displayName||t.name)+'</strong> <span class="sr-catalog-tablekey">'+this.esc(t.name)+'</span> <span class="sr-catalog-count">'+cols.length+' columns</span></summary>');html.push('<div class="sr-catalog-cols">');for(const c of cols){const typeBadge='<span class="sr-catalog-type sr-catalog-type-'+this.esc(c.type)+'">'+this.esc(c.type)+'</span>';const caps=[c.aggregatable?'agg':null,c.groupable?'group':null,c.filterable?'filter':null].filter(Boolean).join(' · ');const seedPrompt=this._buildCatalogSeedPrompt(t.name,c);html.push('<button class="sr-catalog-col" type="button" data-prompt="'+this.esc(seedPrompt)+'" title="Click to load a starter query">'+'<span class="sr-catalog-colname">'+this.esc(c.displayName||c.name)+'</span>'+typeBadge+(caps?'<span class="sr-catalog-caps">'+caps+'</span>':'')+'</button>');}html.push('</div></details>');}html.push('</div>');area.innerHTML=html.join('');area.querySelectorAll('.sr-catalog-col').forEach(btn=>{btn.addEventListener('click',()=>{const inp=document.getElementById('sr-input');if(inp){inp.value=btn.dataset.prompt;inp.focus();}});});} 28_buildCatalogSeedPrompt(table,col){if(table==='AgentHealthMetrics'){if(col.aggregatable)return'Show '+(col.displayName||col.name)+' over the past 24 hours';if(col.groupable)return'Show '+(col.displayName||col.name)+' distribution';return'Show recent values for '+(col.displayName||col.name);}if(table==='HardwareHealthMetrics'){if(col.aggregatable)return'Show '+(col.displayName||col.name)+' across the fleet';return'Show '+(col.displayName||col.name);}if(table==='Events'){if(col.groupable)return'Show event count by '+(col.displayName||col.name)+' over the past 7 days';return'Show '+(col.displayName||col.name);}if(col.groupable)return'List '+(col.displayName||col.name);return'Show '+(col.displayName||col.name);} 29showAnalysisPanel(){const panel=document.getElementById('sr-analysis-section');if(panel)panel.style.display='block';const output=document.getElementById('sr-analysis-output');if(output){output.innerHTML='';output.style.display='none';} 30const progress=document.getElementById('sr-analyze-progress');if(progress)progress.style.display='none';const input=document.getElementById('sr-analyze-input');if(input){input.value='';input.disabled=false;} 31const btn=document.getElementById('sr-analyze-btn');if(btn)btn.disabled=false;const cancel=document.getElementById('sr-analyze-cancel');if(cancel)cancel.style.display='none';} 32submitAnalysis(){const q=document.getElementById('sr-analyze-input')?.value?.trim();if(q&&!this.isAnalyzing)this.runAnalysis(q);} 33cancelAnalysis(){if(this.analysisAbortController){this.analysisAbortController.abort();this.analysisAbortController=null;} 34this.isAnalyzing=false;const input=document.getElementById('sr-analyze-input');if(input)input.disabled=false;const btn=document.getElementById('sr-analyze-btn');if(btn)btn.disabled=false;const cancel=document.getElementById('sr-analyze-cancel');if(cancel)cancel.style.display='none';const progress=document.getElementById('sr-analyze-progress');if(progress)progress.style.display='none';} 35async runAnalysis(question){if(this.isAnalyzing||!this.lastResponse)return;this.isAnalyzing=true;const input=document.getElementById('sr-analyze-input');const btn=document.getElementById('sr-analyze-btn');const cancel=document.getElementById('sr-analyze-cancel');const progress=document.getElementById('sr-analyze-progress');const output=document.getElementById('sr-analysis-output');if(input)input.disabled=true;if(btn)btn.disabled=true;if(cancel)cancel.style.display='inline-flex';if(progress){progress.textContent='Gathering correlated data...';progress.style.display='flex';} 36if(output){output.innerHTML='';output.style.display='none';} 37const context=this.buildAnalysisContext();this.analysisAbortController=new AbortController();const token=await this.getToken();try{const res=await fetch((window.ETDuckyConfig?.API_BASE_URL||'')+'/reports/analyze',{method:'POST',headers:{Authorization:'Bearer '+token,'Content-Type':'application/json'},body:JSON.stringify({followUpQuestion:question,context}),signal:this.analysisAbortController.signal,});if(!res.ok){const err=await res.json().catch(()=>({}));throw new Error(err.error||err.message||'Analysis failed ('+res.status+')');} 38const reader=res.body.getReader();const decoder=new TextDecoder();let buffer='';let narrative='';let currentEvent=null;if(output)output.style.display='block';while(true){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});const lines=buffer.split('\n');buffer=lines.pop()||'';for(const line of lines){if(line.startsWith('event: ')){currentEvent=line.slice(7).trim();}else if(line.startsWith('data: ')&¤tEvent){try{const data=JSON.parse(line.slice(6));this.handleAnalysisEvent(currentEvent,data,output,progress);if(currentEvent==='analysis_token'&&data.token){narrative+=data.token;this.renderMarkdown(output,narrative);}}catch(_){} 39currentEvent=null;}}}}
39catch(err){if(err.name!=='AbortError'){console.error('Analysis error:',err);if(output){output.innerHTML='<div class="sr-analysis-error">'+this.esc(err.message)+'</div>';output.style.display='block';}}}finally{this.isAnalyzing=false;this.analysisAbortController=null;if(input)input.disabled=false;if(btn)btn.disabled=false;if(cancel)cancel.style.display='none';if(progress)progress.style.display='none';}} 40handleAnalysisEvent(eventType,data,output,progress){switch(eventType){case'correlation_started':case'correlation_progress':if(progress){progress.textContent=data.message||'Running queries...';progress.style.display='flex';} 41break;case'correlation_data':if(progress)progress.textContent='Analyzed '+(data.totalDataPoints||0)+' data points. Generating analysis...';break;case'analysis_started':if(progress)progress.textContent='Analyzing...';break;case'analysis_complete':if(progress)progress.style.display='none';break;case'error':if(progress)progress.style.display='none';if(output){output.innerHTML='<div class="sr-analysis-error">'+this.esc(data.message||data.error||'Analysis failed')+'</div>';output.style.display='block';} 42break;}} 43buildAnalysisContext(){const resp=this.lastResponse;if(!resp)return null;const q=resp.query;const ctx={originalQuestion:document.getElementById('sr-input')?.value||'',chartTitle:resp.title||'',dataSource:q?.dataSource||'',groupBy:q?.groupBy||null,primaryMetric:q?.metrics?.[0]?.column||null,primaryServer:null,timeRangeFrom:q?.timeRange?.from||null,timeRangeTo:q?.timeRange?.to||null,resultSummary:[],};if(resp.result?.rows?.length>0){const rows=resp.result.rows;const cols=resp.result.columns||[];if(q?.groupBy==='ServerName'){const numCol=cols.find(c=>c.type==='number'&&c.name!=='count');if(numCol){let maxVal=-Infinity,maxServer=null;for(const row of rows){const val=row[numCol.name];const server=row.ServerName||row['ServerName'];if(typeof val==='number'&&val>maxVal&&server){maxVal=val;maxServer=server;}} 44if(maxServer){ctx.primaryServer=maxServer;ctx.resultSummary.push('Highest '+this.prettyColName(numCol.name)+': '+maxVal.toFixed(2)+' on '+maxServer);}} 45ctx.resultSummary.push(rows.length+' servers in dataset');} 46const serverFilter=q?.filters?.find(f=>f.column==='ServerName');if(serverFilter?.value)ctx.primaryServer=serverFilter.value;ctx.resultSummary.push((resp.result.totalRows||0)+' total data points');} 47return ctx;} 48renderMarkdown(container,text){if(!container||!text)return;let html=this.esc(text);html=html.replace(/^### (.+)$/gm,'<h4 class="sr-md-h4">$1</h4>');html=html.replace(/^## (.+)$/gm,'<h3 class="sr-md-h3">$1</h3>');html=html.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');html=html.replace(/\*(.+?)\*/g,'<em>$1</em>');html=html.replace(/`([^`]+)`/g,'<code class="sr-md-code">$1</code>');html=html.replace(/^- (.+)$/gm,'<li>$1</li>');html=html.replace(/(<li>.*<\/li>\n?)+/g,m=>'<ul class="sr-md-ul">'+m+'</ul>');html=html.replace(/\n\n/g,'</p><p>');html='<p>'+html+'</p>';html=html.replace(/<p>\s*<\/p>/g,'');html=html.replace(/<p>\s*(<h[34])/g,'$1');html=html.replace(/(<\/h[34]>)\s*<\/p>/g,'$1');html=html.replace(/<p>\s*(<ul)/g,'$1');html=html.replace(/(<\/ul>)\s*<\/p>/g,'$1');container.innerHTML=html;container.scrollTop=container.scrollHeight;} 49renderExportButtons(){document.getElementById('sr-export-bar')?.remove();const titleEl=document.getElementById('sr-result-title');if(!titleEl)return;const bar=document.createElement('div');bar.id='sr-export-bar';bar.className='sr-export-bar';bar.innerHTML='<button class="sr-export-btn" data-export="png" title="Export chart as PNG"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg> PNG</button>'+ 50'<button class="sr-export-btn" data-export="csv" title="Export data as CSV"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg> CSV</button>'+ 51'<button class="sr-export-btn" data-export="pdf" title="Export report as PDF"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg> PDF</button>';titleEl.after(bar);bar.querySelector('[data-export="png"]').addEventListener('click',()=>this.exportPNG());bar.querySelector('[data-export="csv"]').addEventListener('click',()=>this.exportCSV());bar.querySelector('[data-export="pdf"]').addEventListener('click',()=>this.exportPDF());} 52exportPNG(){if(!this.chart)return;const a=document.createElement('a');a.download=this.exportFilename('png');a.href=this.chart.toBase64Image();a.click();} 53exportCSV(){const r=this.lastResponse;if(!r?.result?.columns||!r?.result?.rows)return;const cols=r.result.columns,rows=r.result.rows;let csv='\uFEFF';csv+=cols.map(c=>this.csvEsc(this.prettyColName(c.name))).join(',')+'\n';for(const row of rows){csv+=cols.map(c=>{let v=row[c.name];if(v==null)return'';if(c.type==='datetime')return this.csvEsc(new Date(v).toISOString());return this.csvEsc(String(v));}).join(',')+'\n';} 54const blob=new Blob([csv],{type:'text/csv;charset=utf-8;'});const a=document.createElement('a');a.download=this.exportFilename('csv');a.href=URL.createObjectURL(blob);a.click();URL.revokeObjectURL(a.href);} 55exportPDF(){const r=this.lastResponse;if(!r)return;const chartImg=this.chart?this.chart.toBase64Image():null;let tableHtml='';if(r.result?.columns&&r.result?.rows?.length>0){const cols=r.result.columns,rows=r.result.rows.slice(0,200);tableHtml='<table><thead><tr>'+cols.map(c=>'<th>'+this.esc(this.prettyColName(c.name))+'</th>').join('')+'</tr></thead><tbody>'+ 56rows.map(row=>'<tr>'+cols.map(c=>{let v=row[c.name];if(v==null)return'<td>\u2014</td>';
56if(c.type==='datetime')v=new Date(v).toLocaleString();if(c.type==='number'&&typeof v==='number')v=v%1!==0?v.toFixed(2):v;return'<td>'+this.esc(String(v))+'</td>';}).join('')+'</tr>').join('')+'</tbody></table>';} 57const analysisEl=document.getElementById('sr-analysis-output');const analysisHtml=analysisEl?.innerHTML&&analysisEl.style.display!=='none'?'<div class="analysis-section"><h2>Analysis</h2>'+analysisEl.innerHTML+'</div>':'';const w=window.open('','_blank','width=900,height=700');if(!w)return;w.document.write('<!DOCTYPE html><html><head><title>'+this.esc(r.title||'ET Ducky Report')+'</title>'+ 58'<style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;margin:2rem;color:#1a1a2e;background:#fff;}'+ 59'h1{font-size:1.3rem;margin-bottom:0.25rem;}h2{font-size:1.1rem;margin-top:1.5rem;border-bottom:1px solid #e5e7eb;padding-bottom:0.3rem;}'+ 60'.meta{color:#666;font-size:0.82rem;margin-bottom:1.5rem;}'+ 61'.chart-img{max-width:100%;height:auto;border:1px solid #e5e7eb;border-radius:8px;margin-bottom:1.5rem;}'+ 62'table{width:100%;border-collapse:collapse;font-size:0.78rem;margin-top:1rem;}'+ 63'th{background:#f3f4f6;text-align:left;padding:0.5rem 0.6rem;border:1px solid #e5e7eb;font-weight:600;}'+ 64'td{padding:0.4rem 0.6rem;border:1px solid #e5e7eb;}tr:nth-child(even){background:#f9fafb;}'+ 65'.analysis-section{margin-top:1.5rem;padding:1rem;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;}'+ 66'.footer{margin-top:2rem;color:#999;font-size:0.7rem;border-top:1px solid #e5e7eb;padding-top:0.75rem;}'+ 67'@media print{body{margin:0.5cm;}}</style></head><body>'+ 68'<h1>'+this.esc(r.title||'Report')+'</h1>'+ 69'<div class="meta">'+(r.result?.totalRows??0)+' rows \u00B7 '+(r.executionTimeMs??0)+'ms \u00B7 '+this.esc(r.questionType||'')+' \u00B7 Generated '+new Date().toLocaleString()+'</div>'+ 70(chartImg?'<img class="chart-img" src="'+chartImg+'" alt="Chart"/>':'')+ 71tableHtml+analysisHtml+ 72'<div class="footer">ET Ducky Smart Reports \u00B7 '+new Date().toLocaleDateString()+'</div>'+ 73'</body></html>');w.document.close();var __p=function(){try{w.focus();w.print();}catch(e){}};w.document.readyState==="complete"?__p():w.addEventListener("load",__p);} 74exportFilename(ext){return'etducky-'+(this.lastResponse?.title||'report').replace(/[^a-zA-Z0-9]+/g,'-').toLowerCase()+'-'+new Date().toISOString().slice(0,10)+'.'+ext;} 75csvEsc(val){if(val==null)return'';const s=String(val);return s.includes(',')||s.includes('"')||s.includes('\n')?'"'+s.replace(/"/g,'""')+'"':s;} 76buildContextualFollowUps(resp){const sug=[...(resp.suggestions||[])];if(!resp.result?.rows?.length||!resp.result?.columns)return sug;const rows=resp.result.rows,cols=resp.result.columns;const qtype=resp.questionType||'',ds=resp.query?.dataSource||'';if((resp.query?.groupBy||null)==='ServerName'&&rows.length>0){const numCol=cols.find(c=>c.type==='number'&&c.name!=='count');if(numCol){let maxVal=-Infinity,maxServer=null;for(const row of rows){const val=row[numCol.name],server=row.ServerName||row['ServerName'];if(typeof val==='number'&&val>maxVal&&server){maxVal=val;maxServer=server;}} 77if(maxServer){const metric=this.prettyColName(numCol.name).replace(/\s*\(.*?\)\s*/g,'').trim();sug.unshift('What was happening on '+maxServer+' when '+metric+' spiked?');sug.unshift('Show all metrics for '+maxServer+' over the same period');}}} 78if(qtype==='trend'&&cols.some(c=>c.name==='bucket'||c.name==='time_bucket')){if(ds==='AgentHealthMetrics'){sug.push('Show event activity during the same period');sug.push('Which server had the highest CPU spike?');} 79else if(ds==='Events'){sug.push('Show CPU and memory during the same period');sug.push('Which providers generated the most errors?');}} 80if(ds==='Agents'&&qtype==='listing'){sug.push('Compare CPU usage across all servers');sug.push('Are any servers showing high memory usage?');} 81if(qtype==='single_stat')sug.push('Show this metric over time for the past week');const seen=new Set();return sug.filter(s=>{const l=s.toLowerCase();if(seen.has(l))return false;seen.add(l);return true;}).slice(0,6);} 82renderStatCard(chart){const area=document.getElementById('sr-chart-area');if(area){area.innerHTML='<div class="sr-stat-card"><div class="sr-stat-value">
82'+this.esc(chart.statValue||'\u2014')+'</div><div class="sr-stat-label">'+this.esc(chart.statLabel||chart.title)+'</div></div>';document.getElementById('sr-chart-card').style.display='block';}} 83renderTableOnly(){const card=document.getElementById('sr-data-table-card');if(card)card.style.display='block';const wrap=document.getElementById('sr-table-wrap');if(wrap)wrap.style.display='block';} 84renderChart(cfg){if(this.chart){this.chart.destroy();this.chart=null;} 85const area=document.getElementById('sr-chart-area');if(!area)return;area.innerHTML='<canvas id="sr-chart-canvas"></canvas>';const canvas=document.getElementById('sr-chart-canvas');if(!canvas)return;let indexAxis,type=cfg.type;if(type==='horizontalBar'){type='bar';indexAxis='y';} 86const datasets=(cfg.datasets||[]).map((ds,i)=>{const c=COLORS[i%COLORS.length];return{label:ds.label||'Series '+(i+1),data:ds.data||[],borderColor:ds.borderColor||c,backgroundColor:type==='doughnut'?(ds.backgroundColors||ds.data.map((_,j)=>COLORS[j%COLORS.length]+'CC')):(ds.backgroundColor||(type==='bar'?c+'99':c+'22')),fill:ds.fill??(type==='line'),tension:ds.tension??0.3,borderWidth:type==='doughnut'?0:2,pointRadius:(cfg.labels||[]).length>80?0:3,};});const opts=this.buildChartOptions(type,indexAxis);try{this.chart=new Chart(canvas.getContext('2d'),{type,data:{labels:cfg.labels||[],datasets},options:opts});}catch(err){console.error('Chart render error:',err);area.innerHTML='<div class="sr-chart-error">Failed to render chart</div>';} 87document.getElementById('sr-chart-card').style.display='block';} 88buildChartOptions(type,indexAxis){const o={responsive:true,maintainAspectRatio:false,indexAxis,interaction:{mode:'index',intersect:false},plugins:{legend:{labels:{color:'#9CA3AF',font:{size:11}}},tooltip:{backgroundColor:'#151A2B',borderColor:'#2A3348',borderWidth:1,titleColor:'#E8EBF0',bodyColor:'#9CA3AF'},},};if(type==='doughnut'){o.plugins.legend.position='right';} 89else if(type==='radar'){o.scales={r:{grid:{color:'#2A3348'},ticks:{color:'#6B7280',backdropColor:'transparent'},pointLabels:{color:'#9CA3AF'}}};} 90else{o.scales={x:{grid:{color:'rgba(255,255,255,0.04)'},ticks:{color:'#6B7280',maxRotation:45,font:{size:10}}},y:{grid:{color:'rgba(255,255,255,0.04)'},ticks:{color:'#6B7280',font:{size:10}},beginAtZero:true}};} 91return o;} 92switchChartType(type){if(!this.lastResponse?.chart)return;document.querySelectorAll('.sr-chart-toggle-btn').forEach(b=>b.classList.toggle('active',b.dataset.chartType===type));this.renderChart({...this.lastResponse.chart,type});} 93showChartToggles(type){const el=document.getElementById('sr-chart-toggles');if(el){el.style.display='flex';document.querySelectorAll('.sr-chart-toggle-btn').forEach(b=>b.classList.toggle('active',b.dataset.chartType===type));}} 94renderDataTable(result){const head=document.getElementById('sr-table-head'),body=document.getElementById('sr-table-body');if(!head||!body)return;head.innerHTML='<tr>'+result.columns.map(c=>'<th>'+this.esc(this.prettyColName(c.name))+'</th>').join('')+'</tr>';const max=Math.min(result.rows.length,200);body.innerHTML=result.rows.slice(0,max).map(row=>'<tr>'+result.columns.map(c=>{let v=row[c.name];if(v==null)return'<td class="sr-dim">\u2014</td>';if(c.type==='datetime')v=this.fmtTime(v);else if(c.type==='number'&&typeof v==='number')v=v%1!==0?v.toFixed(2):v;return'<td>'+this.esc(String(v))+'</td>';}).join('')+'</tr>').join('');if(result.rows.length>max)body.innerHTML+='<tr><td colspan="'+result.columns.length+'" style="text-align:center;color:var(--text-muted);padding:1rem;">Showing '+max+' of '+result.rows.length+' rows</td></tr>';} 95toggleTable(){const w=document.getElementById('sr-table-wrap'),b=document.getElementById('sr-toggle-table');if(!w)return;const vis=w.style.display!=='none';w.style.display=vis?'none':'block';if(b)b.textContent=vis?'Show Data':'Hide Data';} 96toggleSql(){const w=document.getElementById('sr-sql-body'),b=document.getElementById('sr-toggle-sql');if(!w)return;const vis=w.style.display!=='none';w.style.display=vis?'none':'block';if(b)b.textContent=vis?'Show SQL':'Hide SQL';} 97async loadSuggestions(){try{this.suggestions=await this.apiFetch('/reports/suggestions');this.renderSuggestions();} 98catch(e){console.error('Failed to load suggestions:',e);}} 99renderSuggestions(){const el=document.getElementById('sr-suggestions');if(!el||!this.suggestions?.length)return;el.innerHTML=this.suggestions.map(s=>'<span class="sr-suggestion-chip" data-question="'+this.escAttr(s)+'">'+this.esc(s)+'</span>').join('');el.querySelectorAll('.sr-suggestion-chip').forEach(c=>c.addEventListener('click',()=>{const q=c.dataset.question;if(q)this.askQuestion(q);}));} 100renderFollowUps(items){const el=document.getElementById('sr-followups');if(!el)return;el.innerHTML='<div class="sr-followups-label">Related questions</div><div class="sr-followups-chips">'+ 101items.map(s=>'<span class="sr-suggestion-chip" data-question="'+this.escAttr(s)+'">'+this.esc(s)+'</span>').join('')+'</div>';el.style.display='block';el.querySelectorAll('.sr-suggestion-chip').forEach(c=>c.addEventListener('click',()=>{const q=c.dataset.question;if(q)this.askQuestion(q);}));} 102async loadRecentQueries(){try{this.recentQueries=await this.apiFetch('/reports/recent-queries?limit=10');this.renderRecentQueries();this._recentRetryCount=0;} 103catch(e){console.error('Failed to load recent queries:',e);this._recentRetryCount=(this._recentRetryCount||0)+1,this._recentRetryCount<=3&&setTimeout(()=>this.loadRecentQueries(),600);}} 104renderRecentQueries(){const select=document.getElementById('sr-recent-select');if(!select)return;select.innerHTML='<option value="">Recent Queries</option>';if(!this.recentQueries?.length){select.innerHTML+='<option value="" disabled>No recent queries</option>';return;} 105select.innerHTML+=this.recentQueries.map(q=>{const date=this.fmtDate(q.createdAt),ms=Number.isFinite(q.executionTimeMs)?q.executionTimeMs+'ms':'',meta=[date,ms].filter(
105Boolean).join(' ⢠'),label=meta?q.question+' ('+meta+')':q.question;return'<option value="'+this.escAttr(q.question)+'">'+this.esc(label)+'</option>';}).join('');} 106setLoading(on){this.isLoading=on;const inp=document.getElementById('sr-input'),btn=document.getElementById('sr-send-btn'),ld=document.getElementById('sr-loading');if(inp)inp.disabled=on;if(btn)btn.disabled=on;if(ld)ld.style.display=on?'flex':'none';} 107showResultsArea(){const el=document.getElementById('sr-results');if(el)el.style.display='block';} 108clearResults(){if(this.chart){this.chart.destroy();this.chart=null;} 109['sr-chart-card','sr-data-table-card','sr-sql-card','sr-followups','sr-chart-toggles','sr-error','sr-analysis-section'].forEach(id=>{const el=document.getElementById(id);if(el)el.style.display='none';});const area=document.getElementById('sr-chart-area');if(area)area.innerHTML='';const tw=document.getElementById('sr-table-wrap');if(tw)tw.style.display='none';const sb=document.getElementById('sr-sql-body');if(sb)sb.style.display='none';document.getElementById('sr-export-bar')?.remove();} 110showError(msg){const el=document.getElementById('sr-error');if(el){el.textContent=msg;el.style.display='block';}} 111esc(s){const d=document.createElement('div');d.textContent=s??'';return d.innerHTML;} 112escAttr(s){return(s??'').replace(/"/g,'"').replace(/'/g,''').replace(/</g,'<').replace(/>/g,'>');} 113fmtTime(v){if(!v)return'';const d=new Date(v);return isNaN(d)?String(v):d.toLocaleString(void 0,{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});} 114fmtDate(v){return v?new Date(v).toLocaleDateString(void 0,{month:'short',day:'numeric',year:'numeric'}):'';} 115prettyColName(n){return n?n.replace(/_/g,' ').replace(/([a-z])([A-Z])/g,'$1 $2').replace(/\b(avg|min|max|sum|count)\b/gi,m=>'('+m.charAt(0).toUpperCase()+m.slice(1)+')').replace(/\bCpu\b/g,'CPU').replace(/\bMb\b/g,'MB').replace(/\bMs\b/g,'ms').replace(/\bEtw\b/g,'ETW').replace(/\bIo\b/g,'I/O'):'';}};})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.