feat(audit-dashboard-v3): D3.js timeline section (doctrine 157)
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
This commit is contained in:
@@ -66,6 +66,12 @@ td code{background:#0a0f1a;padding:2px 6px;border-radius:3px;color:#7c5cff;font-
|
||||
<div class="kpi"><span class="kpi-value" id="kpi-scripts">-</span><span class="kpi-label">Scripts shell</span></div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>0. Timeline des patches <span id="timeline-count" class="count-info"></span></h2>
|
||||
<div id="timeline-viz" style="background:#1a2333;border:1px solid #1f2937;border-radius:8px;padding:16px;overflow-x:auto;position:relative"></div>
|
||||
<div style="font-size:11px;color:#64748b;margin-top:6px">1 point = 1 patch. Hover pour details. Couleur = doctrine.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>1. Pages patchees <span id="audit-count" class="count-info"></span></h2>
|
||||
<div class="filters">
|
||||
@@ -108,6 +114,7 @@ td code{background:#0a0f1a;padding:2px 6px;border-radius:3px;color:#7c5cff;font-
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script>
|
||||
let DATA = null;
|
||||
|
||||
@@ -139,6 +146,7 @@ async function refresh(){
|
||||
const src = document.getElementById('filter-source');
|
||||
src.innerHTML = '<option value="">Toutes</option>' + Array.from(sources).sort().map(s => '<option value="' + s + '">' + s + '</option>').join('');
|
||||
|
||||
renderTimeline();
|
||||
renderAudit();
|
||||
renderIntents();
|
||||
renderPresets();
|
||||
@@ -150,6 +158,37 @@ async function refresh(){
|
||||
}
|
||||
}
|
||||
|
||||
function renderTimeline(){
|
||||
const container = document.getElementById("timeline-viz");
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
if (typeof d3 === "undefined") { container.innerHTML = "<div class="empty">D3 loading...</div>"; return; }
|
||||
const events = [];
|
||||
DATA.audit.forEach(a => { (a.markers||[]).forEach(m => {
|
||||
const iso = a.last_commit_iso || a.last_commit_date;
|
||||
if (!iso) return;
|
||||
const dt = new Date(iso.replace(" ","T"));
|
||||
if (isNaN(dt.getTime())) return;
|
||||
events.push({time:dt,file:a.file,marker:m.marker,commit:a.last_commit||"",doctrine:a.doctrine||"?",msg:a.last_commit_msg||""});
|
||||
}); });
|
||||
events.sort((a,b) => a.time - b.time);
|
||||
const cnt = document.getElementById("timeline-count");
|
||||
if (cnt) cnt.textContent = "(" + events.length + " events)";
|
||||
if (events.length === 0) { container.innerHTML = "<div class="empty">Pas de donnees timeline</div>"; return; }
|
||||
const W = container.offsetWidth || 1200, H = 160, M = {top:20,right:30,bottom:40,left:50};
|
||||
const svg = d3.select(container).append("svg").attr("width",W).attr("height",H);
|
||||
const domain = d3.extent(events, e => e.time);
|
||||
if (domain[0].getTime() === domain[1].getTime()) { const pad = 30*60*1000; domain[0] = new Date(domain[0].getTime()-pad); domain[1] = new Date(domain[1].getTime()+pad); }
|
||||
const x = d3.scaleTime().domain(domain).range([M.left, W-M.right]);
|
||||
const DC = {"146":"#10b981","147":"#3b82f6","148":"#8b5cf6","149":"#f59e0b","150":"#ec4899","151":"#06b6d4","152":"#ef4444","153":"#14b8a6","154":"#a855f7","155":"#eab308","156":"#6366f1","157":"#f472b6","158":"#22d3ee","?":"#64748b"};
|
||||
svg.append("g").attr("transform","translate(0,"+(H-M.bottom)+")").call(d3.axisBottom(x).ticks(6).tickFormat(d3.timeFormat("%H:%M"))).attr("color","#94a3b8");
|
||||
const tooltip = d3.select(container).append("div").style("position","absolute").style("pointer-events","none").style("background","#0a0f1a").style("border","1px solid #2a3344").style("padding","8px 12px").style("border-radius","6px").style("font-size","11px").style("color","#e2e8f0").style("z-index","100").style("opacity","0").style("max-width","320px");
|
||||
svg.selectAll("circle").data(events).enter().append("circle").attr("cx", d => x(d.time)).attr("cy", () => M.top+20+Math.random()*(H-M.top-M.bottom-40)).attr("r",7).attr("fill", d => DC[d.doctrine]||"#64748b").attr("stroke","#fff").attr("stroke-width",1.5).style("cursor","pointer").on("mouseover", function(e,d){ d3.select(this).attr("r",10); tooltip.style("opacity",1).html("<b>"+d.marker+"</b><br>"+d.file+"<br>D"+d.doctrine+" | <code>"+d.commit+"</code><br><span style="color:#94a3b8">"+(d.msg.substring(0,100)).replace(/</g,"<")+"</span>").style("left",(e.offsetX+15)+"px").style("top",(e.offsetY-10)+"px"); }).on("mouseout", function(){ d3.select(this).attr("r",7); tooltip.style("opacity",0); });
|
||||
const doctrines = Array.from(new Set(events.map(e => e.doctrine))).sort();
|
||||
const leg = svg.append("g").attr("transform","translate("+M.left+","+(H-15)+")");
|
||||
doctrines.forEach((d,i) => { const g = leg.append("g").attr("transform","translate("+(i*60)+",0)"); g.append("circle").attr("r",5).attr("fill",DC[d]||"#64748b"); g.append("text").attr("x",10).attr("y",4).attr("fill","#94a3b8").style("font-size","10px").text("D"+d); });
|
||||
}
|
||||
|
||||
function renderAudit(){
|
||||
const ag = document.getElementById('audit-grid');
|
||||
const search = (document.getElementById('search-audit').value || '').toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user