Download index.html from Hugdrigo/queuepilot-service-capacity-studio: direct link, hf CLI and curl.
- Browser
- Download file 48.9 kB
-
https://huggingface.co/spaces/Hugdrigo/queuepilot-service-capacity-studio/resolve/main/index.html
- Command line
-
hf download hf://spaces/Hugdrigo/queuepilot-service-capacity-studio/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Hugdrigo/queuepilot-service-capacity-studio/resolve/main/index.html
48.9 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <meta name="theme-color" content="#09182b"><meta name="description" content="QueuePilot: a free, local-first capacity planning and service-queue simulation studio with explainable staffing scenarios."> | |
| <title>QueuePilot | Service Capacity Intelligence Studio</title> | |
| <style> | |
| :root{color-scheme:light;--bg:#f4f6fb;--card:#fff;--ink:#15243a;--muted:#6e7d94;--border:#e3e8f1;--panel:#f7f9fd;--brand:#6366f1;--teal:#009f96;--orange:#e29a43;--red:#e16178;--shadow:0 18px 50px rgba(21,36,58,.048);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px} | |
| body.dark{color-scheme:dark;--bg:#0b1424;--card:#13243b;--ink:#eff5fc;--muted:#9fb0c7;--border:#293d56;--panel:#172c47;--brand:#a09dff;--teal:#49d8c8;--orange:#ffc16c;--red:#ff8b9d;--shadow:none} | |
| *{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);line-height:1.53}button,input,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}a{color:inherit} :focus-visible{outline:3px solid var(--teal);outline-offset:3px} | |
| .shell{display:grid;grid-template-columns:245px minmax(0,1fr);min-height:100vh}.sidebar{background:#0d1c30;color:#a9bacb;padding:29px 16px;position:sticky;top:0;height:100vh;display:flex;flex-direction:column}.brand{display:flex;gap:11px;align-items:center;padding:3px 10px 28px;color:#fff}.brand-mark{width:37px;height:37px;border-radius:13px;display:grid;place-items:center;font-size:24px;background:linear-gradient(140deg,#8b7ffc,#45d9c9);color:#101c37;font-weight:800;box-shadow:0 10px 24px #101b40}.brand strong{font-size:19px;letter-spacing:-.8px}.brand small{display:block;color:#9fb6cb;font-size:9px;letter-spacing:1.6px;font-weight:800}.nav-caption{padding:16px 13px 10px;font-size:10px;letter-spacing:2.2px;color:#60758b;font-weight:900}.nav button{width:100%;text-align:left;border:0;color:#a9bbd0;background:transparent;padding:12px 13px;border-radius:11px;margin-bottom:6px;display:flex;align-items:center;gap:11px;font-size:12px;font-weight:700}.nav button span:first-child{font-size:18px;width:22px;text-align:center}.nav button.active,.nav button:hover{background:#23344e;color:#fff}.nav button.active:before{content:"";position:absolute;left:0;width:3px;height:25px;background:#55d7c8;border-radius:5px}.sidebar-foot{margin-top:auto;background:#172c45;border:1px solid #2a405a;border-radius:16px;padding:16px}.sidebar-foot b{color:white}.sidebar-foot p{color:#aec1d3;font-size:11px;margin:7px 0 11px}.tagline{font-size:10px;color:#85edd9;letter-spacing:1px;font-weight:800}.content{min-width:0;padding:0 32px 50px}.topbar{height:83px;display:flex;align-items:center;justify-content:space-between;gap:15px;border-bottom:1px solid var(--border);margin-bottom:27px}.breadcrumbs{font-size:11px;letter-spacing:1.8px;text-transform:uppercase;color:var(--muted);font-weight:800}.breadcrumbs b{color:var(--ink)}.header-actions{display:flex;gap:9px;align-items:center;flex-wrap:wrap}.pill{font-size:10px;letter-spacing:.45px;border:1px solid var(--border);border-radius:99px;padding:7px 12px;background:var(--card);font-weight:800}.pill.live{color:var(--teal);background:color-mix(in srgb,var(--teal) 8%,var(--card))}.theme-btn{border-radius:11px;border:1px solid var(--border);background:var(--card);color:var(--ink);padding:8px 12px;font-weight:700}.page-title{display:flex;justify-content:space-between;align-items:flex-end;gap:15px;margin:0 0 20px}.eyebrow{font-size:10px;text-transform:uppercase;letter-spacing:2px;font-weight:900;color:var(--teal)}h1,h2,h3,p{margin-top:0}h1{font-size:28px;line-height:1.18;letter-spacing:-1.1px;margin:7px 0 7px}h2{font-size:16px;letter-spacing:-.35px;margin:0}h3{font-size:13px;margin:0 0 8px}.sub{font-size:12px;color:var(--muted);margin-bottom:0}.hero{background:radial-gradient(ellipse at 85% 20%,#4c4788 0,transparent 38%),linear-gradient(105deg,#15253e,#213c61 60%,#293b60);border-radius:19px;padding:27px 29px;color:white;position:relative;overflow:hidden;display:flex;justify-content:space-between;gap:22px;align-items:center;min-height:164px;margin-bottom:20px}.hero:after{content:"";position:absolute;right:14%;bottom:-90px;width:230px;height:230px;border-radius:50%;border:45px solid #ffffff09}.hero .eyebrow{color:#83e5d7}.hero h2{font-size:25px;letter-spacing:-1px;max-width:420px;margin:8px 0}.hero p{color:#b3c9e2;font-size:12px;max-width:540px;margin:0}.hero-side{position:relative;z-index:1;text-align:center;background:#ffffff0d;border:1px solid #ffffff22;border-radius:16px;padding:18px;min-width:160px;backdrop-filter:blur(4px)}.hero-side .big{font-size:34px;letter-spacing:-1px;font-weight:850;color:#83eadb;line-height:1.15}.hero-side small{display:block;text-transform:uppercase;letter-spacing:1.1px;color:#c3d6e4;font-size:10px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:15px;margin-bottom:19px}.card{background:var(--card);border:1px solid var(--border);border-radius:16px;box-shadow:var(--shadow);min-width:0}.metric{padding:17px 18px;min-height:117px}.metric-head{display:flex;justify-content:space-between;gap:6px;align-items:center;color:var(--muted);font-weight:750;font-size:11px}.metric-icon{border-radius:9px;background:var(--panel);font-size:17px;padding:5px 8px;color:var(--brand)}.metric-value{font-size:27px;letter-spacing:-1px;font-weight:850;line-height:1.25;margin:14px 0 3px}.metric-note{font-size:10px;color:var(--muted)}.metric-note.good{color:var(--teal)}.metric-note.bad{color:var(--red)}.dashboard-grid{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(285px,1fr);gap:17px;margin-bottom:17px}.section-pad{padding:21px}.card-head{display:flex;justify-content:space-between;align-items:center;gap:9px;margin-bottom:17px}.muted{color:var(--muted)}.tiny{font-size:10px}.small{font-size:11px}.legend{display:flex;gap:13px;flex-wrap:wrap;color:var(--muted);font-size:10px}.swatch{display:inline-block;height:8px;width:8px;border-radius:3px;margin-right:5px}.chip{font-size:10px;border:1px solid var(--border);padding:5px 9px;border-radius:99px;font-weight:800;white-space:nowrap}.chip.green{color:var(--teal);background:color-mix(in srgb,var(--teal) 9%,var(--card))}.chip.red{color:var(--red);background:color-mix(in srgb,var(--red) 8%,var(--card))}.chart{width:100%;height:auto;display:block;overflow:visible}.grid-lines line{stroke:var(--border);stroke-dasharray:4 4}.chart text{font-family:inherit;fill:var(--muted);font-size:10px}.chart .bar{fill:#8f8afb}.chart .capacity{stroke:#36bdae;fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}.chart .dot{fill:#36bdae}.insight{display:flex;gap:12px;background:var(--panel);padding:14px;border-radius:12px;align-items:flex-start;margin-top:14px}.insight .bubble{display:grid;place-items:center;width:30px;height:30px;border-radius:10px;flex:none;background:#dff8f2;color:#068a81;font-size:18px}.insight p{font-size:11px;margin:2px 0;color:var(--muted)}.form-stack{display:grid;gap:11px}.field{display:grid;gap:5px}.field label{font-size:10px;letter-spacing:.2px;font-weight:800;color:var(--muted)}.field input,.field select{width:100%;background:var(--card);color:var(--ink);border:1px solid var(--border);border-radius:10px;padding:10px 11px;min-width:0}.field select{appearance:auto}.split{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.btn{border:0;background:var(--brand);color:white;border-radius:10px;padding:10px 14px;font-weight:800;font-size:11px;min-height:37px}.btn:hover{filter:brightness(1.08)}.btn.secondary{color:var(--ink);background:var(--panel);border:1px solid var(--border)}.btn.green{background:#008c80}.btn.danger{background:color-mix(in srgb,var(--red) 11%,var(--card));color:var(--red);border:1px solid color-mix(in srgb,var(--red) 25%,var(--border))}.btn-row{display:flex;gap:8px;flex-wrap:wrap}.section-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:13px;margin:26px 0 15px}.section-heading p{font-size:11px;color:var(--muted);margin:3px 0 0}.table-scroll{overflow-x:auto}.data-table{border-collapse:collapse;width:100%;font-size:11px;white-space:nowrap}.data-table th{text-align:left;letter-spacing:.6px;text-transform:uppercase;color:var(--muted);font-size:9px;padding:11px 10px;background:var(--panel)}.data-table td{padding:11px 10px;border-top:1px solid var(--border)}.data-table td:first-child{font-weight:800}.data-table tr.current{background:color-mix(in srgb,var(--teal) 7%,var(--card))}.data-table .number{text-align:right;font-variant-numeric:tabular-nums}.data-table th.number{text-align:right}.mini-actions{display:flex;gap:5px}.mini-actions button{border:1px solid var(--border);background:var(--panel);color:var(--ink);border-radius:8px;padding:6px 9px;font-size:10px;font-weight:750}.mini-actions button:hover{border-color:var(--brand)}.notice{padding:12px 15px;border:1px solid var(--border);background:var(--panel);border-radius:11px;font-size:11px;color:var(--muted)}.scenario-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:11px}.scenario{border:1px solid var(--border);background:var(--card);border-radius:13px;padding:14px 13px;text-align:left;color:var(--ink);transition:transform .15s,border-color .15s}.scenario:hover{transform:translateY(-2px)}.scenario.active{border:2px solid var(--brand);background:color-mix(in srgb,var(--brand) 7%,var(--card));padding:13px 12px}.scenario .s-icon{font-size:19px}.scenario b{display:block;margin:7px 0 3px}.scenario span:last-child{font-size:10px;color:var(--muted)}.stat-row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:14px 0}.stat-tile{background:var(--panel);padding:15px;border-radius:12px}.stat-tile strong{display:block;font-size:20px;margin:5px 0}.stat-tile span{font-size:10px;color:var(--muted)}.work-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.workcard{padding:18px}.workcard .title-row{display:flex;justify-content:space-between;gap:10px}.workcard .avatar{width:37px;height:37px;background:linear-gradient(140deg,#e7eaff,#d6faf3);border-radius:10px;display:grid;place-items:center;font-size:20px}.workcard h3{font-size:13px;margin:12px 0 4px}.workcard p{font-size:10px;color:var(--muted);margin:0 0 14px}.work-meta{display:grid;grid-template-columns:1fr 1fr;gap:9px;border-top:1px solid var(--border);padding:13px 0}.work-meta b{display:block;font-size:14px}.work-meta small{color:var(--muted);font-size:10px}.details{display:grid;gap:10px}.details article{background:var(--panel);border-radius:13px;padding:17px}.details article p{font-size:11px;color:var(--muted);margin:5px 0 0}.note-list{padding-left:18px;color:var(--muted);font-size:11px}.note-list li{margin-bottom:8px}.hidden{display:none!important}.edit-panel{max-width:800px;margin-bottom:17px}.toast{position:fixed;right:20px;bottom:20px;background:#0d2035;color:white;padding:12px 17px;border-radius:12px;box-shadow:0 14px 50px #101e3140;z-index:20;font-size:12px;max-width:min(340px,92vw);display:none}.toast.show{display:block}.footer{font-size:10px;color:var(--muted);margin-top:30px;padding:17px 0;border-top:1px solid var(--border);display:flex;justify-content:space-between;gap:10px}.empty{text-align:center;padding:35px;color:var(--muted)} | |
| @media(max-width:1150px){.shell{grid-template-columns:205px minmax(0,1fr)}.content{padding:0 20px 35px}.metrics{grid-template-columns:repeat(2,1fr)}.scenario-grid{grid-template-columns:repeat(2,1fr)}.work-grid{grid-template-columns:repeat(2,1fr)}} | |
| @media(max-width:750px){.shell{display:block}.sidebar{height:auto;position:static;padding:12px 14px}.brand{padding:0 0 10px}.brand strong{font-size:17px}.brand-mark{width:32px;height:32px}.nav-caption,.sidebar-foot{display:none}.nav{display:flex;overflow-x:auto;gap:7px}.nav button{flex:0 0 auto;width:auto;padding:8px 10px;margin:0;font-size:11px}.nav button.active:before{display:none}.content{padding:0 15px 35px}.topbar{height:auto;min-height:63px;margin-bottom:17px}.breadcrumbs{font-size:9px}.header-actions .pill{display:none}.hero{padding:23px;align-items:flex-start;min-height:140px}.hero-side{min-width:110px;padding:12px}.hero-side .big{font-size:26px}.hero h2{font-size:21px}.dashboard-grid{grid-template-columns:1fr}.work-grid{grid-template-columns:1fr 1fr}.page-title{align-items:flex-start}.section-pad{padding:17px}.footer{flex-direction:column}} | |
| @media(max-width:520px){h1{font-size:23px}.metrics{gap:9px}.metric{padding:13px;min-height:107px}.metric-value{font-size:23px}.metric-head{font-size:10px}.hero{display:block}.hero-side{margin-top:17px;display:flex;justify-content:space-between;align-items:center}.hero-side .big{font-size:25px}.scenario-grid{gap:8px}.scenario{padding:11px}.scenario.active{padding:10px}.work-grid{grid-template-columns:1fr}.split{grid-template-columns:1fr}.stat-row{grid-template-columns:1fr 1fr}.stat-tile:nth-child(3){grid-column:span 2}.topbar{gap:6px}.topbar .theme-btn{font-size:10px;padding:6px 8px}.page-title .btn{font-size:10px;padding:8px}} | |
| @media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;transition:none!important}} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="shell"> | |
| <aside class="sidebar" aria-label="Primary navigation"><div class="brand"><div class="brand-mark">Q</div><div><strong>QueuePilot</strong><small>CAPACITY INTELLIGENCE</small></div></div><div class="nav-caption">WORKSPACE</div><nav class="nav" id="sideNav"><button type="button" data-page="overview" class="active"><span>◫</span> Overview</button><button type="button" data-page="simulator"><span>◷</span> Capacity Lab</button><button type="button" data-page="workloads"><span>▦</span> Workloads</button><button type="button" data-page="method"><span>◎</span> Methodology</button></nav><div class="sidebar-foot"><span class="tagline">◉ LOCAL-FIRST STUDIO</span><p><b>Make capacity visible.</b><br>Explore service performance through reproducible queue simulations.</p><div class="pill" style="background:#223c54;color:#c3eee5;border-color:#345469">PROJECT 092 · HUGDRIGO</div></div></aside> | |
| <main class="content"><header class="topbar"><div class="breadcrumbs">HUGDRIGO / OPERATIONS / <b>QUEUEPILOT</b></div><div class="header-actions"><span class="pill live">● SIMULATION MODE</span><span class="pill">NO BACKEND</span><button class="theme-btn" id="themeBtn" type="button" aria-label="Toggle color theme">☾ Theme</button></div></header> | |
| <section data-view="overview" id="overview"> | |
| <div class="page-title"><div><div class="eyebrow">Executive dashboard · Project #92</div><h1>Service operations, in focus.</h1><p class="sub">Understand waiting time, workload pressure and staffing trade-offs.</p></div><button class="btn secondary" type="button" data-action="export-csv">↓ Export summary</button></div> | |
| <div class="hero"><div><div class="eyebrow">CAPACITY PLANNING / EXPLAINABLE SIMULATION</div><h2>Less waiting.<br>Smarter staffing.</h2><p>Model an eight-hour service shift using reproducible arrivals and service times. Compare staffing choices before making real operational decisions.</p></div><div class="hero-side"><small>Target service level</small><div class="big" id="heroTarget">80%</div><small id="heroSla">within 10 minutes</small></div></div> | |
| <div class="metrics" id="metricCards"></div> | |
| <div class="dashboard-grid"><div class="card section-pad"><div class="card-head"><div><h2>Demand vs. capacity</h2><div class="sub">Expected hourly arrivals and theoretical service capacity</div></div><span class="chip" id="chartWorkload">Workload</span></div><div id="demandChart"></div><div class="legend"><span><i class="swatch" style="background:#8f8afb"></i> Incoming demand</span><span><i class="swatch" style="background:#36bdae"></i> Nominal capacity</span></div><div class="insight"><div class="bubble">↗</div><div><b id="insightTitle">Loading simulation…</b><p id="insightBody"></p></div></div></div><div class="card section-pad"><div class="card-head"><div><h2>Scenario controls</h2><div class="sub">Tune a representative service queue</div></div><span class="chip green">LIVE</span></div><div class="form-stack"><div class="field"><label for="mainWorkload">Active workload</label><select id="mainWorkload"></select></div><div class="split"><div class="field"><label for="mainStaff">Agents on shift</label><input type="number" id="mainStaff" min="1" max="30" step="1"></div><div class="field"><label for="mainSla">SLA wait (minutes)</label><input type="number" id="mainSla" min="1" max="180" step="1"></div></div><div class="field"><label for="mainScenario">Demand scenario</label><select id="mainScenario"></select></div><button class="btn green" type="button" data-action="go-lab">Explore in Capacity Lab ↗</button></div><div class="notice" style="margin-top:14px">All estimates use synthetic example data, not operational forecasts. No customer or employee data is transmitted.</div></div></div> | |
| <div class="section-heading"><div><h2>Staffing decision snapshot</h2><p>Simulated alternatives at the selected SLA target</p></div><span class="chip">24 seeded runs / option</span></div><div class="card table-scroll"><table class="data-table"><thead><tr><th>Agents</th><th class="number">SLA met</th><th class="number">Avg. wait</th><th class="number">p90 wait</th><th class="number">Labor / shift</th><th>Status</th></tr></thead><tbody id="staffTable"></tbody></table></div> | |
| </section> | |
| <section data-view="simulator" class="hidden" id="simulator"><div class="page-title"><div><div class="eyebrow">Capacity intelligence / sandbox</div><h1>Capacity Lab</h1><p class="sub">Stress-test a queue with common-seed comparisons across staffing levels.</p></div><button class="btn secondary" type="button" data-action="export-csv">↓ Export analysis</button></div><div class="card section-pad" style="margin-bottom:16px"><div class="card-head"><div><h2>Choose an operating environment</h2><p class="sub">Scenario presets alter demand intensity and average handling time.</p></div><span class="chip">DEMO INPUTS</span></div><div class="scenario-grid" id="scenarioCards"></div></div><div class="dashboard-grid"><div class="card section-pad"><div class="card-head"><div><h2>Staffing performance curve</h2><div class="sub">SLA attainment across possible staffing choices</div></div><span class="chip" id="labWorkload">Active queue</span></div><div id="staffChart"></div><div class="legend"><span><i class="swatch" style="background:#8f8afb"></i> Simulated SLA attainment</span><span><i class="swatch" style="background:#36bdae"></i> Target SLA</span></div><div class="insight"><div class="bubble">✓</div><div><b id="recommendTitle">Calculating recommendation</b><p id="recommendBody"></p></div></div></div><div class="card section-pad"><div class="card-head"><div><h2>Model assumptions</h2><p class="sub">Adjust staffing economics and service constraints</p></div></div><div class="form-stack"><div class="split"><div class="field"><label for="labDemand">Demand multiplier (%)</label><input id="labDemand" type="number" min="20" max="300" step="5"></div><div class="field"><label for="labSpeed">Handling-time multiplier (%)</label><input id="labSpeed" type="number" min="40" max="250" step="5"></div></div><div class="split"><div class="field"><label for="labTarget">Target SLA (%)</label><input id="labTarget" type="number" min="10" max="100" step="5"></div><div class="field"><label for="labWage">Labor cost / hour ($)</label><input id="labWage" type="number" min="1" max="1000" step="1"></div></div><div class="field"><label for="labSeed">Simulation seed</label><input id="labSeed" type="number" min="1" max="2147483646" step="1"></div><div class="btn-row"><button class="btn" type="button" data-action="reset-controls">Restore defaults</button><button class="btn secondary" type="button" data-action="export-json">↓ Backup JSON</button></div><div class="notice">The recommendation identifies the lowest staffing level tested that meets both the SLA target and a maximum nominal utilization of 85%.</div></div></div></div><div class="metrics" id="labCards"></div><div class="section-heading"><div><h2>Detailed staffing alternatives</h2><p>Results represent stochastic simulations, not measured service performance.</p></div><span class="chip" id="runsInfo">24 runs per option</span></div><div class="card table-scroll"><table class="data-table"><thead><tr><th>Agents</th><th class="number">SLA met</th><th class="number">Mean wait</th><th class="number">p90 wait</th><th class="number">Utilization*</th><th class="number">Cost / shift</th><th>Decision</th></tr></thead><tbody id="labTable"></tbody></table></div></section> | |
| <section data-view="workloads" class="hidden" id="workloads"><div class="page-title"><div><div class="eyebrow">Scenario library / Local browser data</div><h1>Service workloads</h1><p class="sub">Build and compare fictional eight-hour service environments.</p></div><div class="btn-row"><button class="btn secondary" type="button" data-action="import-json">↑ Import JSON</button><button class="btn" type="button" data-action="add-workload">+ New workload</button></div></div><div class="card section-pad edit-panel hidden" id="workloadEditor"><div class="card-head"><h2 id="editorTitle">New workload</h2><button class="btn secondary" type="button" data-action="cancel-editor">Close</button></div><form id="editForm" class="form-stack"><div class="field"><label for="editName">Workload name</label><input id="editName" maxlength="55" required placeholder="e.g., Customer care queue"></div><div class="field"><label for="editDescription">Short context</label><input id="editDescription" maxlength="110" required placeholder="e.g., Subscription service requests"></div><div class="split"><div class="field"><label for="editAgents">Default agents (1–30)</label><input id="editAgents" type="number" min="1" max="30" required></div><div class="field"><label for="editHandle">Average handling minutes (0.5–180)</label><input id="editHandle" type="number" min="0.5" max="180" step="0.5" required></div><div class="field"><label for="editSla">SLA wait minutes (1–180)</label><input id="editSla" type="number" min="1" max="180" required></div><div class="field"><label for="editWage">Agent cost / hour ($1–$1,000)</label><input id="editWage" type="number" min="1" max="1000" required></div></div><div class="field"><label for="editHours">Hourly expected arrivals, 8 values from 0 to 150 (comma-separated)</label><input id="editHours" required placeholder="12, 16, 22, 24, 20, 18, 14, 10"></div><div class="btn-row"><button class="btn green" type="submit">Save workload</button><button class="btn secondary" type="button" data-action="cancel-editor">Cancel</button></div></form></div><div class="work-grid" id="workloadCards"></div><div class="section-heading"><div><h2>Data management</h2><p>Backup your current library or restore the original samples.</p></div></div><div class="card section-pad"><div class="btn-row"><button class="btn secondary" type="button" data-action="export-json">↓ Export JSON backup</button><button class="btn secondary" type="button" data-action="export-csv">↓ Export CSV</button><button class="btn danger" type="button" data-action="reset-all">Reset sample library</button></div><p class="small muted" style="margin:12px 0 0">JSON import validates its structure and limits. Imported text is treated as data, never interpreted as code. Backups only contain your local application settings and workloads.</p></div></section> | |
| <section data-view="method" class="hidden" id="method"><div class="page-title"><div><div class="eyebrow">Transparent mathematics</div><h1>How the model works</h1><p class="sub">A readable explanation of what is simulated, assumed and excluded.</p></div><span class="chip">VERSION 1.0.0</span></div><div class="hero"><div><div class="eyebrow">READ BEFORE USE</div><h2>A learning tool, not an SLA guarantee.</h2><p>QueuePilot is a transparent educational capacity planner. All starter profiles are synthetic and every recommendation depends on simplified model assumptions.</p></div><div class="hero-side"><small>Simulation horizon</small><div class="big">8h</div><small>24 runs / option</small></div></div><div class="details"><article><h3>01 · Arrival process</h3><p>Each hour has a configured mean arrival rate. Requests are generated by an exponential interarrival-time process within that hour, equivalent to a nonhomogeneous, piecewise-constant Poisson arrival model. Back-to-back hours retain queueing delays.</p></article><article><h3>02 · Service discipline</h3><p>A fixed number of parallel agents serve requests first-come, first-served. Service duration is exponentially distributed around the configured mean. Requests arriving before shift end remain eligible for service even when completion takes place after the shift.</p></article><article><h3>03 · SLA and reported metrics</h3><p>SLA attainment is the percentage of arriving requests whose wait to begin service is no longer than the configured threshold. Mean wait and p90 are calculated from individual wait times, and aggregated across seeded simulation runs. Shift labor cost = agents × 8 hours × hourly wage; it excludes overhead, breaks, overtime premiums and idle inefficiencies. Utilization* means expected workload minutes divided by scheduled agent-minutes; this is a nominal capacity-load ratio, not measured actual busy time or a bounded utilization percentage.</p></article><article><h3>04 · Comparison logic</h3><p>Capacity Lab tests 1 through 16 agents by default, or up to 30 when more are already assigned. Every staffing candidate uses the same seed sequence, enabling more comparable stochastic outcomes. The recommended count is the lowest candidate meeting the configured SLA attainment target and a nominal load ratio no greater than 85%. If none meets both constraints, the application reports that no feasible staffing level was found in the tested range.</p></article><article><h3>05 · Boundaries and exclusions</h3><p>No abandonment, scheduled breaks, heterogeneous agents, priority routing, skill constraints, interruptions, shift handoffs, or observed operational inputs are modeled. Because there is no abandonment, waiting times can extend beyond shift end under overload. Performance results are stochastic estimates, not predictions. Validate with real staffing and arrival data before making operational decisions.</p></article><article><h3>06 · Privacy and reuse</h3><p>All processing occurs locally in the browser. Data persists via localStorage where available; your browser may clear it. Use JSON backup to protect customized work. The app has no external fonts, APIs, analytics, trackers or external libraries. It is released under the MIT license.</p></article></div></section> | |
| <footer class="footer"><span>QueuePilot © 2026 · Hugdrigo project #92 · Open-source educational simulation</span><span>Illustrative inputs · Transparent algorithms · Local-first</span></footer></main></div> | |
| <div role="status" aria-live="polite" class="toast" id="toast"></div><input type="file" accept="application/json,.json" id="jsonFile" hidden> | |
| <script> | |
| ; | |
| (function(){ | |
| const VERSION=1, KEY='hugdrigo-queuepilot-v1'; | |
| const SCENARIOS=[{id:'baseline',name:'Baseline',icon:'◎',detail:'Original workload profile',d:1,h:1},{id:'surge',name:'Demand surge',icon:'↗',detail:'+45% incoming requests',d:1.45,h:1},{id:'faster',name:'Process improvement',icon:'⚡',detail:'20% faster handling',d:1,h:.8},{id:'pressure',name:'Compound pressure',icon:'◈',detail:'+30% demand and +15% handling',d:1.3,h:1.15}]; | |
| const EXAMPLES=[ | |
| {id:'customer-care',name:'Customer Care',description:'Subscription support and billing requests',icon:'🎧',agents:6,handle:8,sla:10,wage:22,hours:[23,29,36,42,39,35,27,20]}, | |
| {id:'it-service',name:'IT Service Desk',description:'Internal technical support tickets',icon:'💻',agents:4,handle:12,sla:15,wage:31,hours:[9,14,20,22,19,16,12,8]}, | |
| {id:'order-support',name:'Order Exceptions',description:'Shipment problems and returns assistance',icon:'📦',agents:5,handle:6,sla:8,wage:24,hours:[19,26,35,38,32,28,25,18]}, | |
| {id:'claims-intake',name:'Claims Intake',description:'Document processing and intake requests',icon:'📋',agents:4,handle:15,sla:20,wage:28,hours:[6,11,15,19,17,14,11,7]}, | |
| {id:'merchant-help',name:'Merchant Helpdesk',description:'Seller onboarding and operations requests',icon:'🏬',agents:5,handle:10,sla:12,wage:26,hours:[11,17,23,28,25,21,16,10]}, | |
| {id:'travel-desk',name:'Travel Assistance',description:'Changes, cancellations and rebooking',icon:'✈️',agents:6,handle:7,sla:8,wage:25,hours:[23,28,33,45,44,32,25,18]} | |
| ]; | |
| const fresh=()=>({version:VERSION,workloads:JSON.parse(JSON.stringify(EXAMPLES)),selected:'customer-care',scenario:'baseline',customDemand:100,customHandle:100,target:80,seed:917281,theme:'light',page:'overview'}); | |
| const $=id=>document.getElementById(id);const fmt=(n,d=0)=>Number(n).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d});const money=n=>'$'+fmt(n,0);const clamp=(v,min,max)=>Math.max(min,Math.min(max,v)); | |
| const escapeHTML=s=>String(s).replace(/[&<>"']/g,x=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[x])); | |
| const safeCSV=v=>{const s=String(v??'');let t=/^[\s]*[=+@\-\t\r]/.test(s)?"'"+s:s;return '"'+t.replace(/"/g,'""')+'"'}; | |
| const validWorkload=w=>{if(!w||typeof w!=='object'||Array.isArray(w))return false;const inRange=(v,a,b)=>typeof v==='number'&&Number.isFinite(v)&&v>=a&&v<=b;return typeof w.id==='string'&&/^[a-z0-9-]{1,90}$/.test(w.id)&&typeof w.name==='string'&&w.name.trim().length>=1&&w.name.length<=55&&typeof w.description==='string'&&w.description.length<=110&&typeof w.icon==='string'&&w.icon.length<=10&&Number.isInteger(w.agents)&&inRange(w.agents,1,30)&&inRange(w.handle,.5,180)&&inRange(w.sla,1,180)&&inRange(w.wage,1,1000)&&Array.isArray(w.hours)&&w.hours.length===8&&w.hours.every(x=>inRange(x,0,150))&&w.hours.some(x=>x>0)}; | |
| const validState=s=>s&&typeof s==='object'&&!Array.isArray(s)&&s.version===VERSION&&Array.isArray(s.workloads)&&s.workloads.length>0&&s.workloads.length<=60&&s.workloads.every(validWorkload)&&new Set(s.workloads.map(w=>w.id)).size===s.workloads.length&&s.workloads.some(w=>w.id===s.selected)&&SCENARIOS.some(x=>x.id===s.scenario)&&Number.isInteger(s.customDemand)&&s.customDemand>=20&&s.customDemand<=300&&Number.isInteger(s.customHandle)&&s.customHandle>=40&&s.customHandle<=250&&Number.isInteger(s.target)&&s.target>=10&&s.target<=100&&Number.isInteger(s.seed)&&s.seed>=1&&s.seed<=2147483646&&['dark','light'].includes(s.theme)&&['overview','simulator','workloads','method'].includes(s.page); | |
| let state=fresh(), editing=null, toastTimer=null, counter=0, cached=null; | |
| try{const raw=localStorage.getItem(KEY);if(raw){const found=JSON.parse(raw);if(validState(found))state=found;}}catch(_e){} | |
| const save=()=>{try{localStorage.setItem(KEY,JSON.stringify(state))}catch(_e){}}; | |
| const workload=()=>state.workloads.find(w=>w.id===state.selected)||state.workloads[0];const scenario=()=>SCENARIOS.find(s=>s.id===state.scenario)||SCENARIOS[0]; | |
| function rng(seed){let a=seed>>>0;return()=>{a=(a+0x6D2B79F5)>>>0;let t=a;t=Math.imul(t^(t>>>15),t|1);t^=t+Math.imul(t^(t>>>7),t|61);return ((t^(t>>>14))>>>0)/4294967296}}; | |
| function exponential(r,mean){return -mean*Math.log(Math.max(1e-12,1-r()))} | |
| function sampleArrivals(hours, d, r){const arr=[];for(let h=0;h<8;h++){const lambda=hours[h]*d;if(lambda<=0)continue;let t=0;while(true){t+=exponential(r,60/lambda);if(t>=60)break;arr.push(h*60+t);if(arr.length>30000)break;}if(arr.length>30000)break;}return arr} | |
| function quantile(sorted,p){if(!sorted.length)return 0;const x=(sorted.length-1)*p,i=Math.floor(x),frac=x-i;return sorted[i]*(1-frac)+(sorted[Math.min(sorted.length-1,i+1)]*frac)} | |
| function simulateDay(w,agents,scenarioDemand,scenarioHandle,seed){const r=rng(seed);const arrivals=sampleArrivals(w.hours,scenarioDemand,r);const free=Array(agents).fill(0);let within=0, sumWait=0, backlog=0, overtime=0;const waits=[];for(const at of arrivals){let idx=0;for(let i=1;i<agents;i++)if(free[i]<free[idx])idx=i;const start=Math.max(at,free[idx]),wait=start-at,service=exponential(r,w.handle*scenarioHandle);free[idx]=start+service;waits.push(wait);sumWait+=wait;if(wait<=w.sla+1e-9)within++;if(start>=480)backlog++}waits.sort((a,b)=>a-b);for(const t of free)overtime=Math.max(overtime,t-480);return {arrivals:arrivals.length,sla:arrivals.length?100*within/arrivals.length:100,wait:arrivals.length?sumWait/arrivals.length:0,p90:quantile(waits,.90),backlog,overtime:Math.max(0,overtime)};} | |
| function simMany(w,agents,d,h,seed,runs=24){let n=0,sla=0,wait=0,p90=0,backlog=0,overtime=0;for(let k=0;k<runs;k++){const v=simulateDay(w,agents,d,h,(seed+Math.imul(k,7919))>>>0);n+=v.arrivals;sla+=v.sla;wait+=v.wait;p90+=v.p90;backlog+=v.backlog;overtime+=v.overtime;}return {agents,arrivals:n/runs,sla:sla/runs,wait:wait/runs,p90:p90/runs,backlog:backlog/runs,overtime:overtime/runs,util:w.hours.reduce((a,b)=>a+b,0)*d*w.handle*h/(agents*480)*100,cost:agents*8*w.wage};} | |
| function getFactors(){const s=scenario();return {d:s.d*state.customDemand/100,h:s.h*state.customHandle/100}} | |
| function calculate(){const w=workload(),f=getFactors(),runCount=24;const maxStaff=Math.max(16,Math.min(30,w.agents+3));const grid=[];for(let n=1;n<=maxStaff;n++)grid.push(simMany(w,n,f.d,f.h,state.seed,runCount));let rec=grid.find(r=>r.sla>=state.target&&r.util<=85)||null;return {w,f,grid,rec,active:grid[w.agents-1],total:w.hours.reduce((a,b)=>a+b,0)*f.d};} | |
| function notify(txt){const el=$('toast');el.textContent=txt;el.classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.classList.remove('show'),3800)} | |
| function meter(name,value,sub,icon,extra=''){return `<div class="card metric"><div class="metric-head"><span>${name}</span><span class="metric-icon">${icon}</span></div><div class="metric-value">${value}</div><div class="metric-note ${extra}">${sub}</div></div>`;} | |
| function demandChart(w,f){const vals=w.hours.map(x=>x*f.d),cap=60/w.handle/f.h*w.agents,max=Math.max(1,...vals,cap)*1.19,pts=[],bars=[];for(let i=0;i<8;i++){const x=51+i*67,y=193-vals[i]/max*164;bars.push(`<rect class="bar" x="${x}" y="${y}" width="31" height="${193-y}" rx="4" opacity="${i===3||i===4?.95:.72}"/>`);const cy=193-cap/max*164;pts.push(`${x+15.5},${cy}`)}const lines=[0,.25,.5,.75,1].map(p=>{const y=193-p*164;return `<line x1="39" y1="${y}" x2="559" y2="${y}"/><text x="5" y="${y+4}">${fmt(p*max)}</text>`}).join('');const labels=Array.from({length:8},(_,i)=>`<text x="${51+i*67+15}" y="212" text-anchor="middle">${9+i}:00</text>`).join('');return `<svg class="chart" viewBox="0 0 585 228" role="img" aria-label="Bar chart of expected hourly arrivals versus nominal capacity"><g class="grid-lines">${lines}</g>${bars.join('')}<polyline class="capacity" points="${pts.join(' ')}"/>${pts.map(p=>`<circle class="dot" cx="${p.split(',')[0]}" cy="${p.split(',')[1]}" r="4"/>`).join('')}${labels}</svg>`;} | |
| function staffChart(grid,target){const max=100;let bars='';for(let i=0;i<grid.length;i++){const x=45+i*(500/grid.length),width=Math.max(7,500/grid.length-7),height=grid[i].sla/max*155;bars+=`<rect x="${x}" y="${193-height}" height="${height}" width="${width}" rx="4" fill="${grid[i].sla>=target?'#32c7b0':'#908afa'}"/>`;}const y=193-target/max*155;const ticks=[0,25,50,75,100].map(p=>`<line x1="40" x2="551" y1="${193-p*155/100}" y2="${193-p*155/100}" stroke="var(--border)" stroke-dasharray="4 4"/><text x="7" y="${197-p*155/100}">${p}%</text>`).join('');const labels=grid.map((g,i)=>i%2===0?`<text x="${45+i*(500/grid.length)+(500/grid.length-7)/2}" text-anchor="middle" y="213">${g.agents}</text>`:'').join('');return `<svg class="chart" viewBox="0 0 575 236" role="img" aria-label="SLA percentage by staffing level with target line">${ticks}${bars}<line x1="40" x2="551" y1="${y}" y2="${y}" stroke="#36bdae" stroke-width="2.5" stroke-dasharray="5 5"/><text x="546" y="${y-6}" text-anchor="end">TARGET ${target}%</text>${labels}<text x="290" y="231" text-anchor="middle">Number of service agents</text></svg>`;} | |
| function renderOverview(v){const w=v.w,a=v.active,capacity=60/w.handle/v.f.h*w.agents; $('heroTarget').textContent=state.target+'%';$('heroSla').textContent='within '+w.sla+' minutes';$('metricCards').innerHTML=meter('Daily requests',fmt(v.total),'Expected, based on hourly means','↗')+meter('Requests within SLA',fmt(a.sla,1)+'%',`Target: ${state.target}% in ≤${w.sla} min`,'✓',a.sla>=state.target?'good':'bad')+meter('Average queue wait',fmt(a.wait,1)+'m','Time until service begins','◷')+meter('Staffing cost',money(a.cost),'Scheduled daily labor only','$');$('chartWorkload').textContent=w.name;$('demandChart').innerHTML=demandChart(w,v.f);const high=w.hours.filter(x=>x*v.f.d>capacity).length;$('insightTitle').textContent=high?`${high} of 8 hours exceed nominal capacity`:'Nominal capacity covers each hourly demand average';$('insightBody').textContent=high?'The service rate is below expected arrivals during some hours. Carry-over waiting may remain after the peak.':'Even when nominal capacity is sufficient, random arrivals can still create queues. Review simulated SLA and waiting times.';const select=$('mainWorkload');select.innerHTML=state.workloads.map(x=>`<option value="${escapeHTML(x.id)}">${escapeHTML(x.name)}</option>`).join('');select.value=w.id;$('mainStaff').value=w.agents;$('mainSla').value=w.sla;$('mainScenario').innerHTML=SCENARIOS.map(s=>`<option value="${s.id}">${s.name}</option>`).join('');$('mainScenario').value=state.scenario;const chosen=new Set([Math.max(1,w.agents-2),Math.max(1,w.agents-1),w.agents,Math.min(v.grid.length,w.agents+1),Math.min(v.grid.length,w.agents+2)]);if(v.rec)chosen.add(v.rec.agents);$('staffTable').innerHTML=[...chosen].sort((a,b)=>a-b).map(n=>{const r=v.grid[n-1],ok=r.sla>=state.target&&r.util<=85;return `<tr class="${n===w.agents?'current':''}"><td>${n}${n===w.agents?' · CURRENT':''}</td><td class="number">${fmt(r.sla,1)}%</td><td class="number">${fmt(r.wait,1)}m</td><td class="number">${fmt(r.p90,1)}m</td><td class="number">${money(r.cost)}</td><td><span class="chip ${ok?'green':'red'}">${v.rec&&n===v.rec.agents?'RECOMMENDED':ok?'FEASIBLE':'BELOW TARGET'}</span></td></tr>`}).join('');} | |
| function renderLab(v){$('scenarioCards').innerHTML=SCENARIOS.map(s=>`<button type="button" class="scenario ${state.scenario===s.id?'active':''}" data-scenario="${s.id}" aria-pressed="${state.scenario===s.id}"><span class="s-icon">${s.icon}</span><b>${s.name}</b><span>${s.detail}</span></button>`).join('');$('labWorkload').textContent=v.w.name;$('staffChart').innerHTML=staffChart(v.grid,state.target);$('recommendTitle').textContent=v.rec?`Recommended staffing: ${v.rec.agents} agents`:'No tested staffing level satisfies both constraints';$('recommendBody').textContent=v.rec?`${fmt(v.rec.sla,1)}% of requests meet the ${v.w.sla}-minute SLA, with a nominal load ratio of ${fmt(v.rec.util,1)}%. Estimated scheduled labor: ${money(v.rec.cost)} per shift.`:'Try higher staffing, faster handling or a less demanding SLA. The simulator never promises the target will be met in real-world operations.';$('labDemand').value=state.customDemand;$('labSpeed').value=state.customHandle;$('labTarget').value=state.target;$('labWage').value=v.w.wage;$('labSeed').value=state.seed;const rec=v.rec;const saving=rec?rec.cost-v.active.cost:0;$('labCards').innerHTML=meter('Active agents',v.w.agents+'',`Current profile · ${v.w.name}`,'♙')+meter('SLA attainment',fmt(v.active.sla,1)+'%',`Target ${state.target}%`,'✓',v.active.sla>=state.target?'good':'bad')+meter('Nominal load ratio',fmt(v.active.util,1)+'%','Expected workload / available minutes','◎')+meter('Recommended change',rec?(rec.agents-v.w.agents>=0?'+':'')+(rec.agents-v.w.agents)+' agents':'Not found',rec?`Labor difference ${saving>=0?'+':''}${money(saving)}/shift`:'Tested staffing up to '+v.grid.length,'↗');$('labTable').innerHTML=v.grid.map(r=>{const ok=r.sla>=state.target&&r.util<=85;return `<tr class="${r.agents===v.w.agents?'current':''}"><td>${r.agents}${r.agents===v.w.agents?' · CURRENT':''}</td><td class="number">${fmt(r.sla,1)}%</td><td class="number">${fmt(r.wait,1)}m</td><td class="number">${fmt(r.p90,1)}m</td><td class="number">${fmt(r.util,1)}%</td><td class="number">${money(r.cost)}</td><td><span class="chip ${ok?'green':'red'}">${rec&&rec.agents===r.agents?'RECOMMENDED':ok?'FEASIBLE':'NOT FEASIBLE'}</span></td></tr>`}).join('');} | |
| function renderWorkloads(){const s=$('workloadCards');s.innerHTML=state.workloads.map(w=>{const vol=w.hours.reduce((a,b)=>a+b,0);return `<article class="card workcard"><div class="title-row"><div class="avatar" aria-hidden="true">${escapeHTML(w.icon)}</div><span class="chip ${state.selected===w.id?'green':''}">${state.selected===w.id?'ACTIVE':'SAMPLE / CUSTOM'}</span></div><h3>${escapeHTML(w.name)}</h3><p>${escapeHTML(w.description)}</p><div class="work-meta"><div><b>${fmt(vol)}</b><small>Expected arrivals</small></div><div><b>${w.agents}</b><small>Scheduled agents</small></div><div><b>${fmt(w.handle,1)}m</b><small>Avg. handling</small></div><div><b>${fmt(w.sla)}m</b><small>Wait SLA</small></div></div><div class="btn-row"><button class="btn green" type="button" data-select="${escapeHTML(w.id)}">Activate</button><button class="btn secondary" type="button" data-edit="${escapeHTML(w.id)}">Edit</button>${state.workloads.length>1?`<button class="btn danger" type="button" data-delete="${escapeHTML(w.id)}">Archive</button>`:''}</div></article>`}).join('');} | |
| function render(){const data=calculate();cached=data;document.body.classList.toggle('dark',state.theme==='dark');$('themeBtn').textContent=state.theme==='dark'?'☀ Theme':'☾ Theme';document.querySelectorAll('[data-view]').forEach(el=>el.classList.toggle('hidden',el.dataset.view!==state.page));document.querySelectorAll('[data-page]').forEach(el=>{el.classList.toggle('active',el.dataset.page===state.page);if(el.dataset.page===state.page)el.setAttribute('aria-current','page');else el.removeAttribute('aria-current')});renderOverview(data);renderLab(data);renderWorkloads();save()} | |
| function go(page){state.page=page;render();window.scrollTo({top:0,behavior:'instant'})} | |
| function editWorkload(id){editing=id||null;const w=id?state.workloads.find(x=>x.id===id):null;$('editorTitle').textContent=w?'Edit workload':'New workload';$('editName').value=w?.name||'';$('editDescription').value=w?.description||'';$('editAgents').value=w?.agents??4;$('editHandle').value=w?.handle??9;$('editSla').value=w?.sla??10;$('editWage').value=w?.wage??25;$('editHours').value=w?.hours.join(', ')||'12, 16, 20, 24, 22, 18, 15, 11';$('workloadEditor').classList.remove('hidden');$('workloadEditor').scrollIntoView({behavior:'smooth',block:'start'});} | |
| function closeEditor(){editing=null;$('workloadEditor').classList.add('hidden')} | |
| function changeCurrent(field,val){const w=workload();if(!Number.isFinite(val))return false;if(field==='agents'&&Number.isInteger(val)&&val>=1&&val<=30){w.agents=val;return true}if(field==='sla'&&Number.isInteger(val)&&val>=1&&val<=180){w.sla=val;return true}if(field==='wage'&&val>=1&&val<=1000){w.wage=val;return true}return false} | |
| function modifyGlobal(key,value,min,max){const n=Number(value);if(!Number.isInteger(n)||n<min||n>max){notify(`Use an integer from ${min} to ${max}.`);render();return}state[key]=n;render()} | |
| function jsonExport(){const out={...state,page:'overview'};download('queuepilot-project-92-backup.json',JSON.stringify(out,null,2),'application/json')} | |
| function download(filename,text,mime){const url=URL.createObjectURL(new Blob([text],{type:mime+';charset=utf-8'}));const a=document.createElement('a');a.href=url;a.download=filename;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),4000)} | |
| function csvExport(){const w=workload(),v=cached||calculate(),header=['workload','scenario','agents','expected_daily_arrivals','sla_pct','mean_wait_minutes','p90_wait_minutes','nominal_load_ratio_pct','labor_cost_usd'];const rows=v.grid.map(r=>[w.name,scenario().name,r.agents,fmt(r.arrivals,1),fmt(r.sla,2),fmt(r.wait,2),fmt(r.p90,2),fmt(r.util,2),fmt(r.cost,2)]);const csv=[header,...rows].map(row=>row.map(safeCSV).join(',')).join('\r\n');download('queuepilot-project-92-staffing.csv',csv,'text/csv')} | |
| function resetDefault(){if(!confirm('Replace all local edits and workloads with the original sample library? Export a backup first if needed.'))return;const oldTheme=state.theme;state=fresh();state.theme=oldTheme;closeEditor();render();notify('Demo library restored.')} | |
| $('sideNav').addEventListener('click',e=>{const btn=e.target.closest('[data-page]');if(btn)go(btn.dataset.page)}); | |
| $('themeBtn').addEventListener('click',()=>{state.theme=state.theme==='dark'?'light':'dark';render()}); | |
| $('mainWorkload').addEventListener('change',e=>{state.selected=e.target.value;render()}); | |
| $('mainScenario').addEventListener('change',e=>{state.scenario=e.target.value;render()}); | |
| $('mainStaff').addEventListener('change',e=>{if(!changeCurrent('agents',Number(e.target.value)))notify('Staff must be a whole number from 1 to 30.');render()}); | |
| $('mainSla').addEventListener('change',e=>{if(!changeCurrent('sla',Number(e.target.value)))notify('SLA must be a whole number from 1 to 180.');render()}); | |
| for(const [id,key,min,max] of [['labDemand','customDemand',20,300],['labSpeed','customHandle',40,250],['labTarget','target',10,100],['labSeed','seed',1,2147483646]])$(id).addEventListener('change',e=>modifyGlobal(key,e.target.value,min,max)); | |
| $('labWage').addEventListener('change',e=>{if(!changeCurrent('wage',Number(e.target.value)))notify('Wage must be between 1 and 1000.');render()}); | |
| $('scenarioCards').addEventListener('click',e=>{const b=e.target.closest('[data-scenario]');if(b){state.scenario=b.dataset.scenario;render()}}); | |
| $('workloadCards').addEventListener('click',e=>{const sel=e.target.closest('[data-select]'),edit=e.target.closest('[data-edit]'),del=e.target.closest('[data-delete]');if(sel){state.selected=sel.dataset.select;go('overview');notify('Workload activated.')}if(edit)editWorkload(edit.dataset.edit);if(del){if(!confirm('Archive this workload from the local library? Export a backup to preserve it.'))return;state.workloads=state.workloads.filter(w=>w.id!==del.dataset.delete);if(state.selected===del.dataset.delete)state.selected=state.workloads[0].id;closeEditor();render();notify('Workload archived.')}}); | |
| $('editForm').addEventListener('submit',e=>{e.preventDefault();const hours=$('editHours').value.split(',').map(x=>Number(x.trim()));let id=editing;if(!id){id='custom-'+(++counter)+'-'+Date.now().toString(36).slice(-6);while(state.workloads.some(w=>w.id===id))id+='x';}const old=state.workloads.find(w=>w.id===id);const w={id,name:$('editName').value.trim(),description:$('editDescription').value.trim(),icon:old?.icon||'📊',agents:Number($('editAgents').value),handle:Number($('editHandle').value),sla:Number($('editSla').value),wage:Number($('editWage').value),hours};if(!validWorkload(w)){notify('Check the name, numbers and eight hourly arrival values.');return}if(!old&&state.workloads.length>=60){notify('Maximum 60 workloads.');return}if(old)Object.assign(old,w);else state.workloads.push(w);state.selected=id;closeEditor();render();notify(old?'Workload updated.':'Workload created.')}); | |
| $('jsonFile').addEventListener('change',async e=>{const file=e.target.files[0];e.target.value='';if(!file)return;if(file.size>300000){notify('JSON backup is too large (max 300 KB).');return}try{const imported=JSON.parse(await file.text());if(!validState(imported))throw Error('Invalid structure or numeric limits');if(!confirm('Replace the current local library with the imported backup?'))return;state={...imported,page:'workloads'};closeEditor();render();notify('Validated backup restored.')}catch(error){notify('Import rejected: invalid backup or unsafe values.')}}); | |
| document.addEventListener('click',e=>{const action=e.target.closest('[data-action]')?.dataset.action;if(!action)return;switch(action){case 'export-csv':csvExport();break;case 'export-json':jsonExport();break;case 'import-json':$('jsonFile').click();break;case 'go-lab':go('simulator');break;case 'add-workload':editWorkload(null);break;case 'cancel-editor':closeEditor();break;case 'reset-controls':state.scenario='baseline';state.customDemand=100;state.customHandle=100;state.target=80;state.seed=917281;render();notify('Simulation controls restored.');break;case 'reset-all':resetDefault();break;}}); | |
| window.QueuePilot={fresh,validWorkload,validState,rng,exponential,sampleArrivals,quantile,simulateDay,simMany,getState:()=>JSON.parse(JSON.stringify(state)),getMetrics:()=>calculate(),setScenario:(id)=>{if(SCENARIOS.some(x=>x.id===id)){state.scenario=id;render();return true}return false}}; | |
| render(); | |
| })(); | |
| </script> | |
| </body></html> | |