Download index.html from Hugdrigo/processscope-process-mining-lab: direct link, hf CLI and curl.
- Browser
- Download file 58.4 kB
-
https://huggingface.co/spaces/Hugdrigo/processscope-process-mining-lab/resolve/main/index.html
- Command line
-
hf download hf://spaces/Hugdrigo/processscope-process-mining-lab/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Hugdrigo/processscope-process-mining-lab/resolve/main/index.html
58.4 kB
| <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#080d19"><meta name="description" content="ProcessScope: browser-based process mining, workflow bottleneck and SLA audit lab using event logs."><title>ProcessScope | Process Mining Intelligence Lab</title><style>:root{--bg:#080d19;--panel:#101a2c;--panel2:#142039;--line:#263550;--white:#eef5ff;--muted:#98aac5;--mint:#41e3bb;--mint2:#99f5dc;--pink:#ffad88;--blue:#8cadf8;--red:#ff7a94;--yellow:#ffc96a;--radius:17px}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:radial-gradient(ellipse 60% 30% at 72% -6%,#172945 0,transparent 90%),var(--bg);color:var(--white);font:14px/1.5 Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}button,select,input,textarea{font:inherit}button{cursor:pointer}button:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid var(--mint);outline-offset:3px}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:#354660;border-radius:10px}#app{min-height:100vh}.layout{display:grid;grid-template-columns:238px minmax(0,1fr);min-height:100vh}.sidebar{background:#0b1222;border-right:1px solid #1c2b46;position:sticky;top:0;height:100vh;display:flex;flex-direction:column;padding:24px 15px 18px;z-index:20}.brand{display:flex;gap:12px;align-items:center;padding:5px 10px 24px}.mark{height:38px;width:38px;display:grid;place-items:center;background:linear-gradient(140deg,#3ef0bc,#4eb9e4);border-radius:12px;font-size:20px;color:#081422;font-weight:950;box-shadow:0 0 32px #3eeaba30}.brand strong{font-size:19px;font-weight:850;letter-spacing:-.7px}.brand small{display:block;text-transform:uppercase;letter-spacing:1.5px;font-size:9px;color:var(--muted);margin-top:0}.side-title{font-size:10px;color:#647996;letter-spacing:1.7px;font-weight:800;padding:17px 15px 9px}.nav{width:100%;border:1px solid transparent;display:flex;gap:12px;align-items:center;color:#a5b7cf;background:transparent;padding:11px 12px;border-radius:10px;text-align:left;font-weight:630;transition:background .2s,color .2s}.nav:hover{background:#16243b;color:#fff}.nav.active{color:var(--mint2);background:#173a42;border-color:#275c5c}.nav .glyph{font-size:16px;width:21px;text-align:center}.side-bottom{margin-top:auto;padding:17px 12px;border-top:1px solid #203047;color:#70829e;font-size:11px}.side-bottom b{color:#b1c7df}.status-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--mint);margin-right:7px;box-shadow:0 0 10px var(--mint)}.main{padding:27px 36px 60px;min-width:0;max-width:1650px;width:100%;margin:auto}.topbar{display:flex;align-items:center;justify-content:space-between;gap:15px;margin-bottom:26px}.crumb{color:#8396b2;font-size:12px;font-weight:700;letter-spacing:.6px}.crumb strong{color:#c7d8eb}.top-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.tiny{font-size:11px;color:#8192aa}.pill{display:inline-flex;align-items:center;gap:7px;padding:7px 12px;border:1px solid #2c4956;border-radius:100px;background:#142b34;font-size:11px;color:#9af1d3;font-weight:750}.primary,.secondary,.ghost{border-radius:10px;padding:9px 13px;font-weight:750;font-size:12px;display:inline-flex;align-items:center;gap:8px;justify-content:center;transition:filter .2s,transform .2s}.primary:hover,.secondary:hover,.ghost:hover{filter:brightness(1.1)}.primary{background:var(--mint);border:1px solid var(--mint);color:#0c1d28}.secondary{background:#182941;border:1px solid #35506e;color:#dfebfc}.ghost{background:transparent;border:1px solid #31435f;color:#bed0e9}.heading{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin-bottom:24px}.eyebrow{display:inline-flex;align-items:center;gap:7px;font-size:10px;color:#6be9c5;font-weight:850;letter-spacing:1.7px;text-transform:uppercase;margin-bottom:6px}h1{font-size:clamp(28px,3vw,39px);line-height:1.08;letter-spacing:-1.4px;margin:0 0 8px;font-weight:850}h2{font-size:16px;letter-spacing:-.4px;margin:0;font-weight:790}h3{font-size:13px;margin:0 0 9px}p{margin:0}.sub{color:var(--muted);max-width:800px;font-size:13px}.controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap}select,input[type=number],input[type=text],textarea{background:#0b1526;color:#e4eefa;border:1px solid #31425e;border-radius:9px;padding:9px 10px;min-height:38px;max-width:100%}select{min-width:145px}.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:15px;margin-bottom:20px}.stat{background:linear-gradient(140deg,#14233b,#10192c);border:1px solid #283954;border-radius:var(--radius);padding:18px 19px;position:relative;overflow:hidden}.stat::before{content:'';width:52px;height:3px;background:var(--mint);position:absolute;top:0;left:19px;border-radius:4px}.stat.orange::before{background:var(--pink)}.stat.blue::before{background:var(--blue)}.stat.yellow::before{background:var(--yellow)}.stat .name{font-size:11px;font-weight:700;color:#a8b9ce}.stat .value{font-size:31px;font-weight:850;letter-spacing:-1px;line-height:1.2;margin:10px 0 7px;font-variant-numeric:tabular-nums}.stat .hint{font-size:11px;color:#8295ae}.grid2{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(310px,1fr);gap:16px}.grid-half{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.panel{background:linear-gradient(150deg,#111c30,#101a2c);border:1px solid #293951;border-radius:var(--radius);padding:20px;min-width:0;margin-bottom:16px}.panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px}.subtitle{font-size:11px;color:#8fa5bc;margin-top:4px}.eyebadge{font-size:10px;color:#91ebd4;background:#133a3c;padding:4px 8px;border:1px solid #275753;border-radius:6px}.chart-area{width:100%;overflow:hidden;border:1px solid #243851;border-radius:12px;background:radial-gradient(ellipse at 40% 40%,#192942bb,#0e1728 90%);position:relative}#processMap svg{width:100%;height:auto;min-height:280px;display:block}.node{cursor:pointer}.node rect{fill:#223650;stroke:#5c779b;stroke-width:1.5;rx:11}.node.active rect{stroke:var(--mint);fill:#1a4647;stroke-width:2}.node text{fill:#dceafb;font-size:11px;font-weight:750}.edge{fill:none;stroke:#6585a5;stroke-opacity:.72;stroke-linecap:round}.edge.active{stroke:var(--mint);stroke-opacity:1}.edge-label{fill:#96b3cb;font-size:10px}.legend{font-size:11px;color:#849cb7;margin-top:8px;display:flex;gap:15px;flex-wrap:wrap}.circle-mint{display:inline-block;width:8px;height:8px;background:var(--mint);border-radius:50%;margin-right:5px}.table-wrap{max-width:100%;overflow:auto;border:1px solid #293c56;border-radius:10px}table{border-collapse:collapse;width:100%;font-size:12px}th{background:#192940;color:#a5b8d0;letter-spacing:.6px;font-size:10px;text-transform:uppercase;text-align:left}th,td{padding:10px 11px;border-bottom:1px solid #263952;white-space:nowrap}tr:last-child td{border-bottom:none}tbody tr:hover{background:#172a40}td{color:#d5e1f1;font-variant-numeric:tabular-nums}.row-click{cursor:pointer}.muted{color:#8da0bc}.positive{color:var(--mint2)}.negative{color:#ff9bb0}.track{height:8px;border-radius:10px;background:#24364e;overflow:hidden}.track>span{height:100%;display:block;background:linear-gradient(90deg,#36caa9,#7bebd3);border-radius:inherit}.track.warn>span{background:linear-gradient(90deg,#f8a86b,#ff778e)}.mini-bars{display:flex;align-items:flex-end;gap:5px;height:150px;border-bottom:1px solid #3e5271;padding:16px 8px 0}.mini-bars>div{flex:1;min-width:4px;border-radius:4px 4px 0 0;background:linear-gradient(0deg,#256a7e,#5ae6c4);position:relative;cursor:help}.mini-bars>div:nth-child(3n){background:linear-gradient(0deg,#42659c,#86b9ee)}.donut{width:152px;height:152px;border-radius:50%;display:grid;place-items:center;margin:auto;background:conic-gradient(var(--mint) var(--pct),#2b3d5a 0)}.donut-core{background:#121e32;width:108px;height:108px;border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center}.donut-core b{font-size:24px}.small{font-size:11px;color:#93a8c0}.split{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.metric-row{margin:13px 0}.metric-row .topline{display:flex;justify-content:space-between;margin-bottom:6px;font-size:11px}.metric-row .topline b{color:#e9f3fc}.empty{padding:28px;text-align:center;color:#92a5bc}.view{display:none}.view.active{display:block;animation:fadein .18s ease-out}@keyframes fadein{from{opacity:.4;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}.annotation{font-size:12px;color:#b7c9dd;background:#172941;border:1px solid #315270;border-radius:10px;padding:11px 13px;line-height:1.5;margin:14px 0}.annotation.warn{border-color:#79564b;background:#36252a;color:#ffc3aa}.config{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap}.field{display:flex;flex-direction:column;gap:5px;font-size:11px;color:#abc1d9;font-weight:700}.field select,.field input{min-width:130px}.file-drop{border:2px dashed #3b5673;background:#101e33;border-radius:13px;text-align:center;padding:28px;color:#b2c3dc;cursor:pointer;transition:background .2s}.file-drop:hover,.file-drop.drag{background:#1b3648;border-color:var(--mint)}.file-drop strong{font-size:15px;color:#e3f5f0}.file-drop input{display:none}.download-grid{display:flex;flex-wrap:wrap;gap:10px;margin-top:12px}.code-example{padding:14px;background:#08111f;border-radius:10px;border:1px solid #2d3e57;color:#9fd4c9;white-space:pre;overflow:auto;font:11px/1.6 ui-monospace,monospace}.toast{position:fixed;bottom:20px;right:24px;background:#204b45;color:#e4fff6;box-shadow:0 16px 40px #0008;border:1px solid #41977d;padding:13px 17px;border-radius:11px;font-size:12px;max-width:min(390px,90vw);z-index:70;display:none}.toast.error{background:#502634;border-color:#a64b5c;color:#ffdde2}.toast.show{display:block}.mobile-head{display:none}.panel-reveal{font-size:12px;color:#9fb6d0}.tag{display:inline-flex;align-items:center;gap:4px;background:#22364e;border-radius:7px;padding:4px 8px;color:#becfe4;font-size:10px;font-weight:650}.tag.good{background:#184237;color:#9ef0d2}.tag.bad{background:#4b2936;color:#ffb0c0}.flex{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.sr-only{position:absolute;width:1px;height:1px;clip:rect(0,0,0,0);overflow:hidden}footer{font-size:10px;color:#788ca6;padding-top:24px;border-top:1px solid #29374a;margin-top:15px}.plot-svg{width:100%;height:170px;display:block}.plot-svg .grid-line{stroke:#354862;stroke-dasharray:3 5}.plot-svg text{fill:#91a6be;font-size:10px}.plot-svg .area{fill:#284b59;fill-opacity:.5}.plot-svg .curve{stroke:#51e2bd;stroke-width:3;fill:none}.method-note{font-size:11px;color:#8fa7c1;padding:9px 0}.case-activities{line-height:2}.variant-name{white-space:normal;min-width:190px;max-width:400px}.overlay{display:none} @media(max-width:1150px){.stats{grid-template-columns:repeat(2,1fr)}.grid2{grid-template-columns:1fr}.main{padding:25px 25px 55px}}@media(max-width:800px){.layout{display:block}.sidebar{transform:translateX(-105%);transition:transform .2s;position:fixed;width:260px;height:100dvh;box-shadow:10px 0 40px #0009}.sidebar.open{transform:translateX(0)}.overlay.show{display:block;position:fixed;inset:0;background:#020711ae;z-index:19}.mobile-head{display:flex;align-items:center;gap:12px;margin-bottom:20px}.hamb{background:#1b2e47;color:#fff;border:1px solid #3a4c67;border-radius:9px;padding:7px 11px}.main{padding:20px 16px 40px}.topbar{display:none}.heading{display:block}.controls{margin-top:15px}.stats{gap:9px}.stat{padding:14px}.stat .value{font-size:26px}.panel{padding:14px}.grid-half{grid-template-columns:1fr}.top-right{flex-wrap:wrap}h1{font-size:28px}.file-drop{padding:20px 12px}#processMap svg{min-height:220px;min-width:630px}#mapDetail svg{min-width:630px}.chart-area{overflow-x:auto}}@media(max-width:400px){.stats{grid-template-columns:1fr 1fr}.stat .value{font-size:23px}th,td{padding:9px 8px}} | |
| </style></head><body> | |
| <div id="app" class="layout"><aside id="sidebar" class="sidebar" aria-label="Primary navigation"><div class="brand"><div class="mark">↗</div><div><strong>ProcessScope</strong><small>Operational intelligence</small></div></div><div class="side-title">WORKSPACE</div><button class="nav active" data-view="overview"><span class="glyph">▦</span>Overview</button><button class="nav" data-view="map"><span class="glyph">◈</span>Process discovery</button><button class="nav" data-view="variants"><span class="glyph">≋</span>Journey variants</button><button class="nav" data-view="bottlenecks"><span class="glyph">◷</span>Bottleneck explorer</button><button class="nav" data-view="sla"><span class="glyph">▥</span>SLA & segments</button><div class="side-title">MANAGE</div><button class="nav" data-view="data"><span class="glyph">⇅</span>Data & audit exports</button><div class="side-bottom"><div><span class="status-dot"></span><b>LOCAL-FIRST ANALYSIS</b></div><p style="margin-top:7px">No API keys · No servers · No tracking</p><p style="margin-top:14px">PROCESS MINING LAB · v1.0</p></div></aside><div id="overlay" class="overlay"></div> | |
| <main class="main"><div class="mobile-head"><button class="hamb" id="toggleMenu" aria-label="Open navigation">☰</button><div class="brand" style="padding:0"><div class="mark" style="height:32px;width:32px">↗</div><strong>ProcessScope</strong></div></div><div class="topbar"><div class="crumb">HUGDRIGO / RESEARCH LABS / <strong>PROCESS INTELLIGENCE</strong></div><div class="top-right"><span class="tiny" id="caseTotal"></span><span class="tiny" id="slaVal"></span><span class="pill"><span class="status-dot"></span> Browser-only</span><span class="tiny">Open methodology · MIT</span><button class="secondary" data-export="json">↓ Export audit</button></div></div> | |
| <div class="heading"><div><div class="eyebrow"><span class="status-dot"></span> PROJECT 40 · DATA INTELLIGENCE</div><h1 id="pageTitle">Process intelligence</h1><p id="pageSubtitle" class="sub">From event logs to operational clarity.</p></div><div class="controls"><span class="small" id="projectLabel">SYNTHETIC DATASET</span><span class="sr-only" id="scenarioName"></span><span class="sr-only" id="datasetHint"></span><select id="scenario" aria-label="Choose demo dataset"><option value="fulfillment">Order fulfillment</option><option value="support">Customer support</option><option value="permits">Permit review</option></select></div></div> | |
| <section class="view active" id="view-overview"><div id="stats" class="stats"></div><div class="grid2"><div class="panel"><div class="panel-head"><div><h2>Directly-follows process graph</h2><p class="subtitle">Actual observed steps and transitions from the event log</p></div><button class="ghost" data-view-link="map">Explore map →</button></div><div class="chart-area" id="processMap"></div><div class="legend"><span><span class="circle-mint"></span> Select nodes to inspect</span><span>Edge width = transition count</span><span>Dashed curves = backward transitions</span></div><div id="nodeInspector" class="annotation"></div></div><div class="panel"><div class="panel-head"><div><h2>Slowest transitions</h2><p class="subtitle">Mean elapsed wait per observed transition</p></div><span class="eyebadge">TOP 5</span></div><div id="slowTransitions"></div><div class="annotation">A long interval does not prove that work was actively performed during that period.</div></div></div><div class="grid-half"><div class="panel"><div class="panel-head"><div><h2>Lead-time distribution</h2><p class="subtitle">Elapsed hours between first and final logged event</p></div></div><div id="durationHistogram"></div></div><div class="panel"><div class="panel-head"><div><h2>Early vs later cases</h2><p class="subtitle">Chronological split, descriptive comparison</p></div></div><div id="periods"></div></div></div><div class="panel"><h2>Key audit findings</h2><div id="overviewInsights"></div></div></section> | |
| <section class="view" id="view-map"><div class="panel"><div class="panel-head"><div><h2>Interactive activity network</h2><p class="subtitle">Follow the flow of actual events; select a node</p></div><div class="config"><label class="field">Minimum transition count <input type="range" min="1" max="25" value="1" id="minEdge" style="width:125px;accent-color:#41e3bb"></label><span class="eyebadge" id="minEdgeLabel">1</span></div></div><div class="chart-area" id="mapDetail"></div><p class="method-note" id="mapNote"></p></div><div class="grid-half"><div class="panel"><h2>Selected activity</h2><div id="nodeInspectorPlaceholder" class="annotation">Click a node on the map to inspect its role. The selection is also highlighted on the dashboard.</div></div><div class="panel"><h2>Transition table</h2><div id="edgeTable" class="table-wrap" style="margin-top:12px"></div></div></div></section> | |
| <section class="view" id="view-variants"><div class="grid-half"><div class="panel"><h2>Path diversity</h2><p class="subtitle">Observed activity sequences, exact matching</p><div id="variantStats" style="margin-top:16px"></div></div><div class="panel"><h2>What is a variant?</h2><div class="annotation">A process variant is an exact list of activities, in timestamp order, for one case. If a case repeats a step, that repetition remains visible rather than being silently removed.</div><div class="annotation">Example: Received → Validated → Packed → Delivered. Alternative routes, skips and loops appear as distinct paths.</div></div></div><div class="panel"><div class="panel-head"><h2>Most frequent observed journeys</h2><span class="eyebadge">TOP 12</span></div><div class="table-wrap" id="variantRows"></div></div></section> | |
| <section class="view" id="view-bottlenecks"><div class="grid-half"><div class="panel"><h2>Lead-time spread</h2><div id="leadDist"></div></div><div class="panel"><h2>Operational signals</h2><div id="bottleneckStats" style="margin-top:14px"></div><div class="annotation warn">Waiting intervals measure time between log entries. They do not measure employee effort, queue length or utilization directly.</div></div></div><div class="panel"><div class="panel-head"><h2>Longest average transition intervals</h2><span class="eyebadge">SORTED BY MEAN</span></div><div id="bottleneckTable" class="table-wrap"></div></div></section> | |
| <section class="view" id="view-sla"><div class="panel"><div class="panel-head"><div><h2>SLA policy & completion rule</h2><p class="subtitle">Updates instantly recalculate every metric</p></div></div><div class="config"><label class="field">SLA limit (calendar hours)<input id="slaInput" type="number" min="1" max="240" step="1" value="72"></label><label class="field">Required activity<div id="requiredWrap"></div></label><label class="field">Completion activity<div id="completeWrap"></div></label></div><div id="slaWarnings"></div></div><div class="grid-half"><div class="panel"><h2>SLA by team</h2><p class="subtitle">Attributed to the first event of each case</p><div class="table-wrap" id="teamTable" style="margin-top:12px"></div></div><div class="panel"><h2>SLA by channel</h2><p class="subtitle">Grouped by first recorded channel</p><div class="table-wrap" id="channelTable" style="margin-top:12px"></div></div></div><div class="panel"><h2>Daily cohort SLA breach rate</h2><p class="subtitle">Grouped by case start date</p><div id="slaTrend"></div></div></section> | |
| <section class="view" id="view-data"><div class="grid-half"><div class="panel"><div class="panel-head"><h2>Import a local event log</h2><span class="eyebadge">NO UPLOAD TO SERVER</span></div><div id="drop" class="file-drop" role="button" tabindex="0" aria-label="Select a CSV event log"><strong>⇧ Drop a CSV here or click to browse</strong><p class="small" style="margin-top:5px">Up to 3 MB · 12,000 events · local browser only</p><input type="file" accept=".csv,text/csv" id="csvInput"></div><div class="download-grid"><button class="secondary" data-export="sample">↓ Download CSV template</button><button class="ghost" id="reset">↺ Reset demo</button></div><p class="method-note">Required columns: <b>case_id, activity, timestamp</b>. Optional: team, channel, cost. Use ISO 8601 with Z or timezone offset.</p></div><div class="panel"><h2>Active dataset</h2><div id="dataStats" style="margin-top:14px"></div><div id="qualityNote"></div></div></div><div class="grid-half"><div class="panel"><h2>Audit settings</h2><div class="config" style="margin-top:14px"><label class="field">SLA limit (h)<input type="number" id="slaInput2" min="1" max="240" value="72"></label></div><div class="annotation">Completion means the final recorded activity matches your selected completion step. Required-step audit flags missing events; it does not establish legal or regulatory compliance.</div><h2>Export reproducible results</h2><div class="download-grid"><button class="secondary" data-export="json">↓ Audit JSON</button><button class="secondary" data-export="cases">↓ Cases CSV</button><button class="secondary" data-export="edges">↓ Edges CSV</button><button class="secondary" data-export="events">↓ Events CSV</button><button class="secondary" data-export="svg">↓ Graph SVG</button><button class="secondary" data-export="md">↓ Brief Markdown</button></div></div><div class="panel"><h2>Case explorer</h2><div class="config" style="margin:12px 0"><label class="field">Find a case<input type="text" placeholder="Case ID or activity" id="searchCases" maxlength="80"></label><label class="field">Team<div id="teamWrap"></div></label><label class="field">Sort<select id="caseSort"><option value="duration">Longest first</option><option value="oldest">Earliest first</option><option value="cost">Highest cost first</option></select></label></div><div class="small" id="caseShown"></div><div class="table-wrap" id="caseRows" style="margin-top:10px;max-height:300px"></div></div></div><div class="panel"><h2>Selected case trail</h2><div id="caseDetail" style="margin-top:10px"></div></div><div class="panel"><h2>CSV structure</h2><pre class="code-example">case_id,activity,timestamp,team,channel,cost | |
| A-001,Received,2026-04-01T08:00:00Z,Ops,Web,12 | |
| A-001,Validated,2026-04-01T10:00:00Z,Ops,Web,8 | |
| A-001,Completed,2026-04-01T11:00:00Z,Ops,Web,5</pre><div class="annotation"><strong>Privacy & accuracy:</strong> Run locally without network requests, but do not put sensitive data into a public Space repository or share exported audits indiscriminately. Uploaded records are held only in this browser session. The provided sample data are synthetic.</div></div></section> | |
| <footer>PROCESSSCOPE · HUGDRIGO RESEARCH PORTFOLIO · MIT LICENSE · Synthetic datasets for demonstrations · No external dependencies · Analysis is exploratory, not a production SLA certification</footer></main></div><div id="toast" class="toast" role="status" aria-live="polite"></div><script>/* ProcessScope 1.0: deterministic, dependency-free process analytics core. */ | |
| (function(root, factory){const value=factory();if(typeof module!=='undefined' && module.exports)module.exports=value;root.PSCore=value;})(typeof globalThis!=='undefined'?globalThis:this,function(){ | |
| ; | |
| const VERSION='1.0.0', MAX_ROWS=12000, REQUIRED=['case_id','activity','timestamp']; | |
| const median=arr=>{if(!arr.length)return null;const x=[...arr].sort((a,b)=>a-b);const m=x.length>>1;return x.length%2?x[m]:(x[m-1]+x[m])/2;}; | |
| const average=arr=>arr.length?arr.reduce((a,b)=>a+b,0)/arr.length:null; | |
| const percentile=(arr,p)=>{if(!arr.length)return null;const x=[...arr].sort((a,b)=>a-b),i=(x.length-1)*p,l=Math.floor(i),h=Math.ceil(i);return x[l]+(x[h]-x[l])*(i-l)}; | |
| function csvParse(src){if(typeof src!=='string'||!src.trim())throw Error('El archivo CSV está vacío.');if(src.length>3e6)throw Error('El CSV excede 3 MB.');src=src.replace(/^\uFEFF/,'');const rows=[];let row=[],cell='',quoted=false,afterQuote=false;for(let i=0;i<src.length;i++){let ch=src[i];if(quoted){if(ch==='"'){if(src[i+1]==='"'){cell+='"';i++}else{quoted=false;afterQuote=true}}else cell+=ch;continue;}if(afterQuote){if(ch===','){row.push(cell);cell='';afterQuote=false;continue}if(ch==='\r'||ch==='\n'){row.push(cell);rows.push(row);row=[];cell='';afterQuote=false;if(ch==='\r'&&src[i+1]==='\n')i++;continue}if(ch===' '||ch==='\t')continue;throw Error('CSV inválido: caracteres después de una comilla de cierre.');}if(ch==='"'){if(cell.trim())throw Error('CSV inválido: comillas dentro de un campo no entrecomillado.');cell='';quoted=true;}else if(ch===','){row.push(cell);cell=''}else if(ch==='\n'||ch==='\r'){row.push(cell);rows.push(row);row=[];cell='';if(ch==='\r'&&src[i+1]==='\n')i++}else cell+=ch;} | |
| if(quoted)throw Error('CSV inválido: comillas sin cerrar.');if(afterQuote||cell!==''||row.length){row.push(cell);rows.push(row);}return rows.filter(r=>r.some(c=>c.trim()!==''));} | |
| function parseEventsCSV(src){const rows=csvParse(src);if(rows.length<2)throw Error('El CSV necesita encabezados y registros.');const headers=rows[0].map(x=>x.trim().toLowerCase());if(new Set(headers).size!==headers.length)throw Error('Hay encabezados duplicados.');for(const h of REQUIRED)if(!headers.includes(h))throw Error('Falta la columna obligatoria: '+h);if(rows.length-1>MAX_ROWS)throw Error('El máximo es '+MAX_ROWS+' eventos.');const events=[];for(let i=1;i<rows.length;i++){const row=rows[i];if(row.length!==headers.length)throw Error('Fila '+(i+1)+': columnas incompatibles.');const r=Object.fromEntries(headers.map((h,j)=>[h,row[j].trim()]));events.push({...r,_line:i+1});}return validateEvents(events);} | |
| const ISO_RE=/^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d{1,3})?)?(?:Z|[+-]\d\d:\d\d)$/i; | |
| function validateEvents(input){if(!Array.isArray(input)||!input.length)throw Error('No hay eventos para analizar.');if(input.length>MAX_ROWS)throw Error('Demasiados eventos. Máximo '+MAX_ROWS);const out=[],ids=new Set(),acts=new Set();for(let i=0;i<input.length;i++){const r=input[i],line=r._line||i+1;const id=String(r.case_id??'').trim(),act=String(r.activity??'').trim(),stamp=String(r.timestamp??'').trim();if(!id||!act)throw Error('Fila '+line+': case_id y activity son obligatorios.');if(id.length>90||act.length>70)throw Error('Fila '+line+': etiqueta demasiado larga.');if(!ISO_RE.test(stamp))throw Error('Fila '+line+': timestamp debe incluir hora y zona, por ejemplo 2026-01-01T10:00:00Z.');const time=Date.parse(stamp);if(!Number.isFinite(time))throw Error('Fila '+line+': fecha inválida.');const team=String(r.team??'No definido').trim()||'No definido',channel=String(r.channel??'No definido').trim()||'No definido';if(team.length>50||channel.length>50)throw Error('Fila '+line+': segmento demasiado largo.');const strCost=String(r.cost??'').trim();let cost=0;if(strCost!==''){cost=Number(strCost);if(!Number.isFinite(cost)||cost<0||cost>1e9)throw Error('Fila '+line+': cost debe ser numérico no negativo.');}ids.add(id);acts.add(act);out.push({case_id:id,activity:act,timestamp:new Date(time).toISOString(),t:time,team,channel,cost,_order:i});}if(ids.size>5000)throw Error('Se admiten hasta 5.000 casos.');if(acts.size>45)throw Error('Se admiten hasta 45 actividades.');return out;} | |
| function safeCSV(rows,head){const protect=x=>{let s=x===null||x===undefined?'':String(x);if(/^[\s]*[=+@-]/.test(s))s="'"+s;return '"'+s.replace(/"/g,'""')+'"';};return [head,...rows].map(r=>r.map(protect).join(',')).join('\r\n');} | |
| function groupCases(events){const m=new Map();for(const e of events){if(!m.has(e.case_id))m.set(e.case_id,[]);m.get(e.case_id).push(e);}const list=[];for(const [id,x] of m){x.sort((a,b)=>a.t-b.t||a._order-b._order);let cost=0;for(const e of x)cost+=e.cost;list.push({id,events:x,team:x[0].team,channel:x[0].channel,start:x[0].t,end:x[x.length-1].t,duration:(x[x.length-1].t-x[0].t)/3600000,sequence:x.map(e=>e.activity),cost,eventsCount:x.length,variant:x.map(e=>e.activity).join(' → '),last:x[x.length-1].activity});}list.sort((a,b)=>a.start-b.start||a.id.localeCompare(b.id));return list;} | |
| const rates=(a,b)=>b?100*a/b:0; | |
| function analyze(events,options={}){const list=groupCases(events), sla=+options.slaHours||24, completed=options.completed||'Complete',required=options.required||'None',threshold=+options.edgeMinimum||1;const count=list.length;if(!count)throw Error('Sin casos.');const durations=list.map(c=>c.duration),all=new Map(),edges=new Map(),variants=new Map(),teams=new Map(),channels=new Map();let breaches=0,done=0,missing=0,loops=0,cost=0,ambiguous=0;const daily=new Map();const ds=[]; | |
| for(const c of list){c.breached=c.duration>sla;c.completed=c.last===completed;c.missing=required!=='None'&&!c.sequence.includes(required);c.reworked=c.sequence.some((a,i)=>i&&a===c.sequence[i-1]);if(c.breached)breaches++;if(c.completed)done++;if(c.missing)missing++;if(c.reworked)loops++;cost+=c.cost;const day=new Date(c.start).toISOString().slice(0,10);if(!daily.has(day))daily.set(day,{date:day,cases:0,late:0,hours:0});const d=daily.get(day);d.cases++;d.late+=c.breached?1:0;d.hours+=c.duration; | |
| const v=variants.get(c.variant)||{name:c.variant,count:0,durations:[],late:0,complete:0};v.count++;v.durations.push(c.duration);v.late+=c.breached?1:0;v.complete+=c.completed?1:0;variants.set(c.variant,v); | |
| for(const [map,key] of [[teams,c.team],[channels,c.channel]]){const g=map.get(key)||{name:key,cases:0,late:0,complete:0,durations:[],cost:0};g.cases++;g.late+=c.breached?1:0;g.complete+=c.completed?1:0;g.durations.push(c.duration);g.cost+=c.cost;map.set(key,g);} | |
| for(let i=0;i<c.events.length;i++){const e=c.events[i],n=all.get(e.activity)||{name:e.activity,hits:0,cases:new Set(),indexSum:0};n.hits++;n.cases.add(c.id);n.indexSum+=i;n.indexCount=(n.indexCount||0)+1;all.set(e.activity,n);if(i<c.events.length-1){const next=c.events[i+1],k=e.activity+'\u0000'+next.activity,gap=(next.t-e.t)/3600000,w=edges.get(k)||{from:e.activity,to:next.activity,hours:[],cases:new Set(),sameTime:0};w.hours.push(gap);w.cases.add(c.id);if(gap===0){w.sameTime++;ambiguous++;}edges.set(k,w);}}} | |
| const formatGroup=g=>({...g,avg:average(g.durations),median:median(g.durations),p90:percentile(g.durations,.9),slaRate:rates(g.late,g.cases),completeRate:rates(g.complete,g.cases),costPerCase:g.cost/g.cases}); | |
| const nodes=[...all.values()].map(n=>({name:n.name,hits:n.hits,cases:n.cases.size,avgPosition:n.indexSum/n.indexCount})).sort((a,b)=>a.avgPosition-b.avgPosition||b.hits-a.hits); | |
| const links=[...edges.values()].map(e=>({from:e.from,to:e.to,count:e.hours.length,cases:e.cases.size,avg:average(e.hours),median:median(e.hours),p90:percentile(e.hours,.9),sameTime:e.sameTime})).sort((a,b)=>b.count-a.count||b.avg-a.avg); | |
| const variantRows=[...variants.values()].map(v=>({name:v.name,count:v.count,share:rates(v.count,count),median:median(v.durations),breaches:v.late,completion:rates(v.complete,v.count)})).sort((a,b)=>b.count-a.count); | |
| const slaByTeam=[...teams.values()].map(formatGroup).sort((a,b)=>b.cases-a.cases),slaByChannel=[...channels.values()].map(formatGroup).sort((a,b)=>b.cases-a.cases); | |
| return {options:{slaHours:sla,completed,required,edgeMinimum:threshold},cases:list,totalCases:count,totalEvents:events.length,median:median(durations),mean:average(durations),p90:percentile(durations,.9),late:breaches,lateRate:rates(breaches,count),completion:rates(done,count),completeCount:done,missing:missing,missingRate:rates(missing,count),reworked:loops,reworkRate:rates(loops,count),cost,medianCost:median(list.map(c=>c.cost)),nodes,links,variants:variantRows,teams:slaByTeam,channels:slaByChannel,days:[...daily.values()].sort((a,b)=>a.date.localeCompare(b.date)).map(d=>({...d,lateRate:rates(d.late,d.cases),avg:d.hours/d.cases})),ambiguous,distinctActivities:nodes.length};} | |
| function compareHalves(events,opts){const cases=groupCases(events);if(cases.length<4)return null;const mid=Math.ceil(cases.length/2),a=cases.slice(0,mid),b=cases.slice(mid);const flatten=x=>x.flatMap(c=>c.events);const A=analyze(flatten(a),opts),B=analyze(flatten(b),opts);return {first:{cases:A.totalCases,median:A.median,lateRate:A.lateRate,completion:A.completion},second:{cases:B.totalCases,median:B.median,lateRate:B.lateRate,completion:B.completion},delta:{median:B.median-A.median,lateRate:B.lateRate-A.lateRate,completion:B.completion-A.completion},method:'Casos ordenados por fecha de inicio, divididos en dos mitades de igual tamaño; comparación descriptiva, no causal.'};} | |
| function generateScenario(kind='fulfillment'){const presets={fulfillment:{name:'Order fulfillment',seed:31,teams:['North Ops','Central Ops','South Ops'],channels:['Web','Retail','Partner'],path:['Received','Validated','Picked','Packed','Quality check','Delivered'],complete:'Delivered',required:'Quality check',sla:72,n:170},support:{name:'Customer support',seed:57,teams:['Tier 1','Tier 2','Escalations'],channels:['Email','Chat','Phone'],path:['Opened','Triaged','Assigned','Investigated','Reviewed','Resolved'],complete:'Resolved',required:'Triaged',sla:72,n:190},permits:{name:'Permit review',seed:109,teams:['North Office','Central Office','Regional Office'],channels:['Online','In person','Agency'],path:['Submitted','Screened','Assigned','Evaluated','Approved'],complete:'Approved',required:'Screened',sla:96,n:150}};const config=presets[kind];if(!config)throw Error('Escenario desconocido.');let z=config.seed;const rnd=()=>{z=(1664525*z+1013904223)>>>0;return z/4294967296};const base=Date.parse('2026-04-01T08:00:00Z'),events=[]; | |
| for(let k=0;k<config.n;k++){const caseId=(kind==='fulfillment'?'ORD':kind==='support'?'TCK':'REQ')+'-'+String(k+1).padStart(4,'0'),team=config.teams[Math.min(config.teams.length-1,Math.floor(rnd()*config.teams.length))],channel=config.channels[Math.floor(rnd()*config.channels.length)];const trend=k>config.n*.58?1.35:1,slow=team===config.teams[2]?1.65:1;let t=base+(k*4+rnd()*2)*3600000;let path=config.path.slice();if(rnd()<.12)path.splice(2,0,path[1]);if(rnd()<.09)path=path.filter(a=>a!==config.required);if(rnd()<.13)path.pop();for(let i=0;i<path.length;i++){const act=path[i];const lag=(2+rnd()*8)*(i===2?2.9:1)*(slow)*(trend)*(rnd()<.07?3.2:1);if(i>0)t+=lag*3600000;events.push({case_id:caseId,activity:act,timestamp:new Date(Math.round(t/1000)*1000).toISOString(),team,channel,cost:Math.round((10+rnd()*55)*100)/100});}} | |
| return {name:config.name,kind,events:validateEvents(events),config:{slaHours:config.sla,completed:config.complete,required:config.required},isSynthetic:true};} | |
| return {VERSION,MAX_ROWS,csvParse,parseEventsCSV,validateEvents,safeCSV,groupCases,median,average,percentile,analyze,compareHalves,generateScenario}; | |
| }); | |
| </script><script>(function(){'use strict';const C=window.PSCore;const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];const state={data:C.generateScenario('fulfillment'),view:'overview',selectedNode:null,focusCase:null,sort:'duration',team:'All',edgeMinimum:1,search:''};let report=null; | |
| const enc=String;const escape=s=>String(s??'').replace(/[&<>"']/g,x=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[x]));const fmt=(v,d=1)=>v===null||!Number.isFinite(v)?'n/a':new Intl.NumberFormat('en-US',{maximumFractionDigits:d,minimumFractionDigits:d}).format(v);const pc=v=>fmt(v,1)+'%';const hrs=v=>v==null?'n/a':fmt(v,1)+' h';const el=(id,content)=>{const node=document.getElementById(id);if(node)node.innerHTML=content;};const badge=(s,kind='')=>`<span class="tag ${kind}">${escape(s)}</span>`; | |
| function getOpts(){return {...state.data.config,edgeMinimum:state.edgeMinimum};} | |
| function refresh(){report=C.analyze(state.data.events,getOpts());$('#projectLabel').textContent=state.data.isSynthetic?'SYNTHETIC DATASET':'IMPORTED DATASET';$('#scenarioName').textContent=state.data.name;$('#slaVal').textContent=fmt(report.options.slaHours,0)+' h';$('#caseTotal').textContent=fmt(report.totalCases,0)+' cases';$('#datasetHint').textContent=`${fmt(report.totalEvents,0)} events · ${fmt(report.totalCases,0)} cases · ${report.distinctActivities} activities`; | |
| renderOptions();renderOverview();renderMapView();renderVariants();renderBottlenecks();renderSLA();renderData();applyView();} | |
| function renderOptions(){const nodes=report.nodes.map(n=>n.name);const opts=(arr,value)=>arr.map(a=>`<option value="${escape(a)}" ${value===a?'selected':''}>${escape(a)}</option>`).join('');el('requiredWrap',`<select id="requiredSelect" aria-label="Required activity">${opts(['None',...nodes],report.options.required)}</select>`);el('completeWrap',`<select id="completeSelect" aria-label="Final completed activity">${opts(nodes,report.options.completed)}</select>`);$('#slaInput').value=report.options.slaHours;$('#slaInput2').value=report.options.slaHours; | |
| const teams=['All',...report.teams.map(x=>x.name)];el('teamWrap',`<select id="teamSelect" aria-label="Team filter">${opts(teams,state.team)}</select>`);if(!teams.includes(state.team))state.team='All';} | |
| function statsBlock(){const r=report;return `<div class="stat"><div class="name">Cases processed</div><div class="value">${fmt(r.totalCases,0)}</div><div class="hint">${fmt(r.totalEvents,0)} events across ${r.distinctActivities} activities</div></div><div class="stat blue"><div class="name">Median lead time</div><div class="value">${fmt(r.median,1)} <span style="font-size:13px;color:#98aeca">h</span></div><div class="hint">P90 ${hrs(r.p90)} · calendar hours</div></div><div class="stat orange"><div class="name">SLA breach rate</div><div class="value">${pc(r.lateRate)}</div><div class="hint">${fmt(r.late,0)} cases beyond ${fmt(r.options.slaHours,0)} h</div></div><div class="stat yellow"><div class="name">Completion rate</div><div class="value">${pc(r.completion)}</div><div class="hint">${fmt(r.completeCount,0)} cases end with ${escape(r.options.completed)}</div></div>`;} | |
| function showDurationHistogram(){const vals=report.cases.map(c=>c.duration),min=0,max=Math.max(1,...vals);const n=18,bins=Array(n).fill(0);for(const v of vals){bins[Math.min(n-1,Math.floor(v/max*n))]++;}const peak=Math.max(1,...bins);return `<div class="mini-bars">${bins.map((x,i)=>`<div title="${fmt(i*max/n,1)}–${fmt((i+1)*max/n,1)} h: ${x} cases" style="height:${Math.max(3,x/peak*100)}%"></div>`).join('')}</div><div class="split small" style="margin-top:6px"><span>0 h</span><span>Median ${hrs(report.median)}</span><span>${fmt(max,0)} h</span></div>`;} | |
| function renderOverview(){el('stats',statsBlock());const r=report;const worst=[...r.links].sort((a,b)=>b.avg-a.avg).slice(0,5);el('slowTransitions',worst.map((x,i)=>`<div class="metric-row"><div class="topline"><span>${escape(x.from)} → ${escape(x.to)}</span><b>${hrs(x.avg)}</b></div><div class="track ${i===0?'warn':''}"><span style="width:${Math.max(2,x.avg/Math.max(...worst.map(e=>e.avg))*100)}%"></span></div></div>`).join(''));el('durationHistogram',showDurationHistogram());const c=r.cases;const first=r.variants[0];el('overviewInsights',`<div class="annotation"><strong>Most frequent route:</strong> ${escape(first.name)}<div class="small">${first.count} of ${r.totalCases} cases (${pc(first.share)})</div></div><div class="annotation ${r.missingRate>8?'warn':''}"><strong>Required-step audit:</strong> ${r.missing} of ${r.totalCases} cases skip <b>${escape(r.options.required)}</b>. This is a rule-based check, not full process conformance.</div><div class="annotation">${r.ambiguous?`<strong>Ordering warning:</strong> ${r.ambiguous} transitions have identical timestamps; their ordering depends on source-row order.`:'<strong>Timestamp quality:</strong> No tied adjacent event timestamps detected.'}</div>`); | |
| const half=C.compareHalves(state.data.events,getOpts());el('periods',half?`<div class="grid-half"><div><div class="small">EARLY CASES</div><h2>${hrs(half.first.median)}</h2><p class="small">${pc(half.first.lateRate)} SLA breaches</p></div><div><div class="small">LATER CASES</div><h2>${hrs(half.second.median)}</h2><p class="small">${pc(half.second.lateRate)} SLA breaches</p></div></div><div class="annotation">Median lead-time change: <b class="${half.delta.median>0?'negative':'positive'}">${half.delta.median>=0?'+':''}${hrs(half.delta.median)}</b>. Cases are split chronologically by start time; this is descriptive, not causal.</div>`:'<p class="small">At least 4 cases required for period comparison.</p>');drawProcessMap('processMap',r);} | |
| function drawProcessMap(id,r){const nodes=r.nodes.slice(0,16);const max=Math.max(1,...nodes.map(n=>n.hits)),W=950,H=Math.max(300,160+Math.ceil(nodes.length/4)*95);const spots={};nodes.forEach((n,i)=>{spots[n.name]={x:110+(i%4)*238,y:80+Math.floor(i/4)*100};});const defs='<defs><marker id="pArrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0 0 L6 3 L0 6" fill="none" stroke="#83a5c5" stroke-width="1.2"/></marker></defs>'; | |
| let edges='';for(const e of r.links.filter(e=>e.count>=r.options.edgeMinimum)){const a=spots[e.from],b=spots[e.to];if(!a||!b)continue;const active=state.selectedNode&&(e.from===state.selectedNode||e.to===state.selectedNode);const width=Math.min(6,1+e.count/Math.max(1,...r.links.map(l=>l.count))*6);const x1=a.x+65,y1=a.y,x2=b.x-65,y2=b.y;if(a.x<b.x){const cx=(x1+x2)/2;edges+=`<path class="edge ${active?'active':''}" stroke-width="${width.toFixed(1)}" marker-end="url(#pArrow)" d="M ${x1} ${y1} C ${cx} ${y1},${cx} ${y2},${x2} ${y2}"><title>${escape(e.from+' → '+e.to)} | ${e.count} transitions | average ${fmt(e.avg)} h</title></path>`;}else{const startY=a.y+27,endY=b.y+27,offset=36+Math.abs(a.x-b.x)/12;edges+=`<path class="edge ${active?'active':''}" stroke-width="${width.toFixed(1)}" stroke-dasharray="5 4" marker-end="url(#pArrow)" d="M ${a.x} ${startY} C ${a.x} ${startY+offset},${b.x} ${endY+offset},${b.x} ${endY}"><title>${escape(e.from+' → '+e.to)} | ${e.count} transitions | average ${fmt(e.avg)} h</title></path>`;}} | |
| const rects=nodes.map(n=>{const p=spots[n.name],a=state.selectedNode===n.name;const width=Math.max(90,Math.min(126,65+n.name.length*5));return `<g class="node ${a?'active':''}" data-node="${escape(n.name)}" role="button" tabindex="0" aria-label="Inspect ${escape(n.name)}"><rect x="${p.x-width/2}" y="${p.y-27}" width="${width}" height="54"></rect><text x="${p.x}" y="${p.y-3}" text-anchor="middle">${escape(n.name.length>18?n.name.slice(0,17)+'…':n.name)}</text><text x="${p.x}" y="${p.y+14}" text-anchor="middle" style="font-weight:500;fill:#95aec4;font-size:10px">${n.cases} cases · ${n.hits} events</text></g>`;}).join('');el(id,`<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Directly follows process map">${defs}${edges}${rects}</svg>`);for(const g of $$('#'+id+' [data-node]')){const cb=()=>{state.selectedNode=g.getAttribute('data-node');drawProcessMap(id,report);renderNodeInspector();};g.addEventListener('click',cb);g.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();cb();}});}renderNodeInspector();} | |
| function renderNodeInspector(){const n=report.nodes.find(n=>n.name===state.selectedNode)||report.nodes[0];if(!n)return;const content=`<strong>${escape(n.name)}</strong><div class="flex" style="margin-top:6px">${badge(n.cases+' cases','good')}${badge(n.hits+' events')}</div><p class="small" style="margin-top:8px">${escape(String(n.name))} appears at average position ${fmt(n.avgPosition+1,1)} in a case sequence.</p>`;el('nodeInspector',content);el('nodeInspectorPlaceholder',content);} | |
| function renderMapView(){drawProcessMap('mapDetail',report);const links=report.links.slice(0,14);el('edgeTable',`<table><thead><tr><th>Transition</th><th>Events</th><th>Avg wait</th><th>P90 wait</th></tr></thead><tbody>${links.map(e=>`<tr><td>${escape(e.from)} → ${escape(e.to)}</td><td>${e.count}</td><td>${hrs(e.avg)}</td><td>${hrs(e.p90)}</td></tr>`).join('')}</tbody></table>`);el('mapNote',`Showing ${report.links.filter(e=>e.count>=state.edgeMinimum).length} of ${report.links.length} transitions. Edge thickness reflects frequency; dashed curves indicate backward / cyclic links. Times measure the interval to the next recorded event, not proven active work time.`);$('#minEdge').value=state.edgeMinimum;$('#minEdgeLabel').textContent=state.edgeMinimum;} | |
| function renderVariants(){const vals=report.variants.slice(0,12),max=vals[0]?.count||1;el('variantRows',`<table><thead><tr><th>Rank</th><th>Observed route</th><th>Cases</th><th>Share</th><th>Median time</th><th>SLA failures</th></tr></thead><tbody>${vals.map((v,i)=>`<tr><td>${i+1}</td><td class="variant-name">${escape(v.name)}</td><td>${v.count}</td><td><div style="min-width:100px"><div class="track"><span style="width:${v.count/max*100}%"></span></div><span class="small">${pc(v.share)}</span></div></td><td>${hrs(v.median)}</td><td>${v.breaches}</td></tr>`).join('')}</tbody></table>`);const top5=report.variants.slice(0,5).reduce((s,x)=>s+x.count,0);el('variantStats',`<div class="grid-half"><div><div class="small">DISTINCT VARIANTS</div><h1>${report.variants.length}</h1></div><div><div class="small">TOP 5 SHARE</div><h1>${pc(top5/report.totalCases*100)}</h1></div></div><div class="annotation">Variants are exact activity sequences. Repeated activities create a different variant. No clustering or learned similarity is applied.</div>`);} | |
| function renderBottlenecks(){el('bottleneckStats',`<div class="grid-half"><div><div class="small">REWORK SIGNAL</div><h1>${pc(report.reworkRate)}</h1><p class="small">Consecutive repeat activity</p></div><div><div class="small">P90 LEAD TIME</div><h1>${hrs(report.p90)}</h1><p class="small">90th percentile across cases</p></div></div>`);const list=[...report.links].sort((a,b)=>b.avg-a.avg).slice(0,15);el('bottleneckTable',`<table><thead><tr><th>Transition</th><th>Frequency</th><th>Mean wait</th><th>Median wait</th><th>P90 wait</th></tr></thead><tbody>${list.map(t=>`<tr><td>${escape(t.from)} → ${escape(t.to)}</td><td>${t.count}</td><td>${hrs(t.avg)}</td><td>${hrs(t.median)}</td><td>${hrs(t.p90)}</td></tr>`).join('')}</tbody></table>`);el('leadDist',showDurationHistogram());} | |
| function renderSLA(){const teams=report.teams,channels=report.channels;const tab=(groups)=>`<table><thead><tr><th>Segment</th><th>Cases</th><th>Late %</th><th>Median</th><th>Complete %</th></tr></thead><tbody>${groups.map(g=>`<tr><td>${escape(g.name)}</td><td>${g.cases}</td><td class="${g.slaRate>35?'negative':'positive'}">${pc(g.slaRate)}</td><td>${hrs(g.median)}</td><td>${pc(g.completeRate)}</td></tr>`).join('')}</tbody></table>`;el('teamTable',tab(teams));el('channelTable',tab(channels));const ds=report.days,raw=ds.map(d=>d.lateRate),max=Math.max(1,...raw);el('slaTrend',`<div class="mini-bars">${raw.map((x,i)=>`<div title="${escape(ds[i].date)}: ${pc(x)} (${ds[i].cases} cases)" style="height:${Math.max(2,x/max*100)}%"></div>`).join('')}</div><div class="split small" style="margin-top:7px"><span>${escape(ds[0]?.date||'')}</span><span>Daily case-start cohorts, SLA breach %</span><span>${escape(ds[ds.length-1]?.date||'')}</span></div>`);el('slaWarnings',`<div class="annotation"><strong>SLA rule:</strong> A case is late only when its final recorded event is <b>more than ${fmt(report.options.slaHours,0)} calendar hours</b> after its first event, including unfinished cases.</div><div class="annotation">Teams and channels are taken from each case's <b>first recorded event</b>; they are attribution labels, not claims about responsibility for the full process.</div>`);} | |
| function getCaseRows(){let cases=report.cases.filter(c=>state.team==='All'||c.team===state.team).filter(c=>(c.id+' '+c.variant).toLowerCase().includes(state.search.toLowerCase()));switch(state.sort){case 'duration':cases.sort((a,b)=>b.duration-a.duration);break;case 'oldest':cases.sort((a,b)=>a.start-b.start);break;case 'cost':cases.sort((a,b)=>b.cost-a.cost);break;}return cases;} | |
| function renderCases(){const filtered=getCaseRows();el('caseRows',`<table><thead><tr><th>Case ID</th><th>Team</th><th>Lead time</th><th>End state</th><th>SLA</th></tr></thead><tbody>${filtered.slice(0,90).map(c=>`<tr class="row-click" tabindex="0" data-case="${escape(c.id)}"><td><b>${escape(c.id)}</b></td><td>${escape(c.team)}</td><td>${hrs(c.duration)}</td><td>${escape(c.last)}</td><td>${c.breached?badge('Late','bad'):badge('On time','good')}</td></tr>`).join('')}</tbody></table>`);el('caseShown',`Showing ${Math.min(filtered.length,90)} of ${filtered.length} matching cases`);for(const r of $$('#caseRows [data-case]')){const cb=()=>{state.focusCase=r.getAttribute('data-case');renderCaseDetail();};r.addEventListener('click',cb);r.addEventListener('keydown',e=>{if(e.key==='Enter'){cb();}});}if(!state.focusCase||!report.cases.find(c=>c.id===state.focusCase))state.focusCase=filtered[0]?.id;renderCaseDetail();} | |
| function renderCaseDetail(){const c=report.cases.find(c=>c.id===state.focusCase);if(!c)return el('caseDetail','<div class="empty">Select a case.</div>');el('caseDetail',`<div class="split"><h2>${escape(c.id)}</h2>${c.breached?badge('SLA breached','bad'):badge('Within SLA','good')}</div><div class="small">${escape(c.team)} · ${escape(c.channel)} · ${c.eventsCount} events</div><div class="annotation"><strong>Elapsed:</strong> ${hrs(c.duration)} · <strong>Recorded cost:</strong> ${fmt(c.cost,2)} (source units)</div>${c.events.map((e,i)=>`<div style="padding:10px 0;border-bottom:1px solid #293953"><div class="flex"><span class="circle-mint"></span><b>${escape(e.activity)}</b></div><div class="small" style="padding-left:15px">${escape(e.timestamp.replace('T',' ').replace('.000Z',' UTC'))}${i?` · +${hrs((e.t-c.events[i-1].t)/3600000)}`:''}</div></div>`).join('')}`);} | |
| function renderData(){el('dataStats',`<div class="grid-half"><div><div class="small">ACTIVE DATASET</div><h2>${escape(state.data.name)}</h2><p class="small">${fmt(report.totalEvents,0)} events / ${report.totalCases} cases</p></div><div><div class="small">SOURCE TYPE</div><h2>${state.data.isSynthetic?'Synthetic demonstration':'User-provided CSV'}</h2><p class="small">Processed locally, in your browser</p></div></div>`);const warning=report.ambiguous?`${report.ambiguous} adjacent transitions share the same timestamp; recorded row order is used to break ties.`:'No ambiguous adjacent timestamps detected.';el('qualityNote',`<div class="annotation">${warning} Activity counts reflect recorded events, not unique work steps. Incomplete or missing real-world events can bias all metrics.</div>`);} | |
| function applyView(){for(const v of $$('.view'))v.classList.toggle('active',v.id==='view-'+state.view);for(const n of $$('.nav')){const a=n.dataset.view===state.view;n.classList.toggle('active',a);if(a)n.setAttribute('aria-current','page');else n.removeAttribute('aria-current');}const titles={overview:['Process intelligence','From event logs to operational clarity.'],map:['Workflow map','Explore actual activity-to-activity transitions.'],variants:['Journey variants','Understand the paths your cases really take.'],bottlenecks:['Bottleneck explorer','Distinguish elapsed waiting time from workload.'],sla:['SLA & equity audit','Compare service performance across cohorts and segments.'],data:['Data & methodology','Import a log, configure an audit and export reproducible results.']};$('#pageTitle').textContent=titles[state.view][0];$('#pageSubtitle').textContent=titles[state.view][1];if(state.view==='data')renderCases();$('#sidebar').classList.remove('open');$('#overlay').classList.remove('show');} | |
| function toast(msg,isError=false){const t=$('#toast');t.textContent=msg;t.className='toast show'+(isError?' error':'');clearTimeout(window.toastTimer);window.toastTimer=setTimeout(()=>t.classList.remove('show'),4200);} | |
| function download(name,content,mime='text/plain'){const blob=new Blob([content],{type:mime+';charset=utf-8'}),a=document.createElement('a'),url=URL.createObjectURL(blob);a.href=url;a.download=name;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),3000);toast(name+' downloaded');} | |
| function csvEvents(){const head=['case_id','activity','timestamp','team','channel','cost'];const rows=state.data.events.map(e=>head.map(h=>e[h]));return C.safeCSV(rows,head);} | |
| function exportAction(type){if(type==='sample')return download('processscope-events-template.csv',C.safeCSV([['EXAMPLE-001','Received','2026-04-01T08:00:00Z','Team A','Web','12'],['EXAMPLE-001','Validated','2026-04-01T10:00:00Z','Team A','Web','8'],['EXAMPLE-001','Completed','2026-04-01T11:00:00Z','Team A','Web','5']],['case_id','activity','timestamp','team','channel','cost']),'text/csv');if(type==='events')return download('processscope-events.csv',csvEvents(),'text/csv');if(type==='cases')return download('processscope-case-audit.csv',C.safeCSV(report.cases.map(c=>[c.id,c.team,c.channel,c.duration.toFixed(5),c.cost.toFixed(2),c.eventsCount,c.last,c.breached,c.completed,c.missing,c.variant]),['case_id','team','channel','lead_time_hours','cost','events','last_activity','sla_breached','completed','required_missing','variant']),'text/csv');if(type==='edges')return download('processscope-transition-audit.csv',C.safeCSV(report.links.map(e=>[e.from,e.to,e.count,e.cases,e.avg.toFixed(5),e.median.toFixed(5),e.p90.toFixed(5)]),['source_activity','target_activity','transitions','cases','mean_hours','median_hours','p90_hours']),'text/csv');if(type==='json'){const s={tool:'ProcessScope',version:C.VERSION,generatedAt:new Date().toISOString(),dataset:state.data.name,synthetic:!!state.data.isSynthetic,method:'Directly-follows process discovery; elapsed calendar hours',settings:report.options,summary:{cases:report.totalCases,events:report.totalEvents,medianHours:report.median,p90Hours:report.p90,slaBreaches:report.late,slaRate:report.lateRate,completed:report.completeCount,completionRate:report.completion,missingRequiredStep:report.missing,reworkedCases:report.reworked,ambiguousTimestamps:report.ambiguous},transitions:report.links,variants:report.variants,groups:{teams:report.teams,channels:report.channels},timeComparison:C.compareHalves(state.data.events,getOpts())};return download('processscope-reproducible-audit.json',JSON.stringify(s,null,2),'application/json');}if(type==='svg'){const svg=$('#processMap svg');return download('processscope-directly-follows-map.svg',new XMLSerializer().serializeToString(svg),'image/svg+xml');}if(type==='md'){const m=`# ProcessScope audit\n\nGenerated: ${new Date().toISOString()}\n\nDataset: ${state.data.name} (${state.data.isSynthetic?'synthetic':'uploaded'})\n\n- Cases: ${report.totalCases}\n- Events: ${report.totalEvents}\n- Median lead time: ${fmt(report.median,2)} hours\n- SLA limit: ${report.options.slaHours} calendar hours\n- SLA breach rate: ${pc(report.lateRate)}\n- Completion rate: ${pc(report.completion)}\n- Missing required step: ${report.missing}\n- Rework signal (adjacent repetition): ${report.reworked}\n\n## Method caveats\nThis is an exploratory log analysis. Waiting intervals are elapsed time, not certified effort or labor utilization. Required-step check is not full conformance. Demo data are synthetic. Period comparisons are descriptive, not causal. Uploaded data are analyzed locally in the browser.\n`;return download('processscope-audit-summary.md',m,'text/markdown');}} | |
| function handleFile(file){if(!file)return;if(file.size>3000000)return toast('File is larger than 3 MB.',true);const reader=new FileReader();reader.onload=()=>{try{const events=C.parseEventsCSV(reader.result);const cases=C.groupCases(events);if(cases.length<3)throw Error('Se necesitan por lo menos 3 casos para analizar.');const nodes=[...new Set(events.map(e=>e.activity))];const updated={name:file.name,kind:'custom',events,config:{slaHours:72,completed:nodes[nodes.length-1],required:nodes[0]},isSynthetic:false};state.data=updated;state.team='All';state.focusCase=null;state.selectedNode=null;state.search='';refresh();toast('CSV validated: '+events.length+' events imported.');}catch(e){toast('Import rejected: '+e.message,true);}};reader.onerror=()=>toast('Could not read CSV.',true);reader.readAsText(file);} | |
| function setup(){for(const n of $$('.nav'))n.addEventListener('click',()=>{state.view=n.dataset.view;applyView();});$('#toggleMenu').onclick=()=>{$('#sidebar').classList.toggle('open');$('#overlay').classList.toggle('show');};$('#overlay').onclick=()=>{$('#sidebar').classList.remove('open');$('#overlay').classList.remove('show');};$('#scenario').onchange=e=>{state.data=C.generateScenario(e.target.value);state.team='All';state.search='';state.selectedNode=null;state.focusCase=null;refresh();toast('Synthetic scenario loaded');};$$('[data-view-link]').forEach(x=>x.onclick=()=>{state.view=x.dataset.viewLink;applyView()});document.body.addEventListener('click',e=>{const btn=e.target.closest('[data-export]');if(btn)exportAction(btn.dataset.export);});document.body.addEventListener('change',e=>{if(e.target.id==='requiredSelect'){state.data.config.required=e.target.value;refresh();}if(e.target.id==='completeSelect'){state.data.config.completed=e.target.value;refresh();}if(e.target.id==='teamSelect'){state.team=e.target.value;renderCases();}});$$('#slaInput, #slaInput2').forEach(input=>input.addEventListener('change',e=>{const value=+e.target.value;if(!Number.isFinite(value)||value<1||value>240){toast('SLA must be from 1 to 240 hours.',true);return;}state.data.config.slaHours=value;refresh();}));$('#minEdge').addEventListener('input',e=>{state.edgeMinimum=+e.target.value;renderMapView();drawProcessMap('processMap',report);});$('#caseSort').onchange=e=>{state.sort=e.target.value;renderCases();};$('#searchCases').oninput=e=>{state.search=e.target.value;renderCases();};const drop=$('#drop'),file=$('#csvInput');drop.addEventListener('click',()=>file.click());drop.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();file.click();}});drop.addEventListener('dragover',e=>{e.preventDefault();drop.classList.add('drag');});drop.addEventListener('dragleave',()=>drop.classList.remove('drag'));drop.addEventListener('drop',e=>{e.preventDefault();drop.classList.remove('drag');handleFile(e.dataTransfer.files[0]);});file.addEventListener('change',e=>{handleFile(e.target.files[0]);e.target.value='';});$('#reset').addEventListener('click',()=>{state.data=C.generateScenario('fulfillment');$('#scenario').value='fulfillment';state.team='All';state.search='';state.focusCase=null;state.selectedNode=null;refresh();toast('Demo restored.');});} | |
| setup();refresh();window.ProcessScopeDebug={get report(){return report},get dataset(){return state.data},refresh}; | |
| })(); | |
| </script></body></html> | |