431 lines
30 KiB
HTML
431 lines
30 KiB
HTML
<title>Dispatch Ops Agent</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
|
||
<style>
|
||
:root{
|
||
--paper:#f2f5f4; --surface:#ffffff; --surface-2:#e9edec; --sunken:#dfe5e3;
|
||
--ink:#12201e; --ink-2:#41534f; --ink-3:#6d807c; --rule:#d3dbd9; --rule-2:#c0cbc8;
|
||
--accent:#0a6b62; --accent-soft:#d9eae7; --on-accent:#ffffff;
|
||
--crit:#b3261e; --crit-soft:#fae3e1; --warn:#9a5b00; --warn-soft:#f8e9d2;
|
||
--ok:#2c6b45; --ok-soft:#dcece2;
|
||
--shadow:0 1px 2px rgba(18,32,30,.06), 0 6px 18px -10px rgba(18,32,30,.28);
|
||
}
|
||
@media (prefers-color-scheme: dark){
|
||
:root:not([data-theme="light"]){
|
||
--paper:#0e1615; --surface:#152220; --surface-2:#1d2c29; --sunken:#0a110f;
|
||
--ink:#e6ecea; --ink-2:#a6b5b1; --ink-3:#7b8b87; --rule:#263936; --rule-2:#334a46;
|
||
--accent:#4fbfaf; --accent-soft:#12312d; --on-accent:#06201d;
|
||
--crit:#f08a80; --crit-soft:#3a1a17; --warn:#e0a558; --warn-soft:#33240f;
|
||
--ok:#79c79a; --ok-soft:#122e20;
|
||
--shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 24px -12px rgba(0,0,0,.7);
|
||
}
|
||
}
|
||
:root[data-theme="dark"]{
|
||
--paper:#0e1615; --surface:#152220; --surface-2:#1d2c29; --sunken:#0a110f;
|
||
--ink:#e6ecea; --ink-2:#a6b5b1; --ink-3:#7b8b87; --rule:#263936; --rule-2:#334a46;
|
||
--accent:#4fbfaf; --accent-soft:#12312d; --on-accent:#06201d;
|
||
--crit:#f08a80; --crit-soft:#3a1a17; --warn:#e0a558; --warn-soft:#33240f;
|
||
--ok:#79c79a; --ok-soft:#122e20;
|
||
--shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 24px -12px rgba(0,0,0,.7);
|
||
}
|
||
*{box-sizing:border-box}
|
||
body{
|
||
margin:0; background:var(--paper); color:var(--ink);
|
||
font-family:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",sans-serif;
|
||
font-size:14px; line-height:1.55; -webkit-font-smoothing:antialiased;
|
||
}
|
||
h1,h2,h3{font-family:Archivo,"IBM Plex Sans",system-ui,sans-serif; text-wrap:balance; margin:0}
|
||
.mono{font-family:"IBM Plex Mono",ui-monospace,"Cascadia Mono",monospace; font-variant-numeric:tabular-nums}
|
||
code{font-family:"IBM Plex Mono",ui-monospace,monospace; font-size:.92em}
|
||
.shell{max-width:1280px; margin:0 auto; padding:0 22px 60px}
|
||
|
||
.top{border-bottom:1px solid var(--rule); background:var(--surface)}
|
||
.top-in{max-width:1280px; margin:0 auto; padding:16px 22px; display:flex; flex-wrap:wrap; gap:18px; align-items:center; justify-content:space-between}
|
||
.brand{display:flex; align-items:center; gap:11px}
|
||
.mark{width:30px;height:30px;border-radius:8px;background:var(--accent);display:grid;place-items:center;color:var(--on-accent);font-family:Archivo;font-weight:700;font-size:12px;flex:none}
|
||
.brand b{font-family:Archivo;font-weight:700;font-size:15px;letter-spacing:-.01em;display:block;line-height:1.25}
|
||
.brand span{color:var(--ink-3);font-size:11px;letter-spacing:.06em;text-transform:uppercase}
|
||
.ctx{display:flex;flex-wrap:wrap;margin:0;border:1px solid var(--rule);border-radius:9px;overflow:hidden;background:var(--surface)}
|
||
.ctx>div{padding:5px 13px;border-left:1px solid var(--rule)}
|
||
.ctx>div:first-child{border-left:0}
|
||
.ctx dt{font-size:9.5px;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-3);margin:0}
|
||
.ctx dd{margin:1px 0 0;font-size:12.5px;font-weight:500}
|
||
|
||
.banner{display:block;margin:20px 0 24px;padding:11px 14px;border:1px solid var(--rule-2);border-radius:9px;background:var(--surface-2);color:var(--ink-2);font-size:12.5px}
|
||
.banner b{color:var(--ink)}
|
||
|
||
.verdict{margin-bottom:26px;border:1px solid var(--rule);border-left:4px solid var(--crit);border-radius:10px;background:var(--surface);box-shadow:var(--shadow);overflow:hidden}
|
||
.verdict-hd{padding:17px 20px 15px}
|
||
.verdict-hd h1{font-size:23px;letter-spacing:-.015em;line-height:1.25;max-width:32ch}
|
||
.verdict-hd p{margin:8px 0 0;color:var(--ink-2);max-width:74ch}
|
||
.tiles{display:grid;grid-template-columns:repeat(4,1fr);border-top:1px solid var(--rule)}
|
||
.tile{padding:12px 20px 14px;border-left:1px solid var(--rule)}
|
||
.tile:first-child{border-left:0}
|
||
.tile .k{font-size:9.5px;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-3)}
|
||
.tile .v{font-family:Archivo;font-weight:600;font-size:26px;letter-spacing:-.02em;line-height:1.2;margin-top:2px;font-variant-numeric:tabular-nums}
|
||
.tile .v em{font-style:normal;font-size:15px;color:var(--ink-3)}
|
||
.tile .s{font-size:11.5px;color:var(--ink-3)}
|
||
.v.bad{color:var(--crit)} .v.warnc{color:var(--warn)} .v.okc{color:var(--ok)}
|
||
@media(max-width:820px){.tiles{grid-template-columns:repeat(2,1fr)}.tile:nth-child(3){border-left:0}.tile:nth-child(n+3){border-top:1px solid var(--rule)}}
|
||
|
||
.cols{display:grid;grid-template-columns:minmax(0,1fr) 370px;gap:26px;align-items:start}
|
||
@media(max-width:1080px){.cols{grid-template-columns:minmax(0,1fr)}}
|
||
section{margin-bottom:30px}
|
||
.eyebrow{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);font-weight:600;font-family:Archivo}
|
||
h2{font-size:17px;letter-spacing:-.01em;margin:3px 0}
|
||
.sub{color:var(--ink-2);font-size:13px;margin:5px 0 14px;max-width:74ch}
|
||
|
||
.finds{display:flex;flex-direction:column;gap:1px;background:var(--rule);border:1px solid var(--rule);border-radius:10px;overflow:hidden}
|
||
.find{background:var(--surface);border:0;border-left:3px solid var(--rule-2);padding:13px 16px;text-align:left;width:100%;font:inherit;color:inherit;cursor:pointer;display:block;transition:background .13s}
|
||
.find:hover{background:var(--surface-2)}
|
||
.find:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
||
.find[data-sev="crit"]{border-left-color:var(--crit)}
|
||
.find[data-sev="high"]{border-left-color:var(--warn)}
|
||
.find[data-sev="med"]{border-left-color:var(--ink-3)}
|
||
.find[aria-pressed="true"]{background:var(--surface-2)}
|
||
.find-top{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap}
|
||
.sev{font-family:"IBM Plex Mono",monospace;font-size:9.5px;font-weight:600;letter-spacing:.08em;padding:2px 6px;border-radius:4px;text-transform:uppercase;flex:none}
|
||
.sev.crit{background:var(--crit-soft);color:var(--crit)}
|
||
.sev.high{background:var(--warn-soft);color:var(--warn)}
|
||
.sev.med{background:var(--surface-2);color:var(--ink-2)}
|
||
.find h3{font-size:14px;font-weight:600;flex:1;min-width:220px}
|
||
.find p{margin:7px 0 0;color:var(--ink-2);font-size:12.8px;max-width:78ch}
|
||
.ev{margin-top:9px;padding:8px 10px;background:var(--sunken);border-radius:6px;font-family:"IBM Plex Mono",monospace;font-size:11.5px;line-height:1.6;color:var(--ink-2);overflow-x:auto;white-space:pre;font-variant-numeric:tabular-nums}
|
||
.fix{margin-top:9px;font-size:12.5px;color:var(--ink)}
|
||
.fix b{font-family:Archivo;font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin-right:8px}
|
||
|
||
.chartwrap{border:1px solid var(--rule);border-radius:10px;background:var(--surface);padding:13px 8px 4px;overflow-x:auto;box-shadow:var(--shadow)}
|
||
.legend{display:flex;gap:15px;flex-wrap:wrap;padding:0 12px 9px;font-size:11.5px;color:var(--ink-2)}
|
||
.legend i{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:5px;vertical-align:-1px}
|
||
.legend i.hollow{background:var(--paper);box-shadow:inset 0 0 0 1.6px var(--ink-3)}
|
||
|
||
.tblwrap{border:1px solid var(--rule);border-radius:10px;overflow:auto;background:var(--surface);box-shadow:var(--shadow);max-height:520px}
|
||
table{border-collapse:collapse;width:100%;min-width:740px;font-size:12.5px}
|
||
th{font-family:Archivo;font-weight:600;font-size:9.5px;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-3);text-align:left;padding:9px 12px;border-bottom:1px solid var(--rule);background:var(--surface-2);position:sticky;top:0;white-space:nowrap}
|
||
th.num{text-align:right}
|
||
td{padding:8px 12px;border-bottom:1px solid var(--rule);white-space:nowrap}
|
||
tr:last-child td{border-bottom:0}
|
||
td.num{text-align:right;font-variant-numeric:tabular-nums}
|
||
tbody tr{transition:background .18s}
|
||
tbody tr.hl{background:var(--accent-soft)}
|
||
.chip{display:inline-block;font-family:"IBM Plex Mono",monospace;font-size:10.5px;padding:1px 6px;border-radius:4px;background:var(--surface-2);color:var(--ink-2)}
|
||
.chip.bad{background:var(--crit-soft);color:var(--crit)}
|
||
.chip.good{background:var(--ok-soft);color:var(--ok)}
|
||
.dim{color:var(--ink-3)}
|
||
|
||
.agent{position:sticky;top:16px;border:1px solid var(--rule);border-radius:12px;background:var(--surface);box-shadow:var(--shadow);overflow:hidden;display:flex;flex-direction:column;max-height:calc(100vh - 32px)}
|
||
.agent-hd{padding:11px 15px;border-bottom:1px solid var(--rule);background:var(--surface-2);display:flex;gap:9px;align-items:center}
|
||
.pulse{width:7px;height:7px;border-radius:50%;background:var(--ok);flex:none;box-shadow:0 0 0 3px var(--ok-soft)}
|
||
.agent-hd b{font-family:Archivo;font-size:13px;font-weight:600;display:block;line-height:1.3}
|
||
.agent-hd span{font-size:11px;color:var(--ink-3)}
|
||
.thread{padding:14px 15px;display:flex;flex-direction:column;gap:14px;overflow-y:auto;flex:1;min-height:300px}
|
||
.msg{font-size:13px;line-height:1.55}
|
||
.msg.you{align-self:flex-end;max-width:88%;background:var(--accent);color:var(--on-accent);padding:7px 12px;border-radius:12px 12px 3px 12px}
|
||
.msg.bot .who{font-family:Archivo;font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--accent);font-weight:600;margin-bottom:5px}
|
||
.msg.bot p{margin:0 0 8px}
|
||
.msg.bot p:last-of-type{margin-bottom:0}
|
||
.msg.bot code{background:var(--sunken);padding:1px 4px;border-radius:3px}
|
||
.cite{margin-top:9px;font-size:11px;color:var(--ink-3);border-left:2px solid var(--rule-2);padding-left:8px;line-height:1.45}
|
||
.enter{animation:rise .22s ease-out both}
|
||
@keyframes rise{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
|
||
.asks{border-top:1px solid var(--rule);padding:10px 15px 13px;background:var(--surface-2)}
|
||
.asks .lbl{font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);font-family:Archivo;font-weight:600;margin-bottom:8px}
|
||
.asks .row{display:flex;flex-wrap:wrap;gap:6px}
|
||
.ask{font:inherit;font-size:12px;padding:5px 10px;border:1px solid var(--rule-2);background:var(--surface);color:var(--ink);border-radius:14px;cursor:pointer;transition:.13s}
|
||
.ask:hover{border-color:var(--accent);color:var(--accent)}
|
||
.ask:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
||
.ask[disabled]{opacity:.38;cursor:default;border-color:var(--rule)}
|
||
|
||
.build{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--rule);border:1px solid var(--rule);border-radius:10px;overflow:hidden}
|
||
@media(max-width:880px){.build{grid-template-columns:1fr}}
|
||
.blk{background:var(--surface);padding:15px 17px}
|
||
.blk .step{font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--accent);font-weight:600}
|
||
.blk h3{font-size:14px;font-weight:600;margin:3px 0 9px}
|
||
.blk ul{margin:0;padding-left:16px;color:var(--ink-2);font-size:12.5px}
|
||
.blk li{margin-bottom:6px}
|
||
.blk li:last-child{margin-bottom:0}
|
||
.blk code{color:var(--ink)}
|
||
footer{margin-top:28px;padding-top:15px;border-top:1px solid var(--rule);font-size:11.5px;color:var(--ink-3)}
|
||
@media (prefers-reduced-motion: reduce){*{animation:none!important;transition:none!important}}
|
||
</style>
|
||
|
||
<header class="top">
|
||
<div class="top-in">
|
||
<div class="brand">
|
||
<div class="mark">DG</div>
|
||
<div><b>Dispatch Ops Agent</b><span>DailyGrubs Console · demo</span></div>
|
||
</div>
|
||
<dl class="ctx mono">
|
||
<div><dt>Tenant</dt><dd>Bawa Medical, Nagercoil</dd></div>
|
||
<div><dt>Day</dt><dd>2 Sep 2026</dd></div>
|
||
<div><dt>Riders</dt><dd>1</dd></div>
|
||
<div><dt>Orders</dt><dd>12</dd></div>
|
||
<div><dt>Source</dt><dd>Orders_Detail export</dd></div>
|
||
</dl>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="shell">
|
||
|
||
<p class="banner"><b>Demo.</b> Every figure below is computed from your real <code>Orders_Detail_2026-09-02</code> export — 12 orders, one rider, one day. The agent replies are scripted for this walkthrough; nothing calls a model yet. Customer names and phone numbers are left out.</p>
|
||
|
||
<div class="verdict">
|
||
<div class="verdict-hd">
|
||
<h1>This day’s delivery times were never measured. They were typed in at 23:00.</h1>
|
||
<p>Eleven of the twelve orders were marked delivered inside an 86-second window, 8–11 seconds apart. The twelfth has real stamps, but they contradict each other. Before an agent can answer anything about lateness, ETAs or rider productivity, this one field has to become real — otherwise it will answer confidently and be wrong.</p>
|
||
</div>
|
||
<div class="tiles">
|
||
<div class="tile"><div class="k">Usable delivery times</div><div class="v bad mono">0<em> / 12</em></div><div class="s">bulk-closed 22:59–23:00</div></div>
|
||
<div class="tile"><div class="k">Lifecycle capture</div><div class="v warnc mono">45<em>%</em></div><div class="s">27 of 60 expected stamps</div></div>
|
||
<div class="tile"><div class="k">Slot → assignment</div><div class="v okc mono">+13<em> min</em></div><div class="s">median, after TZ correction</div></div>
|
||
<div class="tile"><div class="k">Day settled</div><div class="v mono">₹756</div><div class="s">36 km · 2 pickup points</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cols">
|
||
<div>
|
||
|
||
<section>
|
||
<div class="eyebrow">Evidence</div>
|
||
<h2>One day, three clocks</h2>
|
||
<p class="sub">Promised slot, assignment and delivery for each order. Slots are drawn corrected by +5:30 — see the second finding. The wall on the right is the problem.</p>
|
||
<div class="chartwrap">
|
||
<div class="legend">
|
||
<span><i class="hollow"></i>Promised slot</span>
|
||
<span><i style="background:var(--accent)"></i>Assigned to rider</span>
|
||
<span><i style="background:var(--crit)"></i>Marked delivered</span>
|
||
<span><i style="background:var(--ok)"></i>Genuine lifecycle — 1 order</span>
|
||
</div>
|
||
<div id="chart"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<div class="eyebrow">Findings — click one to highlight its rows below</div>
|
||
<h2>What the agent found</h2>
|
||
<p class="sub">Ranked by what they block, not by how bad they look. The first two invalidate every time-based number in the console; the rest are hygiene that will bite as volume grows.</p>
|
||
<div class="finds" id="finds"></div>
|
||
</section>
|
||
|
||
<section>
|
||
<div class="eyebrow">Records</div>
|
||
<h2>The twelve orders</h2>
|
||
<p class="sub">Δ is assignment minus corrected slot. Stamps counts how many of the five lifecycle events — assign, start, arrival, pickup, delivery — the rider app actually recorded.</p>
|
||
<div class="tblwrap">
|
||
<table>
|
||
<thead><tr>
|
||
<th>Order</th><th>Slot IST</th><th>Assigned</th><th class="num">Δ</th><th>Delivered</th>
|
||
<th>Zone</th><th>Locality</th><th class="num">Km</th><th class="num">₹</th><th>Stamps</th>
|
||
</tr></thead>
|
||
<tbody id="rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section>
|
||
<div class="eyebrow">Getting there</div>
|
||
<h2>What it takes to make this real</h2>
|
||
<p class="sub">You already own most of the machinery. The gap is not a model — it is a trustworthy event stream and somewhere in the console to put the answer.</p>
|
||
<div class="build">
|
||
<div class="blk">
|
||
<span class="step">01</span>
|
||
<h3>Fix the clock first</h3>
|
||
<ul>
|
||
<li>Stamp <code>deliverytime</code> server-side from the rider’s delivery action, not from a screen cleared at night</li>
|
||
<li>Reject more than ~3 closes per rider per minute, and log every rejection</li>
|
||
<li>Emit <code>deliverydate</code> as true UTC with <code>Z</code>, or true IST with <code>+05:30</code> — not today’s mix of both</li>
|
||
<li>Backfill nothing. Treat everything before the fix as unmeasured</li>
|
||
</ul>
|
||
</div>
|
||
<div class="blk">
|
||
<span class="step">02</span>
|
||
<h3>Build it inside Jupiter</h3>
|
||
<ul>
|
||
<li>One read-only, tool-calling agent that belongs to DailyGrubs — its own service, no borrowed runtime</li>
|
||
<li>Tools, not raw SQL: <code>get_orders</code>, <code>get_rider_logs</code>, <code>zone_summary</code>, <code>data_quality_scan</code></li>
|
||
<li>Scope every tool by <code>tenantid</code> server-side, from the session — never from a model argument</li>
|
||
<li>Keep this day as a regression fixture. The correct answer is “these timestamps are fabricated”</li>
|
||
</ul>
|
||
</div>
|
||
<div class="blk">
|
||
<span class="step">03</span>
|
||
<h3>Land it in the console</h3>
|
||
<ul>
|
||
<li>A drawer on <code>/nearle/reports</code> and <code>/nearle/dispatch</code>, opened from the existing header</li>
|
||
<li>TanStack Query against a new <code>/ai/ask</code> on Jupiter. Never call Anthropic from the browser</li>
|
||
<li>Every answer cites the orders it read, as in the panel. An uncited claim is a bug</li>
|
||
<li>Read-only in v1. Reassigning riders is a separate project, with a confirm step</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<aside>
|
||
<div class="agent">
|
||
<div class="agent-hd">
|
||
<span class="pulse"></span>
|
||
<div><b>Ask about this day</b><span>12 orders · 1 rider · read-only</span></div>
|
||
</div>
|
||
<div class="thread" id="thread">
|
||
<div class="msg you">Did we deliver on time yesterday?</div>
|
||
<div class="msg bot">
|
||
<div class="who">Ops Agent</div>
|
||
<p>I can’t tell you, and neither can the report — the delivery times aren’t real.</p>
|
||
<p>Eleven of the 12 were stamped delivered between <code>22:59:24</code> and <code>23:00:50</code>, 8–11 seconds apart. That is someone closing a list at the end of the day, not eleven deliveries. The twelfth records delivery 16 seconds after its own start.</p>
|
||
<p>What I can tell you: assignment was healthy. Median gap from slot to assignment was 13 minutes.</p>
|
||
<div class="cite">Read: 12 rows · deliverytime, assigntime, deliverydate</div>
|
||
</div>
|
||
</div>
|
||
<div class="asks">
|
||
<div class="lbl">Try asking</div>
|
||
<div class="row" id="asks"></div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<footer>Built from <span class="mono">Orders_Detail_2026-09-02_073552</span> · 13 rows in: 12 orders plus one TOTAL row · all times IST</footer>
|
||
</div>
|
||
|
||
<script>
|
||
var O=[
|
||
{id:"908-20246307",slot:"10:00",assign:"10:04:53",dev:"10:50:56",km:3,rs:60,zone:"King Nagar",loc:"Udhayamarthandam",st:5},
|
||
{id:"908-20246308",slot:"11:00",assign:"11:43:03",dev:"23:00:25",km:1,rs:60,zone:"Weavers Colony",loc:"ராமவர்மபுரம்",st:2},
|
||
{id:"908-20246309",slot:"11:30",assign:"11:43:03",dev:"23:00:34",km:6,rs:84,zone:"King Nagar",loc:"Erumbukadu (post",st:2},
|
||
{id:"908-20246310",slot:"11:30",assign:"11:43:03",dev:"23:00:42",km:4,rs:60,zone:"King Nagar",loc:"Kurusady",st:2},
|
||
{id:"908-20246311",slot:"11:30",assign:"11:43:03",dev:"23:00:50",km:5,rs:72,zone:"King Nagar",loc:"NGL",st:2},
|
||
{id:"908-20246312",slot:"13:00",assign:"12:39:45",dev:"23:00:18",km:1,rs:60,zone:"Weavers Colony",loc:"Simon Nagar",st:2},
|
||
{id:"908-20246313",slot:"13:00",assign:"13:26:51",dev:"22:59:59",km:3,rs:60,zone:"King Nagar",loc:"Erulapappuram",st:2},
|
||
{id:"908-20246314",slot:"13:00",assign:"13:26:51",dev:"23:00:07",km:2,rs:60,zone:"King Nagar",loc:"Ramavarmapuram",st:2},
|
||
{id:"908-20246315",slot:"13:00",assign:"13:26:51",dev:"22:59:51",km:4,rs:60,zone:"King Nagar",loc:"NGL",st:2},
|
||
{id:"908-20246316",slot:"15:00",assign:"14:36:45",dev:"22:59:42",km:3,rs:60,zone:"King Nagar",loc:"CBH ROAD",st:2},
|
||
{id:"908-20246317",slot:"17:00",assign:"19:20:42",dev:"22:59:33",km:1,rs:60,zone:"King Nagar",loc:"Puthukudierupu",st:2},
|
||
{id:"908-20246318",slot:"19:30",assign:"19:20:42",dev:"22:59:24",km:3,rs:60,zone:"King Nagar",loc:"Parvathipuram",st:2}
|
||
];
|
||
function hrs(t){var p=t.split(":").map(Number);return p[0]+p[1]/60+(p[2]||0)/3600}
|
||
function dmin(o){return Math.round((hrs(o.assign)-hrs(o.slot))*60)}
|
||
|
||
(function(){
|
||
var W=760,PL=106,PR=728,T0=9.4,T1=23.7,RH=25,TOP=22;
|
||
var H=TOP+O.length*RH+40;
|
||
function x(h){return PL+(h-T0)/(T1-T0)*(PR-PL)}
|
||
var s='<svg viewBox="0 0 '+W+' '+H+'" width="100%" style="display:block;min-width:640px" role="img" aria-label="Timeline showing promised slot, assignment and delivery for twelve orders. All twelve deliveries are stamped just before 23:00.">';
|
||
for(var h=10;h<=23;h++){
|
||
s+='<line x1="'+x(h).toFixed(1)+'" y1="'+TOP+'" x2="'+x(h).toFixed(1)+'" y2="'+(TOP+O.length*RH)+'" stroke="var(--rule)" stroke-width="1" fill="none"/>';
|
||
if(h%2===0||h===23)s+='<text x="'+x(h).toFixed(1)+'" y="'+(TOP+O.length*RH+16)+'" fill="var(--ink-3)" font-size="10" font-family="IBM Plex Mono, monospace" text-anchor="middle">'+String(h)+':00</text>';
|
||
}
|
||
O.forEach(function(o,i){
|
||
var y=TOP+i*RH+RH/2, xs=x(hrs(o.slot)), xa=x(hrs(o.assign)), xd=x(hrs(o.dev)), real=(o.st===5);
|
||
s+='<text x="'+(PL-11)+'" y="'+(y+3.5)+'" fill="var(--ink-2)" font-size="10.5" font-family="IBM Plex Mono, monospace" text-anchor="end">'+o.id.slice(4)+'</text>';
|
||
s+='<line x1="'+PL+'" y1="'+y+'" x2="'+PR+'" y2="'+y+'" stroke="var(--rule)" stroke-width="1" stroke-dasharray="2 4" fill="none"/>';
|
||
s+='<line x1="'+Math.min(xa,xd).toFixed(1)+'" y1="'+y+'" x2="'+Math.max(xa,xd).toFixed(1)+'" y2="'+y+'" stroke="'+(real?'var(--ok)':'var(--crit)')+'" stroke-width="'+(real?2.5:1.5)+'" opacity="'+(real?0.85:0.3)+'" fill="none"/>';
|
||
s+='<circle cx="'+xs.toFixed(1)+'" cy="'+y+'" r="4" fill="var(--paper)" stroke="var(--ink-3)" stroke-width="1.6"/>';
|
||
s+='<circle cx="'+xa.toFixed(1)+'" cy="'+y+'" r="4" fill="var(--accent)"/>';
|
||
s+='<circle cx="'+xd.toFixed(1)+'" cy="'+y+'" r="4.5" fill="'+(real?'var(--ok)':'var(--crit)')+'"/>';
|
||
});
|
||
var xw=x(23);
|
||
s+='<line x1="'+xw.toFixed(1)+'" y1="'+(TOP-4)+'" x2="'+xw.toFixed(1)+'" y2="'+(TOP+O.length*RH+2)+'" stroke="var(--crit)" stroke-width="1" stroke-dasharray="3 3" opacity="0.5" fill="none"/>';
|
||
s+='<text x="'+(xw-7).toFixed(1)+'" y="'+(TOP-8)+'" fill="var(--crit)" font-size="10" font-family="Archivo, sans-serif" font-weight="600" text-anchor="end">11 deliveries stamped in 86s →</text>';
|
||
s+='</svg>';
|
||
document.getElementById("chart").innerHTML=s;
|
||
})();
|
||
|
||
document.getElementById("rows").innerHTML=O.map(function(o){
|
||
var d=dmin(o), late=Math.abs(d)>30, real=(o.st===5);
|
||
return '<tr data-id="'+o.id+'"><td class="mono">'+o.id+'</td>'+
|
||
'<td class="mono dim">'+o.slot+'</td><td class="mono">'+o.assign+'</td>'+
|
||
'<td class="num mono" style="color:'+(late?'var(--warn)':'var(--ink-2)')+'">'+(d>0?'+':'')+d+'m</td>'+
|
||
'<td><span class="chip '+(real?'good':'bad')+'">'+o.dev+'</span></td>'+
|
||
'<td>'+o.zone+'</td><td class="dim">'+o.loc+'</td>'+
|
||
'<td class="num mono">'+o.km+'</td><td class="num mono">'+o.rs+'</td>'+
|
||
'<td><span class="chip '+(real?'good':'bad')+'">'+o.st+'/5</span></td></tr>';
|
||
}).join("");
|
||
|
||
var F=[
|
||
{sev:"crit",t:"Delivery timestamps are written in bulk, not observed",
|
||
b:"Eleven orders, one 86-second window, spaced 8 to 11 seconds apart — a rider cannot complete drops nine seconds apart. The twelfth is no better: its delivery lands 16 seconds after its own start. No SLA, no ETA accuracy and no rider productivity figure can be derived from this column — and any agent that reads it will report nonsense with total confidence.",
|
||
e:"22:59:24 …318\n22:59:33 …317 Δ 9s\n22:59:42 …316 Δ 9s\n ⋮ (8 more, 8–11s apart)\n23:00:50 …311",
|
||
f:"Stamp delivery server-side from the rider’s action. Rate-limit closes per rider per minute.",
|
||
ids:["908-20246308","908-20246309","908-20246310","908-20246311","908-20246312","908-20246313","908-20246314","908-20246315","908-20246316","908-20246317","908-20246318"]},
|
||
{sev:"crit",t:"deliverydate is labelled +05:30 but the wall clock is UTC",
|
||
b:"The raw field reads 04:30 for an order assigned at 10:04. Add 5:30 and every slot lands within minutes of its assignment, so the offset is real and the string is double-labelled. Read as-is, every promised time in the console is five and a half hours early.",
|
||
e:"raw 2026-09-02T04:30:00+05:30\nassign 10:04:53\n+5:30 10:00 → assigned +5m ✓",
|
||
f:"Emit true UTC with Z, or true IST with +05:30. Normalise on ingest, before anything reads it.",
|
||
ids:O.map(function(o){return o.id})},
|
||
{sev:"high",t:"The rider app records 2 of 5 lifecycle events",
|
||
b:"start, arrival and pickup are empty on 11 of 12 orders — 27 of 60 expected stamps, 45%. The one order that has them is internally impossible: arrival at 10:33:25 and pickup at 10:33:38 both precede start at 10:50:40, and delivery lands 16 seconds after start. Cycle-time analysis is not available from this feed at any sample size.",
|
||
e:"assign 12/12\nstart 1/12\narrival 1/12\npickup 1/12\ndelivery 12/12 ← 11 fabricated\n──────────────\ntotal 27/60 = 45%",
|
||
f:"Instrument the four intermediate transitions, then add a monotonic-ordering check at write time.",
|
||
ids:["908-20246307"]},
|
||
{sev:"med",t:"Locality is free text, and one place appears twice in two scripts",
|
||
b:"ராமவர்மபுரம் and Ramavarmapuram are the same locality. Erumbukadu (post carries an unclosed parenthesis. Casing is inconsistent. Dispatch buckets zones on deliverysuburb, so this leaks straight into the plan.",
|
||
e:"ராமவர்மபுரம் ≡ Ramavarmapuram\n\"Erumbukadu (post\" unbalanced\nNGL / CBH ROAD casing",
|
||
f:"Resolve localities against a gazetteer at order creation; keep the typed string as a display alias.",
|
||
ids:["908-20246308","908-20246314","908-20246309"]},
|
||
{sev:"med",t:"The export puts a TOTAL row inside the data",
|
||
b:"Row 14 has a blank orderid and carries the column sums. The sums are right — 36 km and ₹756 — but any pipeline reading the file as records gains a thirteenth phantom order with a 36 km trip. It already fooled the first pass of this analysis.",
|
||
e:"row 14: orderid = \"\"\n ordernotes = TOTAL\n kms = 36 deliverycharge = 756",
|
||
f:"Move totals to a footer sheet, or drop rows with no orderid on ingest.",
|
||
ids:[]},
|
||
{sev:"med",t:"Pickup geometry is inconsistent for a fixed store",
|
||
b:"Bawa Medical resolves to both 8.1841265 and 8.184126 — the same point at two precisions — and pickupsuburb is blank on all twelve rows while locationsuburb is populated. Distance and clustering both key off these fields.",
|
||
e:"8.1841265, 77.4318002 Bawa medical\n8.184126 , 77.4318002 same store\n8.1798997, 77.4195841 Bawaa medicals 2\npickupsuburb blank × 12",
|
||
f:"Snap pickup coordinates to the stored location record instead of copying them onto each order.",
|
||
ids:[]}
|
||
];
|
||
document.getElementById("finds").innerHTML=F.map(function(f,i){
|
||
var label=f.sev==="crit"?"blocking":(f.sev==="high"?"high":"medium");
|
||
return '<button class="find" data-sev="'+f.sev+'" data-i="'+i+'" aria-pressed="false">'+
|
||
'<span class="find-top"><span class="sev '+f.sev+'">'+label+'</span><h3>'+f.t+'</h3></span>'+
|
||
'<p>'+f.b+'</p><div class="ev">'+f.e+'</div>'+
|
||
'<div class="fix"><b>Fix</b>'+f.f+'</div></button>';
|
||
}).join("");
|
||
|
||
var active=-1;
|
||
document.getElementById("finds").addEventListener("click",function(e){
|
||
var btn=e.target.closest(".find"); if(!btn)return;
|
||
var i=+btn.dataset.i, on=(i!==active); active=on?i:-1;
|
||
document.querySelectorAll(".find").forEach(function(b){b.setAttribute("aria-pressed",String(+b.dataset.i===active))});
|
||
var set={}; if(on)F[i].ids.forEach(function(id){set[id]=1});
|
||
document.querySelectorAll("#rows tr").forEach(function(tr){tr.classList.toggle("hl",!!set[tr.dataset.id])});
|
||
});
|
||
|
||
var QA=[
|
||
{q:"Was the rider actually late?",
|
||
a:'<p>Not on assignment. Once the timezone is corrected, the median gap from promised slot to assignment is <strong>13 minutes</strong>, and nine of twelve were assigned inside 30.</p>'+
|
||
'<p>Two ran wide: <code>…317</code> at +140 minutes, and <code>…316</code> assigned 24 minutes <em>before</em> its slot. Both were single-order slots late in the day.</p>'+
|
||
'<p>Whether the delivery itself was late is unanswerable, for the reason in the first finding.</p>',
|
||
c:"Read: assigntime, deliverydate corrected +5:30 · 12 rows"},
|
||
{q:"What is the delivery charge rule?",
|
||
a:'<p>Nobody told me, but the twelve rows only fit one rule: <strong>₹60 up to 4 km, then ₹12 for each further km.</strong></p>'+
|
||
'<p>It holds exactly. 6 km → ₹84, 5 km → ₹72, everything else flat ₹60. Day total ₹756 over 36 km, matching the export footer.</p>'+
|
||
'<p>Treat it as a hypothesis: it comes from one day and one tenant, so a slab above 6 km would be invisible here.</p>',
|
||
c:"Derived: kms × deliverycharge · 12 rows · no pricing config consulted"},
|
||
{q:"Which zone costs most per drop?",
|
||
a:'<p>King Nagar carries 10 of 12 orders, 34 km and ₹636 — <strong>3.4 km and ₹63.60 per drop.</strong></p>'+
|
||
'<p>Weavers Colony has 2 orders, 2 km and ₹120, both from the second pickup point at Bawaa Medicals 2.</p>'+
|
||
'<p>I would not act on this. At 12 orders the whole difference is two short trips; ask me again after three weeks.</p>',
|
||
c:"Grouped: deliverysuburb × kms, deliverycharge · 12 rows"},
|
||
{q:"What should I fix first?",
|
||
a:'<p>The delivery timestamp, and nothing else until it is real. Every question you actually want answered — are we late, is one rider enough, which zone is bleeding — reads that field.</p>'+
|
||
'<p>Second the timezone, because it is a one-line fix that silently corrupts every slot shown in the console.</p>'+
|
||
'<p>The other four are hygiene. They will hurt at 200 orders a day. They are not hurting at 12.</p>',
|
||
c:"Ranked by downstream dependency · 6 findings"}
|
||
];
|
||
var thread=document.getElementById("thread"), asks=document.getElementById("asks");
|
||
asks.innerHTML=QA.map(function(x,i){return '<button class="ask" data-i="'+i+'">'+x.q+'</button>'}).join("");
|
||
asks.addEventListener("click",function(e){
|
||
var b=e.target.closest(".ask"); if(!b||b.disabled)return;
|
||
var x=QA[+b.dataset.i]; b.disabled=true;
|
||
thread.insertAdjacentHTML("beforeend",'<div class="msg you enter">'+x.q+'</div>');
|
||
thread.scrollTop=thread.scrollHeight;
|
||
setTimeout(function(){
|
||
thread.insertAdjacentHTML("beforeend",'<div class="msg bot enter"><div class="who">Ops Agent</div>'+x.a+'<div class="cite">'+x.c+'</div></div>');
|
||
thread.scrollTop=thread.scrollHeight;
|
||
},380);
|
||
});
|
||
</script>
|