Files
meguri/static/generation-map.js
T
Ultradesu 5260c2828b
Checks / test (push) Failing after 2m25s
NEXT
2026-10-07 15:30:31 +01:00

148 lines
26 KiB
JavaScript

// Canvas graph is kept outside Alpine's reactive proxy. It never mutates scheduling state.
function generationMapUi(){let graph=null,origin=null,request=0;const colors=new Map();return {
mapProbe:null,mapProbeHistory:[],mapProbeStyle:{},
mapBindingKey(b){if(!b)return '';return JSON.stringify([b.kind,b.variable||null,b.iteration||null,b.output||null,b.value??null,b.path||[],b.prefix||'',b.suffix||'',(b.items||[]).map(v=>this.mapBindingKey(v)),b.key?this.mapBindingKey(b.key):null]);},
mapColor(key){if(!colors.has(key))colors.set(key,`hsl(${(colors.size*137.508)%360} 58% 32%)`);return colors.get(key);},
mapAddressParts(address){const text=address.replace(/^\{\s*|\s*\}$/g,''),parts=[];let start=0,quote='',escape=false,depth=0;for(let i=0;i<text.length;i++){const c=text[i];if(quote){if(escape)escape=false;else if(c==='\\')escape=true;else if(c===quote)quote='';continue;}if(c==='"'||c==="'"){quote=c;continue;}if(c==='[')depth++;if(c===']')depth--;if(c==='.'&&depth===0){parts.push({text:text.slice(start,i),dot:false});parts.push({text:'.',dot:true});start=i+1;}}parts.push({text:text.slice(start),dot:false});return parts.map((p,i)=>({...p,color:this.mapColor(parts.slice(0,i+1).map(p=>p.text).join(''))}));},
mapAllBindings(rule){const list=[];const walk=v=>{if(!v||typeof v!=='object')return;if(['literal','variable','item','key','lookup','output','list'].includes(v.kind)){list.push(v);if(v.key)walk(v.key);(v.items||[]).forEach(walk);return;}Object.values(v).forEach(walk);};walk(rule);return list;},
mapAddressBinding(address){const rule=this.mapSnapshot.payload.rules.find(r=>r.id===this.mapCurrent()?.rule);return {rule:rule?.id,binding:this.mapAllBindings(rule).find(b=>'{ '+this.bindingAddress(b,0,rule.iterations)+' }'===address)};},
mapTraceBinding(rule,binding,row){const key=this.mapBindingKey(binding),rows=this.mapTrace(rule)?.rows||[],found=[];for(const [index,pass] of rows.entries()){if(row!=null&&index!==row)continue;const scan=p=>{if(!p)return null;if(this.mapBindingKey(p.binding)===key)return p;for(const parent of p.parents||[]){const match=scan(parent);if(match)return match;}return null;};const trace=(pass.bindings||[]).map(scan).find(Boolean);if(trace)found.push({row:index,trace});}return found;},
mapResourceOrigin(item,key){if(item.manual)return {manual:true,title:item.name,field:key,value:item.resource[key]};const rule=this.mapSnapshot.payload.rules.find(r=>r.id===item.rule),block=rule?.blocks.find(b=>b.id===item.block),row=this.mapTrace(item.rule)?.rows.findIndex(r=>r.resources.includes(item.id));let binding=block?.fields[key];const step=/^steps\.(\d+)\.(delay|recipient)$/.exec(key);if(step){const s=block.steps[Number(step[1])];binding=step[2]==='recipient'&&(s.team_all||s.team_random)?this.literal(tr(s.team_random?'a_random_team_member_8f35f3':'everyone_on_the_team_235b55')):s[step[2]];}const condition=/^conditions\.(\d+)$/.exec(key);if(condition)binding=block.conditions[Number(condition[1])]?.value;return {rule:item.rule,row:row>=0?row:null,binding,title:item.name,field:key,value:item.resource[key]};},
mapNameParts(item){const origin=this.mapResourceOrigin(item,'name'),b=origin.binding;if(!b||b.kind==='literal')return [{text:item.name,origin,color:this.mapColor(this.mapBindingKey(b)||item.id)}];const prefix=b.prefix||'',suffix=b.suffix||'';return [{text:prefix,origin:{...origin,binding:this.literal(prefix)},color:this.mapColor('prefix')},{text:item.name.slice(prefix.length,suffix?item.name.length-suffix.length:undefined),origin,color:this.mapColor(this.mapBindingKey(b))},{text:suffix,origin:{...origin,binding:this.literal(suffix)},color:this.mapColor('suffix')}].filter(p=>p.text);},
mapSampleElement(value,path){if(!path)return value;for(const key of path.split(' / ')){value=Array.isArray(value)?value[Number(key)-1]:value?.[key];}return value;},
async mapExploreAddressPart(address,index,event){await this.mapExploreAddress(address,event);if(!this.mapProbe)return;const parts=this.mapAddressParts(address).filter(p=>!p.dot),selected=this.mapAddressParts(address).slice(0,index+1).filter(p=>!p.dot).length,stages=this.mapProbeStages();this.mapProbe.stage=selected===parts.length?Math.max(0,stages.length-1):Math.max(0,selected-(parts.length-stages.length)-1);},
async mapExploreAddress(address,event){const source=this.mapAddressBinding(address);if(!source.binding)return;const row=this.mapRow==='all'?null:Number(this.mapRow);await this.mapExplore({...source,row,title:address},event);},
async mapExplore(origin,event){const traces=origin.binding?this.mapTraceBinding(origin.rule,origin.binding,origin.row):[];const probe={...origin,traces,stage:null};if(this.mapProbe)this.mapProbeHistory.push(this.mapProbe);else this.mapProbeHistory=[];this.mapProbe=probe;const rect=event?.currentTarget?.getBoundingClientRect()||{left:innerWidth/2,top:80};this.mapProbeStyle={left:Math.max(12,Math.min(rect.left,innerWidth-402))+'px',top:Math.max(12,Math.min(rect.top,innerHeight-570))+'px'};await this.$nextTick();const popup=document.querySelector('.generation-map .map-probe');if(this.mapOpen && this.mapProbe && popup && !popup.matches(':popover-open'))popup.showPopover();},
mapProbeClose(){const el=document.querySelector('.generation-map .map-probe');if(el?.matches(':popover-open'))el.hidePopover();this.mapProbe=null;this.mapProbeHistory=[];},
mapProbeBack(){if(this.mapProbeHistory.length)this.mapProbe=this.mapProbeHistory.pop();},
mapProbeAddress(){const p=this.mapProbe,rule=this.mapSnapshot?.payload.rules.find(r=>r.id===p?.rule);return p?.binding?'{ '+this.bindingAddress(p.binding,0,rule?.iterations||[])+' }':p?.field||'';},
mapProbeStages(){return this.mapProbe?.traces[0]?.trace.stages||[];},
mapProbeValues(){const p=this.mapProbe;if(!p)return [];if(!p.traces.length){return [{label:tr(p.manual?'map_manual':p.binding?.kind==='literal'?'fixed_value_191429':'map_no_trace'),value:p.binding?.kind==='literal'?p.binding.value:p.value}];}return p.traces.slice(0,20).map(({row,trace})=>{const result=p.stage==null?trace.result:trace.stages[p.stage]?.result;return {label:tr('map_pass_number',{number:row+1}),value:this.mapSampleElement(result?.value,p.stage==null?p.elementPath:null),error:result?.error};});},
mapProbeOutputSources(){const b=this.mapProbe?.binding;if(b?.kind!=='output')return [];const rule=this.mapSnapshot.payload.rules.find(r=>r.blocks.some(block=>block.id===b.output)),block=rule?.blocks.find(block=>block.id===b.output);return block?Object.entries(block.fields).map(([field,binding])=>({title:this.fieldsFor(block.kind).find(f=>f.key===field)?.label||field,binding,rule:rule.id,row:rule.id===this.mapProbe.rule?this.mapProbe.row:null})):[];},
mapProbeParents(){return this.mapProbe?.traces[0]?.trace.parents||[];},
mapProbeFollow(trace,event){this.mapExplore({rule:this.mapProbe.rule,row:this.mapProbe.row,binding:trace.binding,title:tr('map_source_chain')},event);},
mapProbeTarget(){const b=this.mapProbe?.binding;if(!b)return '';if(b.variable)return 'v:'+b.variable;if(b.iteration)return 'i:'+b.iteration;if(b.output)return 'b:'+b.output;if(b.kind==='literal'&&this.mapNodes.some(n=>n.id==='m:'+b.value))return 'm:'+b.value;return '';},
mapExploreToken(item,field,token,event){const origin=this.mapResourceOrigin(item,field.key);if(!origin.binding)origin.value=field.value;const index=Number(token.path?.split(' / ')[0])-1;if(origin.binding?.kind==='list' && origin.binding.items[index])origin.binding=origin.binding.items[index];else origin.elementPath=token.path;this.mapExplore(origin,event);},
mapOpen:false,mapBusy:false,mapError:'',mapSnapshot:null,mapNodes:[],mapSelected:'',mapExpanded:{},mapRow:'all',
async openGenerationMap(){origin=document.activeElement;this.mapOpen=true;this.mapBusy=true;this.mapSnapshot=null;this.mapNodes=[];this.mapSelected='';this.mapExpanded={};this.mapRow='all';await this.$nextTick();this.$refs.generationMap.showModal();await this.refreshGenerationMap();},
closeGenerationMap(){this.mapProbeClose();request++;graph?.destroy();graph=null;this.$refs.generationMap.close();this.mapOpen=false;origin?.focus();},
async refreshGenerationMap(){const sequence=++request;this.mapBusy=true;this.mapError='';try{const payload=this.generationPayload(),workspace=this.workspace.id,document=copy(this.doc);const report=await this.api(this.endpoint('/generation-preview'),'POST',payload);if(sequence!==request || !this.mapOpen || workspace!==this.workspace.id)return;this.mapSnapshot={payload,report,document};await this.$nextTick();this.drawGenerationMap();}catch(e){if(sequence===request)this.mapError=e.message;}finally{if(sequence===request)this.mapBusy=false;}},
mapSelect(id){this.mapSelected=id;this.mapRow='all';if(graph){graph.elements().removeClass('selected related');const n=graph.getElementById(id);n.addClass('selected');n.closedNeighborhood().addClass('related');}},
mapCurrent(){return this.mapNodes.find(n=>n.id===this.mapSelected);},
mapTrace(rule){return this.mapSnapshot?.report.trace?.find(t=>t.rule===rule);},
mapResources(){const n=this.mapCurrent(),report=this.mapSnapshot?.report;if(this.mapBusy || this.mapError || !n || n.variable)return [];if(n.manual)return [{id:n.resourceId,kind:n.manualKind,resource:n.source,name:n.source.name,manual:true}];if(!report?.result)return [];const row=this.mapRow==='all'?null:this.mapTrace(n.rule)?.rows[Number(this.mapRow)];return report.result.managed.filter(m=>(!n.rule || m.rule===n.rule)&&(!n.block || m.block===n.block)&&(!n.resourceId || m.id===n.resourceId)&&(!row || row.resources.includes(m.id))).map(m=>{const kind=['rotation','priority'].includes(m.kind)?'schedules':m.kind==='escalation'?'escalations':'routes';const resource=report.result[kind].find(r=>r.id===m.id);return {...m,resource,name:resource?.name||m.id};});},
mapIssues(){const n=this.mapCurrent();return (this.mapSnapshot?.report.issues||[]).filter(i=>!n || (n.variable?i.variable_id===n.variable:!i.rule_id || i.rule_id===n.rule)&&(!n.block || !i.block_id || i.block_id===n.block));},
mapRows(){return this.mapTrace(this.mapCurrent()?.rule)?.rows||[];},
mapRowLabel(row,index){const rule=this.mapSnapshot.payload.rules.find(r=>r.id===this.mapCurrent()?.rule);const names=(rule?.iterations||[]).map((iteration,i)=>{const v=row.inputs?.[iteration.id];return typeof v==='object'&&v!==null?(v.name||v.title||row.labels?.[i]||iteration.name):String(v??iteration.name);});return (index+1)+'. '+(names.join(' / ')||tr('map_once'))+' · '+tr('map_'+row.status,{count:1});},
mapInputSources(){
const n=this.mapCurrent(),snapshot=this.mapSnapshot;if(!n||!snapshot||this.mapBusy)return [];
const {payload,report}=snapshot,rule=payload.rules.find(r=>r.id===n.rule),sources=new Map();
const variable=(id,address)=>{const v=payload.variables.find(v=>v.id===id);if(!v)return;const key='v:'+id;if(!sources.has(key))sources.set(key,{id:key,name:v.name,type:this.label(v.node.kind),count:v.node.entries?.length||0,addresses:[],target:key});const item=sources.get(key);if(address&&!item.addresses.includes(address))item.addresses.push(address);};
const scan=(value,owner=rule,depth=0)=>{if(!value||typeof value!=='object'||depth>12)return;
if(value.kind==='variable'||value.kind==='lookup')variable(value.variable,'{ '+this.bindingAddress(value,0,owner?.iterations||[])+' }');
if(value.kind==='item'||value.kind==='key'){const iteration=owner?.iterations.find(i=>i.id===value.iteration);if(iteration){const before=new Set(sources.keys());scan(iteration.source,owner,depth+1);for(const item of sources.values())if(item.id.startsWith('v:')&&(!before.has(item.id)||iteration.source.variable===item.id.slice(2))){const address='{ '+this.bindingAddress(value,0,owner.iterations)+' }';if(!item.addresses.includes(address))item.addresses.push(address);}}}
if(value.kind==='output'){
const producer=payload.rules.find(r=>r.blocks.some(b=>b.id===value.output)),block=producer?.blocks.find(b=>b.id===value.output);
if(block && (n.block || producer.id!==n.rule)){const managed=report.result?.managed.filter(m=>m.block===block.id)||[],resources=[...(report.result?.schedules||[]),...(report.result?.escalations||[]),...(report.result?.routes||[])],resource=managed.length===1?resources.find(r=>r.id===managed[0].id):null,key='o:'+block.id;const target=this.mapNodes.find(v=>v.block===block.id)||this.mapNodes.find(v=>v.id==='g:'+producer.id)||this.mapNodes.find(v=>v.id==='r:'+producer.id);sources.set(key,{id:key,name:resource?.name||producer.name,type:['rotation','priority'].includes(block.kind)?tr('resource_schedule'):this.label(block.kind),count:managed.length,generated:true,addresses:[],target:target?.id});}
}
if(value.kind==='literal'&&typeof value.value==='string'){const id=value.value,resource=[...this.doc.schedules,...this.doc.escalations,...this.doc.routes].find(r=>r.id===id);if(resource)sources.set('saved:'+id,{id:'saved:'+id,name:resource.name,type:tr('source_saved_resource',{name:''}),addresses:[],target:'m:'+id});}
Object.entries(value).forEach(([key,child])=>{if(key!=='recipient'||!(value.team_all||value.team_random))scan(child,owner,depth+1);});
};
if(n.variable)variable(n.variable);else if(n.kind==='iteration')scan(n.source.source);else if(n.kind==='condition')scan(rule?.when);else if(rule){if(!n.block)scan(rule.iterations);scan(rule.blocks.filter(b=>!n.block||b.id===n.block));if(!n.block)scan(rule.when);}
return [...sources.values()];
},
mapInputSections(){
const n=this.mapCurrent();if(!n||!this.mapSnapshot||this.mapBusy)return [];
if(n.variable)return [{title:n.title,rows:[{label:this.label(n.source.kind),values:[this.sampleNode(n.source)]}]}];
const rule=this.mapSnapshot.payload.rules.find(r=>r.id===n.rule);if(!rule)return [];
const all=this.mapRows(),rows=this.mapRow==='all'?all:[all[Number(this.mapRow)]].filter(Boolean),unique=values=>[...new Map(values.filter(v=>v!==undefined).map(v=>[JSON.stringify(v),v])).values()];
if(n.kind==='iteration')return [{title:n.title,rows:[{label:n.source.name,values:unique(rows.map(r=>r.inputs?.[n.source.id]))}]}];
if(n.kind==='condition')return [{title:tr('map_condition'),rows:[{label:tr('map_condition_left'),values:unique(rows.map(r=>r.condition?.left))},{label:tr('map_condition_operation'),values:[this.label(rule.when.operation)]},{label:tr('map_condition_right'),values:unique(rows.map(r=>r.condition?.right))}]}];
return rule.blocks.filter(b=>!n.block||n.block===b.id).map(block=>{
const inputs=rows.flatMap(r=>(r.blocks||[]).filter(b=>b.block===block.id)),fields=this.fieldsFor(block.kind),out=[];
for(const field of fields){const values=unique(inputs.map(b=>b.fields[field.key]));if(values.length)out.push({label:field.label,values,binding:block.fields[field.key]});}
const maxSteps=Math.max(0,...inputs.map(b=>b.steps.length));for(let i=0;i<maxSteps;i++){out.push({label:tr('map_step_delay',{step:i+1}),values:unique(inputs.map(b=>b.steps[i]?.delay))},{label:tr('map_step_recipient',{step:i+1}),values:unique(inputs.map(b=>b.steps[i]?.recipient))});}
const conditions=unique(inputs.flatMap(b=>b.conditions));for(const c of conditions)out.push({label:this.label(c.field)+' · '+(c.negate?tr('map_not')+' ':'')+this.label(c.operation),values:[c.value]});
if(block.kind==='route'&&inputs.length&&!conditions.length)out.push({label:tr('map_condition'),values:[tr('map_matches_all')]});
return {title:this.label(block.kind),rows:out};
}).filter(section=>section.rows.length);
},
mapResourceFields(item){
const r=item.resource;if(!r)return [];const fields=[],add=(key,label,value,hint='')=>fields.push({key,label:tr(label),value,hint});
if(['rotation','priority'].includes(item.kind)){
add('participants',item.kind==='priority'?'responsible_people_in_priority_order_7205a5':'participants_in_rotation_order_4edee0',r.participants);
if(item.kind==='priority')add('fallback','fallback_schedule_7b7a21',r.fallback);
else {add('anchor','anchor_date_e77bd6',this.formatDate(r.anchor));add('shift_days','shift_length_days_0b28fa',r.shift_days);}
add('handover','handover_time_c9e315',r.handover.slice(0,5));
}else if(item.kind==='escalation'){
r.steps.forEach((step,i)=>{add('steps.'+i+'.recipient','escalation_recipient_9dfd31',step.recipient.id||(step.recipient.type==='team_all'?tr('everyone_on_the_team_235b55'):tr('a_random_team_member_8f35f3')),(step.delay_minutes===0?tr('immediately_21b60a'):step.delay_minutes+tr('min_after_alert_8adeb2'))+' · '+tr(step.condition==='if-not-acked'?'if_unacknowledged_407d03':'if_still_open_c34949'));add('steps.'+i+'.delay','delay_in_minutes_e18c95',step.delay_minutes);});
add('repeat_count','repeat_count_912e08',r.repeat.count);add('repeat_wait','minutes_between_repeats_f17ab0',r.repeat.wait_minutes);
}else if(item.kind==='route'){
r.conditions.forEach((c,i)=>add('conditions.'+i,'value_8dce17',c.value,this.conditionText(c)));
add('working','only_during_working_hours_fa1eb6',!!r.window);add('escalation','resource_detail_destination',r.escalation_id||tr('do_not_notify_1a59a6'));
}
return fields;
},
mapInputTokens(value){
const result=this.mapSnapshot?.report.result,refs=new Map();
for(const p of [...this.people,...(result?.people||[])]){const item={kind:tr('map_person'),text:p.display_name};refs.set(p.id,item);if(p.email)refs.set(p.email,item);}
for(const [kind,list] of [['schedule',result?.schedules||this.doc.schedules],['escalation',result?.escalations||this.doc.escalations],['route',result?.routes||this.doc.routes]])for(const r of list){refs.set(r.id,{kind:this.label(kind),text:r.name,generated:!!result?.managed.some(m=>m.id===r.id)});}
const tokens=[];const visit=(v,path='',depth=0)=>{if(tokens.length>=80||depth>10)return;if(typeof v==='string'&&refs.has(v)){tokens.push({...refs.get(v),path});return;}if(Array.isArray(v)){if(!v.length)tokens.push({text:tr('map_empty_value'),path});else v.forEach((item,i)=>visit(item,path?path+' / '+(i+1):String(i+1),depth+1));return;}if(v&&typeof v==='object'){Object.entries(v).forEach(([key,item])=>visit(item,path?path+' / '+key:key,depth+1));return;}tokens.push({path,text:v===null?tr('map_missing_value'):typeof v==='boolean'?tr(v?'true_88b33e':'false_97cdbd'):typeof v==='string'&&/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(v)?tr('map_missing_reference'):String(v)});};visit(value);return tokens;
},
mapToggle(){const n=this.mapCurrent();if(!n?.rule)return;this.mapExpanded[n.rule]=!this.mapExpanded[n.rule];this.drawGenerationMap();},
mapFit(){if(!graph)return;graph.resize();graph.fit(undefined,35);const box=graph.elements().boundingBox(),zoom=graph.zoom();graph.pan({x:35-box.x1*zoom,y:35-box.y1*zoom});},mapZoom(factor){if(graph)graph.zoom({level:Math.max(.2,Math.min(2,graph.zoom()*factor)),renderedPosition:{x:graph.width()/2,y:graph.height()/2}});},
async mapEdit(){const n=this.mapCurrent();if(!n)return;if(n.manual){this.closeGenerationMap();this.open(n.manualKind==='escalation'?'policy':n.manualKind,n.source);return;}this.closeGenerationMap();await this.openIssue(n.variable?{kind:'variable',id:n.variable}:{kind:'rule',id:n.rule,block:n.block});},
drawGenerationMap(){
if(!this.mapSnapshot || !this.mapOpen)return;graph?.destroy();const {payload,report}=this.mapSnapshot,nodes=[],edges=[],ids=new Set(),blockTargets=new Map();
const add=(id,title,subtitle,kind,x,y,meta={})=>{ids.add(id);nodes.push({id,title,subtitle,kind,...meta,position:{x,y}});};
const edge=(source,target,role='flow')=>{if(source!==target)edges.push({data:{id:role+':'+source+'>'+target,source,target,role}});};
const references=value=>{const out=[];const walk=v=>{if(!v||typeof v!=='object')return;if(v.kind==='variable'||v.kind==='lookup')out.push('v:'+v.variable);if(v.kind==='output')out.push('b:'+v.output);if(v.kind==='literal'&&typeof v.value==='string')out.push('m:'+v.value);if(v.kind==='literal')return;Object.entries(v).forEach(([key,child])=>{if(key!=='recipient'||!(v.team_all||v.team_random))walk(child);});};walk(value);return [...new Set(out)];};
const firstUse=new Map();payload.rules.forEach((r,i)=>references(r).forEach(id=>{if(!firstUse.has(id))firstUse.set(id,i);}));
const variables=[...payload.variables].sort((a,b)=>(firstUse.get('v:'+a.id)??Infinity)-(firstUse.get('v:'+b.id)??Infinity));
const columns=[],varX=new Map();let cursor=0;
payload.rules.forEach((r,i)=>{const vars=variables.filter(v=>firstUse.get('v:'+v.id)===i),width=Math.max(300,vars.length*260);columns.push(cursor+width/2);vars.forEach((v,j)=>varX.set(v.id,cursor+130+j*260));cursor+=width+40;});
variables.filter(v=>!varX.has(v.id)).forEach(v=>{varX.set(v.id,cursor+130);cursor+=280;});
const resourceY=420+Math.max(0,...payload.rules.map(r=>r.iterations.length+(r.when?1:0)))*90;
for(const v of variables)add('v:'+v.id,v.name,this.nodeSummary(v.node),'variable',varX.get(v.id),40,{variable:v.id,source:v.node,lane:0});
const resources=report.result||this.mapSnapshot.document,managed=report.result?.managed||[],all=[...resources.schedules,...resources.escalations,...resources.routes];
for(const [i,rule] of payload.rules.entries()){
const x=columns[i],trace=this.mapTrace(rule.id),count=managed.filter(m=>m.rule===rule.id).length,issue=report.issues.some(v=>v.rule_id===rule.id);
add('r:'+rule.id,rule.name,!rule.enabled?tr('disabled_f4f447'):issue?tr('map_invalid'):tr('map_count',{count}),'rule',x,240,{rule:rule.id,source:rule,step:i+1,lane:1,disabled:!rule.enabled,error:issue});
if(i)edge('r:'+payload.rules[i-1].id,'r:'+rule.id,'order');
let parent='r:'+rule.id;
rule.iterations.forEach((it,j)=>{const id='i:'+it.id;add(id,it.name,tr('map_loop',{count:trace?.stages.find(s=>s.iteration===it.id)?.count||0}),'iteration',x,335+j*90,{rule:rule.id,source:it,iteration:true,lane:1});edge(parent,id);parent=id;});
if(rule.when){const id='c:'+rule.id;add(id,tr('map_condition'),tr('map_filtered',{count:trace?.rows.filter(r=>r.status==='filtered').length||0}),'condition',x,335+rule.iterations.length*90,{rule:rule.id,source:rule.when,lane:1});edge(parent,id);}
let row=0;
for(const block of rule.blocks){
const produced=managed.filter(m=>m.block===block.id),id='b:'+block.id,kind=['rotation','priority'].includes(block.kind)?'schedule':block.kind;
blockTargets.set(block.id,id);
const single=produced.length===1?all.find(r=>r.id===produced[0].id):null;
add(id,single?.name||this.label(block.kind),report.result?tr('map_count',{count:produced.length}):tr('map_unverified'),'output',x,resourceY+row++*100,{rule:rule.id,block:block.id,source:block,lane:2,resourceKind:kind,iteration:produced.length>1});edge('r:'+rule.id,id);
if(this.mapExpanded[rule.id]&&produced.length>1)for(const m of produced){const resource=all.find(r=>r.id===m.id);add('o:'+m.id,resource?.name||m.id,this.label(block.kind),'output',x,resourceY+row++*100,{rule:rule.id,block:block.id,resourceId:m.id,source:block,lane:2,resourceKind:kind});edge(id,'o:'+m.id);}
}
}
const manual=all.filter(r=>!(resources.managed||resources.generation?.managed||[]).some(m=>m.id===r.id));
manual.forEach((r,i)=>{const kind=resources.schedules.includes(r)?r.type:resources.escalations.includes(r)?'escalation':'route';add('m:'+r.id,r.name,tr('map_manual'),'output',cursor+150,resourceY+i*100,{manual:true,resourceId:r.id,source:r,lane:2,resourceKind:['priority','rotation'].includes(kind)?'schedule':kind,manualKind:kind});});
payload.rules.forEach(rule=>{for(const ref of references(rule)){const target=ref.startsWith('b:')?blockTargets.get(ref.slice(2)):ref;if(target)edge(target,'r:'+rule.id,'dependency');}});
const maxX=Math.max(400,...nodes.map(n=>n.position.x))+170,minX=-180;
const guides=[];for(const [i,y] of [140,resourceY-60].entries()){guides.push({data:{id:'line-left-'+i,kind:'guide',label:''},position:{x:minX,y},selectable:false,grabbable:false},{data:{id:'line-right-'+i,kind:'guide',label:''},position:{x:maxX,y},selectable:false,grabbable:false},{data:{id:'line-'+i,source:'line-left-'+i,target:'line-right-'+i,role:'boundary'}});}
[ ['map_variables',40],['map_generators',240],['map_resource_lane',resourceY] ].forEach(([key,y],i)=>guides.push({data:{id:'lane-'+i,kind:'lane',label:tr(key)},position:{x:minX,y},selectable:false,grabbable:false}));
this.mapNodes=nodes;graph=cytoscape({container:this.$refs.generationCanvas,elements:[...guides,...nodes.map(n=>({data:{id:n.id,label:(n.step?n.step+'. ':'')+n.title+'\n'+n.subtitle,kind:n.kind,resourceKind:n.resourceKind||'',disabled:!!n.disabled,error:!!n.error},position:n.position})),...edges.filter(e=>ids.has(e.data.source)&&ids.has(e.data.target))],layout:{name:'preset'},minZoom:.15,maxZoom:2,wheelSensitivity:.2,style:[
{selector:'node',style:{shape:'round-rectangle',width:220,height:66,'background-color':'#fff','border-color':'#92a89a','border-width':1.5,label:'data(label)','text-wrap':'wrap','text-max-width':185,'text-valign':'center',color:'#243c32','font-family':'system-ui','font-size':11}},
{selector:'node[kind="variable"]',style:{shape:'ellipse','background-color':'#f5eedf'}},
{selector:'node[kind="rule"],node[kind="iteration"],node[kind="condition"]',style:{shape:'hexagon','background-color':'#e4eee9',width:240}},
{selector:'node[resourceKind="schedule"]',style:{'background-color':'#e5efff','border-color':'#799dd1'}},
{selector:'node[resourceKind="escalation"]',style:{'background-color':'#f2e8fb','border-color':'#b18ace'}},
{selector:'node[resourceKind="route"]',style:{'background-color':'#fff0d9','border-color':'#c5a46c'}},
{selector:'node[?disabled]',style:{opacity:.5}},{selector:'node[?error]',style:{'border-color':'#bd4343','border-width':3}},
{selector:'node[kind="guide"]',style:{width:1,height:1,opacity:0,events:'no'}},
{selector:'node[kind="lane"]',style:{width:150,height:35,shape:'rectangle','border-width':0,'background-opacity':0,'font-weight':700,'text-max-width':145,events:'no'}},
{selector:'edge',style:{width:1.5,'line-color':'#96a99d','target-arrow-color':'#739681','target-arrow-shape':'triangle','curve-style':'bezier'}},
{selector:'edge[role="order"]',style:{width:2.5,'line-color':'#428465','curve-style':'straight'}},
{selector:'edge[role="dependency"]',style:{'line-style':'dashed',opacity:.5}},
{selector:'edge[role="boundary"]',style:{'target-arrow-shape':'none','curve-style':'straight','line-color':'#b9c9be',events:'no'}},
{selector:'.related',style:{'border-color':'#286448',opacity:1}},{selector:'node.selected',style:{'border-width':3,'border-color':'#286448'}}
]});graph.on('tap','node',e=>{if(ids.has(e.target.id()))this.mapSelect(e.target.id());});graph.on('tap',e=>{if(e.target===graph){this.mapSelected='';graph.elements().removeClass('selected related');}});this.mapFit();if(!ids.has(this.mapSelected))this.mapSelected=nodes.find(n=>n.rule)?.id||nodes[0]?.id||'';if(this.mapSelected)this.mapSelect(this.mapSelected);
}
};}