generated from mathias/template-go-web
feat(atlas): weight replay pacing on CI/CD stages by real job durations (#5)
TDD: Job.Seconds() + StageSeconds() derive real CI/CD dwell time from the latest run's per-job created_at/updated_at (last job in pipeline order = deploy/stage 07, everything before it = CI/stage 06). Frontend: weightedSpineDist() redistributes the pixel-time-budget the 06/07 segments already had, splitting it by real CI:CD duration ratio instead of raw pixel width. Falls back to the exact prior constant-speed sweep when no run data is available (weights default to segment pixel length) or when a job is skipped (0 duration) — no behavior change for stages 00-05/08, which still have no live timing source (same gap as #5's ledger item). Verified: real duration values flow through /api/atlas.json (ci_duration_s: 18 observed against a real run), full page screenshot confirms no visual regression.
This commit is contained in:
@@ -55,6 +55,7 @@ func NewHandler() http.Handler {
|
||||
a.Stages[i].Nodes = nodes
|
||||
}
|
||||
}
|
||||
a.CIDurationS, a.CDDurationS = atlas.StageSeconds(*ld.run)
|
||||
}
|
||||
if ld.issues != nil {
|
||||
for i := range a.Stages {
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
// stage from the live cd.yml + substrate from the live cluster). These start
|
||||
// empty and are filled by init()'s fetch; on failure the page shows an error
|
||||
// banner rather than stale inline data.
|
||||
let SUBSTRATE=[], NS="", STAGES=[], TIMELINE=[];
|
||||
let SUBSTRATE=[], NS="", STAGES=[], TIMELINE=[], CI_DUR=0, CD_DUR=0;
|
||||
let MODE = localStorage.getItem('atlas-mode') || 'plain'; // 'plain' | 'technical'
|
||||
|
||||
const track=document.getElementById('track');
|
||||
@@ -330,14 +330,47 @@ function build(){
|
||||
}
|
||||
spineLen=spinePath.getTotalLength();loopLen=loopPath.getTotalLength();
|
||||
}
|
||||
// weightedSpineDist maps f (0..1 time-progress across the spine) to an arc-length
|
||||
// distance. Default weight per stage-to-stage segment is its own pixel length —
|
||||
// reduces to the old constant-pixel-speed sweep. When a real run's CI/CD durations
|
||||
// are known, the pixel-time-budget already held by the 06(CI)/07(CD) segments is
|
||||
// re-split by their real relative duration instead of by raw pixel width — so the
|
||||
// reel visits stage 06 vs 07 at speeds proportional to how long they actually took.
|
||||
function weightedSpineDist(f){
|
||||
const n=cs.length-1;
|
||||
if(n<=0)return 0;
|
||||
const segLens=[];for(let i=0;i<n;i++)segLens.push(cs[i+1]-cs[i]);
|
||||
const weights=segLens.slice();
|
||||
if(CI_DUR>0&&CD_DUR>0&&n>=7){
|
||||
const budget=weights[5]+weights[6], tot=CI_DUR+CD_DUR;
|
||||
weights[5]=budget*CI_DUR/tot; weights[6]=budget*CD_DUR/tot;
|
||||
}
|
||||
const totalW=weights.reduce((a,b)=>a+b,0);
|
||||
const target=f*totalW;
|
||||
let acc=0;
|
||||
for(let i=0;i<n;i++){
|
||||
if(target<=acc+weights[i]||i===n-1){
|
||||
const local=weights[i]>0?(target-acc)/weights[i]:0;
|
||||
const segStart=cs[i]-cs[0];
|
||||
return Math.min(spineLen,Math.max(0,segStart+segLens[i]*Math.min(1,Math.max(0,local))));
|
||||
}
|
||||
acc+=weights[i];
|
||||
}
|
||||
return spineLen;
|
||||
}
|
||||
function run(ts){
|
||||
if(mobile)return;
|
||||
if(!t0)t0=ts;
|
||||
const dur=slow?16000:7000;
|
||||
const p=Math.min((ts-t0)/dur,1);
|
||||
const total=spineLen+loopLen, dist=total*p;
|
||||
let pt,onLoop=dist>spineLen;
|
||||
pt=onLoop?loopPath.getPointAtLength(dist-spineLen):spinePath.getPointAtLength(dist);
|
||||
const spineFrac=spineLen/(spineLen+loopLen);
|
||||
let dist,onLoop;
|
||||
if(p<spineFrac){
|
||||
dist=weightedSpineDist(p/spineFrac); onLoop=false;
|
||||
}else{
|
||||
dist=spineLen+loopLen*((p-spineFrac)/(1-spineFrac)); onLoop=true;
|
||||
}
|
||||
let pt=onLoop?loopPath.getPointAtLength(dist-spineLen):spinePath.getPointAtLength(dist);
|
||||
pulse.style.left=pt.x+'px';pulse.style.top=pt.y+'px';
|
||||
pulse.style.background=onLoop?'var(--violet)':'var(--amber)';
|
||||
pulse.style.boxShadow=onLoop?'0 0 15px 4px rgba(155,140,255,.75)':'0 0 15px 4px rgba(245,185,66,.75)';
|
||||
@@ -370,6 +403,8 @@ async function init(){
|
||||
if(typeof data.ns==='string') NS=data.ns;
|
||||
if(Array.isArray(data.stages)) STAGES=data.stages;
|
||||
if(Array.isArray(data.timeline)) TIMELINE=data.timeline;
|
||||
if(typeof data.ci_duration_s==='number') CI_DUR=data.ci_duration_s;
|
||||
if(typeof data.cd_duration_s==='number') CD_DUR=data.cd_duration_s;
|
||||
if(data.version) document.getElementById('ver').textContent=data.version;
|
||||
}catch(e){
|
||||
console.error('atlas: failed to load /api/atlas.json —',e);
|
||||
|
||||
Reference in New Issue
Block a user