design
PM, Tricycle Hardware — console design
Section titled “PM, Tricycle Hardware — console design”The cross-business-unit cockpit for the Product Manager who owns the whole Tricycle Hardware unit. Its job is one sentence: let the PM answer any question about any team, in any business unit, at a glance. It is a read-only rollup — it monitors and analyses, it never operates a work order. Operational control stays in each role’s own console.
Live-data rule: this console ships before the PM Maintenance and Cluster Maintenance consoles, so every metric here must be computable from a source that is live at launch. Maintenance metrics therefore come from the Head of Maintenance console (Fleet-ready, open work orders, median time to resolve, first-pass validation, immobilised, WO aging, WO fault-class); Production/Retrofit/Inventory/Procurement metrics come from their own live consoles. Metrics that only exist on the not-yet-live consoles (30-day recurrence, repeat-failure rate, rider-days lost, MTBF, cohort intelligence, engineering escalations to TH Engineering) are deliberately not used — including in the team scorecards, which are re-metriced onto the same live sources.
Built on the canonical Atlas web shell (sidebar + topbar + stacked #pg-* pages, the
.kpi band, .tbl tables, .pseg filters, .dlg confirmations, toast()), the same
atom system as the swept cluster-maintenance-manager reference. Boots straight to the
Overview — no login gate — and honours ?scenario=<view> deep links.
Persona
Section titled “Persona”Hadiza Mamudu, Product Manager, Tricycle Hardware. Scope is all clusters,
read-only. A page’s top date filter (dateRange() in the header actions, never the
topbar) scopes that page’s charts only; every data table carries its own from/to
filter in the table head (dateFilter()), independent of the top one — the work-order/case
tables (dated via woIso() from the WO id), plus Requests and Tech feedback — alongside
their status/priority filters and search. So a chart page shows both: the top filter drives
the trend chart, the table filter drives the table. Console access
is role-based, so the PM
monitors a business unit’s rollup here but does not open another role’s workspace from it. Distinct from the two neighbouring surfaces: PM, Maintenance goes deep on
reliability and roadmap for one business unit; VP, Tricycle Hardware is the executive
sign-off view. This console sits between them as the PM’s day-to-day monitoring cockpit
across every business unit.
Information architecture
Section titled “Information architecture”Sidebar groups → one #pg-* page each, rendered by a RENDER map and the nav() router.
- Overview —
#pg-overviewand#pg-riders(Rider overview), then Business units, then Insight, then Reporting. - Business units —
#pg-production,#pg-maintenance,#pg-retrofit,#pg-supply(Inventory). - Insight —
#pg-personas,#pg-analytics,#pg-field(Requests),#pg-feedback(Tech feedback). - Reporting —
#pg-reports. - Shell pages —
#pg-notifications(the bell),#pg-settings(the account menu).
Every business unit nav item is also reachable by ?scenario= for deep links from the flows
gallery.
Overview — the cockpit (#pg-overview)
Section titled “Overview — the cockpit (#pg-overview)”Layered so each band answers a different question the moment the page opens:
- North-star KPI band (
renderKpis('ov-kpis', NKPI, 4)) — four unit-wide numbers, each a.kpiwith a tonal icon, a.kpi-dtrend chip and a sparkline: units closed (MTD), fleet-ready %, median time to resolve, riders live. Riders live is a part of the retrofitted fleet, not a figure beside it (RETRO_TO_DATE,retroOffRoad,ridersLive): the card states riders on the road today out of the kekes retrofitted since the programme started, and the gap is what is off the road, immobilised or up for retrieval. Live can never exceed retrofitted. Units closed counts units, not work orders. One work order builds or converts several units, so the two counts differ, and the unit count is the one a PM acts on. The tile states the measure and stops there: what the number is, where it comes from and the fact that stands beside it, never a paragraph teaching the reader the difference between two counts. First-pass yield and typical cover came off. Both are line and store measures, read on the pages that own them; the cockpit carries the four the PM is answerable for. The bays are Argo’s: Metro and Honda, with Ifo not created yet, the retrofit store is at Sango, and the personas are the ones the other consoles name. Every tile opens a.dlgwith its 13-week trend chart and two cross-links. - Business unit health board (
domainCards) — a.grid2of four business unit cards, each a.cardcarrying a status.badge, a.bay-statsgrid of headline metrics, a sparkline and an “Open ›”.card-link. The whole card navigates to that business unit. This is the literal “any business unit at a glance” surface. The status is computed, not set: each business unit scores its key metrics against targets (DHEALTH) and the worst band wins (green on target / amber watch / red breach). Clicking the badge opens the metric breakdown (domainHealthDlg) so the rating is auditable, e.g. Maintenance = Watch because Fleet-ready (92%) is under its 95% target, median time to resolve (17.4h) is over the 12h target, and 4 units are immobilised (target 0); Inventory = At risk because 3 items are critical (target 0). Targets: On-time ≥85%, First-pass yield ≥90%, Behind ≤3, Fleet-ready ≥95%, Median time to resolve ≤12h, Immobilised 0, Critical items 0, Days of cover ≥21d, Open requests ≤10. - Cross-business-unit watchlist (
attnList) — a.lrowlist of what needs the PM across every business unit, each row tagged with its business unit and severity, click-through to the business unit page. - Signal from the field — a
.grid2: requests coming up (.statrowrollup) and console adoption per team (.hbarbars).
Rider overview (#pg-riders)
Section titled “Rider overview (#pg-riders)”Retrofit holds the conversion, the kit and the retrieval; Maintenance holds the cases. Both are facts about one rider on one keke, and answering “how is this rider doing” meant opening two pages and joining them by plate. One row carries both halves.
The register is the console’s rider-to-plate map (RIDERS), which is the same map the
Maintenance page reads a plate’s rider from, so the two can never name a different rider for
the same keke. Each rider carries their number under their name (RIDER_TEL), on the row
and on the record, in the format the Head of Maintenance console holds it — Musa Ibrahim is
on both consoles and is on one number.
A five-cell .mstat summary leads with active riders, then retrofit complete, awaiting
retrofit, riders with an open case (with the open-case count and how many are on a repeat
fault) and vehicles up for retrieval.
Active is the Onboarding consoles’ word, used with their definition rather than a second
one. pm-onboarding’s riderIsActive is retrofit completed, activated, the account enabled
and at least one swap logged, and rdActive() is that, so the two consoles cannot mean
different things by the same word. This console already carries the first two — the rider
record’s onboarding rows carry the activation fee, and the lifecycle says when the keke was
converted.
The account is read from the retrieval the fleet raised against the keke. A retrieval is
raised for one of three reasons and two of them are account states that disable the rider —
no swap in 28 days, and a wallet in arrears. Efficiency under 90% is a performance
flag and leaves the account alone. Swaps accrue from the day the retrofit completed at a rate
seeded off the plate (rdAcct), so a rider converted last week reads a smaller lifetime than
one converted in May and a keke still queued has never swapped.
The number reads back off the rows it sits above: of the six retrofit-complete riders, one is
up for retrieval for no swap in 28 days, which is the Retrieval column on that row. The rider
record carries the four facts behind it (rdAcctRows) — rider status, account with the reason
it was disabled, swaps lifetime and last swap. Under it a .grid2 puts where riders are in retrofit beside
open cases by priority, both .hbar over the same rows the table below holds.
The table carries rider, plate, retrofit stage, cases and open cases in the window, last
fault, retrieval stage and where the keke was last seen. Filters: search, retrofit stage,
case state (with an open case / no open case / repeat fault) and retrieval state — search
also matches the phone number — plus the
page’s own from/to range, which is the same GFROM/GTO window every other page uses — so
a case count here and a case count on Maintenance are the same number over the same weeks.
The kit is named only where it is still on the keke. A retrieved kit is already fitted to another keke, so printing its id on both rows read as one kit fitted twice; a vehicle whose kit came off reads Kit retrieved with no id, and where it went is on the rider record’s kit history. A keke that never had one reads No kit fitted.
A rider with no lifecycle row still opens. openRider used to refuse a plate with no
TUNIT record — the two riders whose kit was retrieved and the one never fitted — so three
riders on the register were unreachable. It now falls through to openRiderNoKit, which
carries the rider, the plate, the case count, the last known position, the cases, the
retrieval and the kit history that says the kit came off.
No ranking cards. The page carried a .grid2 putting retrofit stage beside open cases by
priority. Both restated the table under them a second time, in a second shape, so they came
off and the page runs summary band → register.
The rider record carries the cases. A Maintenance tab on the rider record lists every case on that keke newest first — case id, fault, type, priority, status, opened, time to resolve — each opening the case record, through the same case set the Maintenance page reads.
Business unit pages
Section titled “Business unit pages”A section label says what the section holds. Production and Retrofit labelled their work-order section Needs attention while the table under it holds every work order the filters allow, so the label described a subset that was not there. Both read Work orders.
Each business unit page carries its own six-tile .kpi band, a trend lineChart with hover, a
ranking or breakdown, and an “at risk” .tbl table. Metrics reconcile with the
underlying source consoles.
-
Maintenance (
#pg-maintenance) — every metric is sourced from the Head of Maintenance console (the live-at-launch source), not PM Maintenance or Cluster Maintenance which ship later: Fleet-ready, open work orders, median time to resolve, repeat rate, immobilised (withdrawn) and WO aging. Repeat rate, not first-pass validation: the Maintenance Supervisor works the repair rather than receiving a technician’s submission for it, so there is no first pass to score and no send-back to count. What a Head of Maintenance acts on is a vehicle back for the same fault inside 30 days, which is what the recurrence watchlist and the Bundle R2 investigation are about. Reconciled with the Head of Maintenance rework: work-order priority is Argo’s P1/P2/P3 (not “severity”); the Fleet Status Wall bands are derived from each vehicle’s worst open case priority (any category, not just repair WOs) — shown as a shape + colour legend (never colour-only,vshape()) with the severity spelled out: Critical = P1 open, At-risk = P2 open, Watch = P3 open, Healthy = no open case, Unknown = telemetry dark (VBANDS/bandBlock(), mirrored from the Head of Maintenance wall). Laid out side by side, the fault-class ranking (.hbar, each class over the 30-day total) and a Preventive maintenance card (PM compliance, schedule attainment, due this week, overdue, PM backlog) share one.grid2row. The open-work table is a consolidated cases table with an Issue category column (Repair / Oil change / Field escalation / Battery / Unclassified), filterable by category and priority, each row carrying its rider beside the plate and status With technician / With Maintenance Supervisor. A page-level Monitor / Analyse.pseg(mtView) splits the two jobs. Monitor is the at-a-glance state above. Analyse is a data-analysis instrument built from the case-level records (MCASES— opened week, resolved week, fault class, priority, plate, bay, reopen): a stat band of median / mean / P90 time-to-resolve with open count, repeat rate and the 8-week backlog change; a time-to-resolve distribution histogram (.hbarbuckets) beside a most common faults ranking - each fault’s own share of all cases, the vital few painted red; an open-backlog weekly line (lineChart, arrival vs resolution with a falling-behind / keeping-up read); a recurring-vehicles table (plates with 2+ cases,recurrence(), each row opening its case history viaopenRecCases); and an Investigations register. Action cells sit under their header: the recurring and investigation tables right-aligned the button inline while theActionheader stayed left, so the control floated away from the word it belongs under. The flow defines no.rclass, so the override came off rather than a second one going on.The Analyse view is not read-only: the PM can flag a recurring vehicle for investigation and open an investigation, and export a findings pack (the owner can export from their own console too) — investigation actions only, never work-order operation, which stays in the Head of Maintenance / persona consoles.
A flag carries its evidence. Raising an investigation attaches the records that justified it (
evPlate,evSlow,evFault,evBacklog,evGroup,evUnitSlow,evUnitCause,evPart,evThin): a measured headline plus the actual rows, shown in the raise dialog before it is sent and kept on the record for the owner to read. A rider is always named with their plate, never one without the other — so maintenance and retrofit evidence carries plate + rider + fault + priority + opened + time to resolve, production carries unit + line, and inventory carries part + SKU + stockouts + cover. A rider belongs to a vehicle, so the same plate always shows the same rider.The analysis itself can be flagged, not just a repeat offender. Every Analyse card carries its own flag: Flag slow resolution off the time-to-resolve distribution (attaching the cases over target and everything still open), Flag top fault off the most-common-faults ranking (or click any fault row to flag that one), Flag backlog off the arrival-vs-resolution trend, Flag slow rework and Flag top cause on Production and Retrofit, and Flag thin cover on Inventory. So “resolution time is running long” is flaggable in one click, with the evidence attached.
One measure per idea, and one source per number. The Speed & throughput band carries typical and slowest 10% only — median and mean sat side by side reading the same figure, so the mean is gone; repeat rate moved out because the Rates band’s first-time fix is its exact complement; and the open count is read from the case records, so the Monitor tile and the Analyse band can no longer disagree (they said 10 and 16). On Inventory the rates band no longer restates the counts above it (well stocked and in stock now were the complements of under target and out of stock); it carries what the counts cannot say — ran out, chronic, and demand concentration from the top five parts.
The targets the PM sets drive the analysis, not numbers buried in it.
tgtMax/tgtMin/tgtLblread the green band the PM set on Analytics > Health targets, so Fixed on time measures against the Median time to resolve target and says so (“within the 12h target”), Production and Retrofit measure against a new PM-settable Rework time target, Inventory’s Under cover target count uses the Days of cover target, and the evidence attached to a flag quotes the same threshold. Move a band and every one of those figures and labels moves with it.Investigations are an accountable process, not a checkbox. Opening one assigns a named owner with their role (
INVOWNlists who is assignable per business unit, so the Maintenance owner is the Head of Maintenance and the Inventory owner is the Inventory Manager) and notifies them — the assignment lands in their own console’s queue, which is how another persona sees it. Status runs Open → In progress → Resolved, and every step is recorded in an activity log stamped with who did it, their role and when (log[], rendered as.ilogrows): who opened it, that the assignment was emailed, when the owner started work, each finding the owner records, and the closure. Closing requires both a root cause and the action taken (invCloseDlg→invResolverefuses to resolve without them), and a resolved investigation shows both back as its conclusion. The register lists ref, subject, owner, role, severity, how long it has been running, status and a Remind action that nudges the owner and records the nudge in the log. The register carries the full filter set — search, status, severity, a from/to date on when it was raised, and a clear — with a live count, and its summary band (active, not started, in progress, running over) sits clear of the table. Assigning an investigation also drops an assignment notification into the owner’s own console, so it is not missed among the personas who share the work.The PM sets how long an investigation may run before it alerts them.
slaSelect()(3 to 30 days,invSLA) sits in the register head and on Notifications; anything still unresolved past that period is marked running over in the register and raised to the PM as an Investigation running over notification naming the owner and the days elapsed (invAlerts()/invOver()). So the owner gets the reminder and the PM gets told when something is drifting, on a period they control.The PM sets the delivery goals for a business unit, and every console tracks the same list. Several goals can run at once in one unit -
GOALS[unit]is a list, so Production can be chasing battery packs and controller units on different windows at the same time, each with its own target, deadline and delivery owner. Production and Retrofit carry a Goals section at the top of Monitor, one.gcardper goal (goalSection,goalCardOne,gStat): the goal and deadline on their own line, then its state, delivery owner and actions, then a progress bar with a marker for where we should be by now, whose legend carries delivered against target and how far off pace it is. Two figures sit under it, the ones the bar does not carry: the rate needed from here, and where the current pace finishes (short by how many, if short). Two targets run side by side in a.grid2, so the card carries what fits half a page · it used to put the goal, its state, its owner and two buttons on one line and four figures beneath, and the goal was squeezed to three words a line. Underneath, the split by line or bay shows where the output is coming from. Set goal (openGoalDlg/goalSave) takes what we are delivering, the target, the delivery owner and the start and deadline, recomputes the window and notifies the owner. A goal drifting off pace raises a Goal behind pace notification to the PM (goalAlerts) alongside the investigation overruns.A goal has a lifecycle, so finishing one is a real event (
gState): Not started until its window opens (it is never scored as behind before it begins), Active while it runs, Met the moment delivered reaches the target, Missed if the window closes short.A finished goal leaves the dashboard by itself — only live goals (active or not started) keep a card, so the page does not silt up with finished work. Met and missed goals drop straight into a Past goals table (ref, goal, target, delivered, result, window; headed by how many were met, and a row opens the record). The PM can also void a goal — superseded, set in error, priority changed — which requires a reason, files it under past goals with that reason on the record, and keeps everything delivered against it (
openVoidDlg/voidGoal). A met goal raises a Goal met notification so it does not finish unnoticed. New goal in the page actions adds to the list — it never overwrites a running goal — and starts at zero delivered; Edit changes a running one. The delivery owner’s own console (Production Manager, Retrofit Station Manager) and the accountable head (Head of Production, Head of Retrofit) show the same list with the same states, so the PM, the owner and the head read one set of numbers.Recurrence tables show the worst 6 only (badged “worst 6 of 18”) so the page stays readable without a long scroll; the drill-down dialog holds each one’s full history.
Every business unit carries the same Monitor / Analyse split (
UVIEW), so the CTO gets the same depth on each. Production, Retrofit and Inventory drive their Analyse view through a shared engine (genSetbuilds the record set,unitAnalyserenders the five sections andSETS+grpDlgback the drill-down), configured per unit; Inventory is SKU-shaped rather than case-shaped so it has its ownrenderSpAnalyse. All four share theinvCard()register and the investigation actions. -
Production (
#pg-production) — Monitor as above; Analyse (PCASES, QA defects) reads median / mean / P90 rework time, a rework-time distribution beside a defect Pareto (cumulative %), an open-defect backlog weekly line, and units failing QA more than once (recKey:'ent') each opening its defect history. -
Retrofit (
#pg-retrofit) — Monitor: conversions trend, the conversion funnel and build quality (.statrow), retrofit work orders at risk. Analyse (RCASES, conversion reworks) mirrors Production with a conversion-issue Pareto and vehicles reworked more than once. -
Maintenance carries a Report beside Cases, Parts & spend and Analyse, built to the head-of-maintenance console’s shape: a contents list of seven sections you switch on and off (
MTSEC,.tgl), the brief itself rendered underneath from exactly those sections, the written commentary the figures cannot supply (shipped this cycle, operational improvements, next), and a log of every report built. A section switched off is absent from the brief and from the file Build report hands over, because the page says the analysis below is what exports. Every figure is read from the helpers the Analyse view reads (ttrStats,weekSeries,ttrBuckets,faultPareto,recurrence,spendSection) over the console’s own comparison window, so a report cannot cover a different period from the page it came off. The section choice survives a walk to another page (localStorage, falling back to every section). -
Inventory (
#pg-supply) — Stock & catalogue: warehouse KPIs and stock running low by days of cover (.hbar), the item catalogue and the batches register. Categories & units (invVocabSections()): the registers an item is named from, item categories, item types and units of measure, each counted offINVCATitself rather than held as a second list that can disagree with the catalogue. Locations, New items. Analyse (SKUS,IVWK) reads median days of cover with below-reorder / stocked-out / critical counts, a days-of-cover distribution (thin buckets red), consumption top movers, a stock-cover weekly line, and parts that stocked out 2+ times. -
Procurement (
#pg-procurement) — its own unit on the sidebar and its own page, taken from the built console. Orders & requisitions: open requests, the urgent ones among them, orders in transit and requisitions in flight (PKPI), the orders table andinvProcSections(). It was the first tab of Inventory, which put a question about a vendor inside the answer to a question about stock. The four supply KPIs split with it: the shelf counts stayed, the order counts moved. -
Targets (
#pg-targets) — every number a unit is held to, one unit at a time off a.psegof General and the four units: the breach verdict, the targets running now and the finished ones for the units that carry them, that unit’s drivers and who leads them, the health targets each driver is read against, and its breach thresholds and step SLAs. The General tab holds the measurement rules that are not a band, and the change log under all of them. Analytics no longer has a Health targets tab and carries no tab strip at all: it is the analysis, and this page is everything the PM sets. It exists to take all of that off the unit pages, which were carrying a target band, the running cards and the finished-target history above the work the page is actually about. It is not called SLAs: a step SLA is hours on one transition and lives as a section inside this page, while a delivery push of 5,000 conversions by 31 August is not a response time, and the console settled on one word for the thing a persona is held to. -
The breach row moved here too, so a unit page no longer opens on two KPI bands. Each unit page now opens on its own band, and the verdict reads beside the target that produced it rather than four pages away from it.
-
What stayed on the unit pages. New target, in the page head, because that is where you are when you decide to set one; saving lands on Targets with that unit selected (
goTargets), so the record you just wrote is on screen rather than on the page you raised it from. -
The board still scores them as one. The Overview card reads Inventory & Procurement and covers both halves of supply. Procurement has no driver pair, no band set and no card of its own there, and a fifth unit would need all three.
Teams (#pg-personas)
Section titled “Teams (#pg-personas)”Three tabs on a .pseg (pgTabs('teams',…)): Scorecards, Members, Activity. The
business unit used to be the .pseg, then a .filt-select in each table head, and is now on
none of them: the role a person holds and the place they work name the unit plainly enough, and
a column, a filter and a count all partitioning by the same word is the same fact three times.
Scorecards
Section titled “Scorecards”The signature “answer any question about any single team” surface. Four counts — teams
listed, on track, watch, at risk — over a .tbl of all nine built roles, each row showing the
team, its location, its two lead metrics and a status. The counts are of the teams on
screen, so the status filter and the search in the table head move them with the rows. The Headline and Secondary column headers carry a visible
.th-sub sub-label (Lead driver / Second driver) — a column-scoped descriptor, not a
banned floating explainer paragraph. Those two metrics are not set on this page. They come
from the business unit’s driver set on Analytics › Health targets (BUDRV), so the
metric a team is judged on is the one the PM chose to judge that business unit on, and editing
the driver set there changes every team row in that unit. Which of them leads and which is
second is named there too (LEAD / SECOND, teamDrivers). It used to be a list position:
teamDrivers floated whichever drivers the team happened to report and sliced the top two, so
the second column was whatever came next. The second is now chosen from that unit’s own
drivers minus the lead — not from every metric in the system, and never the same measure
twice — and every team in a unit is scored on the same pair, which is what makes the column
comparable down the table. Promoting the second to lead moves the second on; removing a driver
that held either role hands it to what is left. The value comes from METRIC_DEF and is coloured by its band (bandOf), so it
reads against the same green/amber/red target the PM set. A row opens a .dlg scorecard with that team’s full
KPI set, read-only — no jump into their console, since access is role-based.
Members
Section titled “Members”A scorecard names a team; the roster names the people on it. Three counts — members, roles and
places — over a .tbl of every member sorted by
business unit, then the unit lead, then role seniority (UNIT_ORDER, UNIT_LEAD,
UNIT_ROLES): Member (name, .badge Lead for the head of a line, email on a .cellsub),
Role (staff id on a .td-sub), Assigned to — the first place that member covers with a
+N for the rest (locsCell), the record listing every one — and Reports to. One list of
places (PLACES): the inventory locations, named as Inventory Manager names them, plus the
cluster a field technician covers, so a
person cannot be posted to a place the Inventory page has never heard of. There is no “all
clusters” or “all warehouses”: a manager covering everything ticks the places they cover, which
is what multiple assignment is for. A member holds one or more places: a cluster
manager covering two bays and a store keeper covering two warehouses were always that, and the
roster now says so. There is no joined
date and no added-by: those are staff-record facts the company system holds, and the trail
already carries who added whom. Reports to follows the org chart (ORG_LINE,
reportsTo): a technician reports to the supervisor at a bay they share, a supervisor to
the cluster manager, a manager to the head of that line, and the head of a line reads as an
em-rule. The chart is a line of roles, so the business unit is not part of the member model at
all — the role carries the line, and ROLE_ORDER carries the order the roster sorts in. Filters: search, role, clear. A row opens a .dlg
record: the member’s fields, the trail rows they are the actor of, and Change assignment
(memMoveDlg, memMove) — staff move between bays and sites far more often than they change
role, so the assignment is the field the roster edits. The places offered are the ones that
member’s business unit runs, the dialog shows who they will report to at the new place before
it is saved, and the move lands in the trail.
Activity
Section titled “Activity”The trail of what the console did: When (day, date and time), Who (name, role on a
.cellsub), Action, Detail — the record behind it. One function writes it
(logAct), and every action that changes a record calls it: a case resolved or a reminder
sent, a target set or voided, a measurement rule moved, an investigation opened or resolved,
a new item approved or rejected, an announcement pushed, feedback answered or reopened, an
approval level changed, a member added or moved, an item edited, and every export. A trail
that only three actions wrote to was a seeded list with a live corner. An entry can name a
subject, so a record reads its own history out of the same log (trailFor) rather than
keeping a second one: the item record’s Changes tab is the trail filtered to that SKU. There is no module column: the
action and the record it names already say where a thing happened, and a badge repeating it
narrowed on a word the row carried twice. Filters: search, a from/to date range, clear.
Three counts — actions, people, today. The rows open nothing, so the table carries
data-canon="allow-static-rows": a log of what happened has no record of its own to open,
and the record it names is reached from its own page.
Adding a member
Section titled “Adding a member”Add team member in the .pg-actions opens the flow the Retrofit Station Manager and the
Production Manager already use, and the one the Head of Maintenance uses to add staff
to a bay. The pool is staff who hold no seat on this console (POL_STAFF, the staff record
Polaris keeps); searching it matches name, role, staff id or place, and picking a person
populates the rest — the role and the places they work come from their record rather than
being typed a second time. Role is a select over the whole line of roles and Assigned to is a
multiple choice over every place the operation runs (placePicker, placesPicked); changing
either repaints the reporting line, so what the card will say is visible before it is saved.
Change assignment on a member’s record is the same control. The add moves
the person out of the pool, into the roster, and into the trail as Added a team member under
the Teams module, which is what a bay’s audit records when the Head of Maintenance assigns
staff to it.
Inventory locations
Section titled “Inventory locations”The console could always answer where is this part — an item record lists every holder — but
never what does the retrofit station hold, which is the question asked before a build, a
repair or a stock push. Inventory locations (invLocSections, locStat, openLoc) is the
other way in: every location with who holds it, the units on hand, the serialised units on hand
and its last movement, over the counts for the set on screen. The business unit and the lines
held read on the location’s own record, not as columns · the name of a place says which unit it
serves, and a count of lines is a total, not a fact about the row.
Filters: search and clear · six rows on one dimension the table no longer shows do not earn a
select. A row opens the location — a record card, the items on
its shelf (item, category, on hand, serialised, last movement) and every serialised unit traced
to it. Both readings come from the same holdings (THOLD), so a location and an item can never
disagree.
The word is inventory location, and the names are the ones Inventory Manager holds (Procurement & Inventory §4, the source of truth for them): each location has its own warehouse and there is no central warehouse, a facility is the site that raises requests and receives stock, and a warehouse and a facility can share a site name without being the same thing. So: Ikeja warehouse, Sango warehouse, Sango facility, Sango retrofit station, Honda bay, Metro bay, Ibadan warehouse. Not store, and no invented central one. Maintenance keeps its own words, since a bay and the Ikeja cluster are the maintenance domain’s vocabulary rather than the requester-site vocabulary that document defines. Item units come from the same place: pcs, each, roll, metre, litre, and nothing else is offered.
Tech feedback (#pg-feedback)
Section titled “Tech feedback (#pg-feedback)”Carries the same queue treatment as every other table: a .kpi band counting new, in progress,
resolved and dismissed, then a status filter in the table head, an app filter across the six
apps feedback arrives from, a type filter, search that also matches the area, a from/to date
filter, a clear and a live count. The status card row that used to sit under the band is gone: it
counted the same four states a second time, and the filter belongs where the other filters are.
Inbound feedback from the people using the apps, submitted through the in-app feedback form in
each app and typed at source: what kind it is (Bug or Improvement), where in the app
it happened (the app plus the screen or area), and how much it hurts (Blocks work / Slows work /
Minor). The table shows Type and Where as columns, the head counts the bugs and how many
are blocking, and the record leads with type, where and impact before the note. Submitted (Atlas, Forge, Polaris, Rider,
Inventory, Kit Production). A new submission raises a notification and emails the PM
on arrival (logged on the record), and every triage action then emails the sender (a
real send at launch).
Each item carries the sender’s name + email, the app it came from (Atlas, Forge,
Polaris, Rider app, Inventory app, Kit Production app), the received date and their
note. A four-tile .kpi band partitions by status (New / In progress / Resolved /
Dismissed), a .pseg status filter, and a .tbl with a date-range filter and search.
A row opens a .dlg with the full note and actionable controls that each email the
sender with a note attached: Acknowledge & start (acknowledgement email, note
optional), Mark resolved (note required), Dismiss (a confirm step with a required
reason, .btn-danger-out), or Reopen. Status updates live, the KPIs recount, and the
sent reply is shown back on the record.
Announcements are the outbound half of feedback. Resolving a report answers the one
person who filed it; everyone else who hit the same thing finds out by hitting it again. Composing lives at the top of Tech feedback (New announcement in the page head, beside an
Announcements button), and previous announcements are their own inner page (#pg-announce,
openAnnouncements) — reached from that head with a .pg-back, never from the side nav. A
resolved report offers Announce the fix (prefilled title and message, annDlg(i)), and the audience is a choice with a number on it — everyone on the app the report
came from, only the reporter, or every console and app — reach read from per-app user counts
(APP_USERS), so “pushed to 64 people” is a count, not a mood. The push is an in-app
notification, not an email, and the prototype proves it: it lands unread on this console’s own
Notifications page. Every push is recorded — what, about which report, to whom, how many, by
whom — and the report carries its announcement id, so a fix cannot be announced twice by
accident (the button reads Announced · AN-43 after).
Analytics (#pg-analytics)
Section titled “Analytics (#pg-analytics)”A proactive-management view — analysis the cockpit doesn’t give, not a repeat of the
Overview trends. It opens on four counts — drivers tracked, on target, watch, at risk —
across the four units. A drivers-vs-target .tbl groups every business unit’s health drivers
(value vs target, RAG-scored) under the unit — each shown once (name + overall status) via a
row-spanning cell, units ordered worst-first, drivers worst-first within — with an
off-target count, a business-unit filter and a band filter, so slipping units surface
at the top and a reader can take one band at a time. Open requests
by business unit (.hbar) closes the page. Work orders by fault class moved to Maintenance
and the by-site table is parked off Production for now — each chart reads on the unit it concerns,
not on Analytics; only genuinely cross-cutting reading stays here.
A .pseg switches between Analysis (the above) and Health targets — the config
surface, in two parts. Business unit drivers: each unit lists its drivers as removable
chips with an Add driver picker, so which live metrics feed a unit’s computed health is
chosen, not a fixed three. Bands per driver: each metric defines its green, amber and
red bands as explicit low–high ranges — uniform layout, red recorded like the others, no
≥/≤ (a value falls into whichever range contains it). Bands are one set per metric
(METRIC_DEF), so a shared driver reads the same everywhere; DHEALTH scores each unit’s
value with bandOf and the worst band wins. Every edit recomputes the RAG live across the
console (this analysis table, the Overview business unit health board and its breakdown
dialog). This is the one place the health drivers are configured.
Requests (#pg-field)
Section titled “Requests (#pg-field)”A four-tile .kpi band and the open-requests .tbl — request, location, requester
with their address on a .cellsub, priority, date, status. The name and the email are
one fact about one person, so they are one column: split across two they clipped the address
and made a reader read a person twice. Where a
request is wanted is what Procurement acts on: a bay waiting on brake pads is a different job
from a warehouse topping up its shelf, and the business unit behind the requester says neither.
The table carries a location filter read from the rows (reqLocs) and a priority filter
alongside the date filter and search · the request type is neither a column nor a filter, since
the two types the queue holds are named in the tile band above it — the queue of what the field is asking for. Types at launch: parts
replenishment (Production / Retrofit / Inventory to Procurement) and parts requests
(repair parts from the Head of Maintenance to Inventory). (Console adoption lives on the
Overview, not here.)
Reporting & shell
Section titled “Reporting & shell”- Reports (
#pg-reports) — one operations report, every line computed from the records on the console at render time, against the targets the PM set on Targets. It opens on four numbers — health score (the four units averaged, banded), units on track, measures breached of the sixteen with a target, and parts spend against budget — and then five tabs, each a.tbl-cardwith the verdict as its head badge: Business unit health (band, score, lead driver vs target per unit · head badge is the overall band, worst unit wins), Breaches against targets (all sixteen PM-set measures with value, target and a Breached/Within badge · head badge counts the breaches), Speed · start to finish (per-unit cycle counts, typical and slowest-10% over the comparison window), Spend on parts replacement (top parts priced, total and window in the card sub, budget verdict in the badge,fxNote()when converted), Open queues (every queue with its open count and the detail that matters · behind, P1, past-SLA). The audit trail is not on the report — it reads on the Targets views, where the rules are edited. The windowed figures derive from the comparison window set on Targets · General, so the head carries no date picker — a control that filters nothing never ships. Export report downloads the same rows as one CSV (exportReport()→exportCsv, Section/Item/Value/Detail). No section toggles, no static doc list, no sign-off — never a brief compiled from seeded sparks. - Notifications (
#pg-notifications) — cross-business-unit.ntf-rowfeed (reliability, stock, requests, tech feedback, production). Each row clicks through to the relevant page and clears its unread dot; a Mark all read action clears the rest. - Settings (
#pg-settings) — canonical two-pane.settings-layoutaccount panel.
One case population, and one number per measure
Section titled “One case population, and one number per measure”The page carried two case sets: the cases a reader can open from the table, and a
separate history behind Analyse. “Cases open 8” and “Open cases 16” sat one above
the other, each true of its own set and neither checkable against the other. The
history is now the history (every record in it closed) and the cases still
running are the ones in the table, mapped into the same shape and read into the
set at render time, because a case’s status is decided further down the file than
the seed. One population: the analysis measures a history that contains the cases
a reader can open, and every open count goes through mcOpenN() rather than
each card counting its own set.
The median time to resolve is computed, not carried. The driver and the tile
said 17.4h while the records computed 2.1d, so one measure read three ways. The
seeded repair durations were spans of up to 96 hours (150 for throttle), which is
a fortnight of bay time and not what this console describes; they read 4 to 46
hours now, and the driver, the tile, the unit card, the scorecards and the
Analyse card all read that computation (syncTtr), in whole hours, so the same
number cannot appear twice in two roundings. Maintenance stays in the amber band
its health story is built on.
The scorecards read the computation too. The median, the repeat rate, the
open case count and the open request count were typed into eight team
scorecards, the business unit card and the tiles, so the same figures existed in
a dozen places and drifted the moment the records changed. syncFigures() writes
them by label. One scorecard had also come to list the median twice.
Every KPI band is as wide as the cards it holds. The maintenance band had five cards in a six-column grid, so the sixth slot sat empty; each band now reads its own length instead of carrying a hardcoded column count, which is how a band gains a card and keeps a stale grid.
Every filter filters, and offers only what the rows can hold
Section titled “Every filter filters, and offers only what the rows can hold”Driving every option of every filter on the console (37 of them) turned up three that changed nothing and five tables that should have had one.
Three that did nothing. The parts-replaced table had a search box its rows
never read. The kit log’s state filter repainted Production while the log sits on
Retrofit, so nothing moved: a filter repaints the page that mounts its table,
and TR_OWNER now names every page that mounts one, because a missing name is a
filter that changes state and leaves the rows alone. And every kit was seeded
Fitted, so the state column read one word on all eleven rows and the filter
had nothing to separate; three kits are assembled and waiting now, and a few
units have not been through QA.
Five that were missing, each on a dimension its own rows carry: a state on the bundle log (the same dimension as the kit log beside it), cover status on the catalogue (stocked out and under cover are what a reader opens it for), retrieval on Riders & vehicles, bay on Open cases, and request type on Open requests.
A filter never offers a value the rows cannot have. The option lists for
parts categories, case bays and request types are read from the data
(partCats, mcBays, reqTypes) rather than typed out, so a new category
cannot appear in the rows and be missing from the filter.
One control that only looked like a filter. The approval-levels table put a
.filt-select in every row, so a table of settings read as a table with four
filters on it. Setting how deep an approval goes is a form control, and it takes
the form select.
Three states the console counted but never had. No case was blocked, no team was at risk, and each unit’s investigation register held a single row. One case is blocked on a part, one team is at risk, and Production’s register has a second investigation, resolved with its cause and action. What remains empty is a real answer: no P3 case is open today, and nothing sits between stocked out and the cover target.
One vocabulary per state. The team badges said on shift, active and on track for green and attention for amber, while the filter beside them offered on track, watch and at risk. They all say the filter’s words now.
Every date reads day, date and time
Section titled “Every date reads day, date and time”“17 Jul” and “Sat 20 Jun 2026” are both unreconcilable against a paper record:
one hides the year, the other hides the clock. One formatter renders every stamp
as Sat 20 Jun 2026, 14:12 (trDate, with trIsoWhen for the seeds that
carry an ISO day), and trStamp is now the same function rather than a second
one that appended its own clock. Where a seed carries only a day, the time is
derived from that day (trClock) so it is stable across renders instead of
printing 00:00, which reads as missing data.
Two things keep the day alone, and only these: a period (a target’s window,
a By day bucket, a chart axis) because a period is not a moment, and a date
input. A target window also names its year now (gWindow) — “1 Jun to 31
Jul” left the reader to assume it.
The analyse sets used to carry their open date as a bare “2 Jun” display string;
they carry the moment (openedMs) and the formatter renders it, so the repeat
tables and the evidence tables read like everything else.
Dialogs are 620px
Section titled “Dialogs are 620px”Every dialog was 440px, narrower than the canon 540px, so a record with a note
and an audit trail scrolled inside itself. At 620px the feedback record — the
worst case — fits without scrolling. There is precedent in Atlas for a wider
dialog: procurement-manager carries a .dlg-wide of its own at 680px, a class
this console does not use · the width is set on .dlg here.
The wrench is Maintenance, and Retrofit is a tricycle
Section titled “The wrench is Maintenance, and Retrofit is a tricycle”Retrofit carried the wrench in the sidebar and on its business unit card, which
is the one icon a reader will always read as maintenance, while Maintenance
carried a tick in a box. Maintenance takes the wrench. Retrofit takes the
Rechaj icon set’s own tricycle rather than something invented here: that set
draws filled outlines rather than stroked lines, so the glyph is painted
(.ds-glyph path) — stroked, it renders as a blob. It is stored once and
dropped into the sidebar at boot rather than pasted into the file twice. The
parts-request tile that also used the wrench takes the box it draws from.
The catalogue reads what the Inventory Manager reads
Section titled “The catalogue reads what the Inventory Manager reads”An id is not what a reader scans a catalogue for, so the SKU column is gone. In its place: the category, the tracking and unit beneath the item name (Serialised · pcs, the Inventory Manager’s own pattern) and when the item was last counted beneath its status. The SKU stays on the item record, where it is identity rather than a column.
Every page and tab opens on its counts
Section titled “Every page and tab opens on its counts”A tab that goes straight into a table makes the reader count rows. Four tabs did: the vehicles, the catalogue, the new item requests and the parts replaced.
- Vehicles and their kits — vehicles with a kit fitted · retrofit complete · up for retrieval · kits reassigned. The last two are the Retrofit Station Manager’s work read from here: a keke sent for retrieval and a kit taken off one keke and refitted to another.
- Item catalogue — items held · on-hand units · under the cover target · stocked out.
- New item requests — requests · awaiting a line manager · approved · rejected.
- Spend on parts replacement — parts replaced · units drawn · spend · biggest single
part, from the same aggregation the table reads (
spendSum), so the cards and the rows can never disagree.
Retrievals read as a fact about the vehicle
Section titled “Retrievals read as a fact about the vehicle”A keke is sent for retrieval by the retrieval logistics team, marked arrived when the pickup agent drops it at the retrofit station, confirmed by the station, and completed when the kit comes off. The station owns the last two marks (that is the Retrofit Station Manager’s Kit retrieval); this console reads all four states, because a PM reads the fleet from here.
The vehicles table carries a Retrieval column — the stage as a badge with the retrieval id and the reason it was raised beneath it, or “not up for retrieval” — and the vehicle record carries a Retrieval tab with every mark and who made it: sent, arrived, arrival confirmed, retrieval completed. Kit reassignment was already on the record as the kit history; it is now a number on the page as well.
An on-hand figure is only as good as the last count
Section titled “An on-hand figure is only as good as the last count”The item record carries a Stock counts tab: what the record said, what was on the shelf, the variance, who counted it and why (cycle count · month-end count · spot check), newest first, because the last count is the one the on-hand figure rests on. The Inventory Manager calls the same log the quantity change history and a count is one kind of line in it.
Tables, detail views & filters
Section titled “Tables, detail views & filters”Every table carries a search box, an Export and, where it has one, a
status/priority filter and a date range, with a live count in the head (setCnt
renders “N of M” when filtered, “N total” otherwise). When any filter or search is active
a Clear control appears in the table head (clearBtn + tglClear, one per table) and
resets that table’s search, filter and count in a single click.
Search and Export are wired from the table, not written per card (tblWire, run over
every .tbl-card as pages render). A card with rows gets an Export; a card with more than
five gets a search box and a row count; a card that already has either keeps its own. So a
table added later cannot ship without them, and the hand-written ones stay as they are.
A record’s own field list is not a table at all: it is a description of one thing, not a list
of rows, so searching it or taking it away as a file would mean nothing. trDet renders it as
a record card (.rec-card, .rec-rows, .rec-row, .rec-row-ic, .rec-row-label,
.rec-row-value): an uppercase section label, then one row per fact with a leading icon
chosen from what the label is about (recIcon · an id, a date, a person, a vehicle, a part,
money, a risk, a state, a place, a note, a reading), the label in secondary text and the value
set right, any qualifier reading inline after it. Every detail page on the console — case,
work order, item, kit, batch, rider, request, order — is built from that one function, so
they change together.
tblExport reads the rows on screen — header text, visible cells — so what a reader
has filtered to is what the CSV holds, and tblFind narrows the same rows the export
reads. Export is CSV and nothing else.
Export sits on the table it exports. Five pages carried it in the page head, where on a page with three tables it could only mean the first one; the button and the five hand-written exporters behind it are gone. The two page-level exports that remain are whole-page documents: the Overview snapshot and Export report on Reporting.
Every row opens a read-only detail — a centred .dlg (rowDetail) showing the
record’s fields and an audit trail. Each audit entry carries the name and email
of the person who took the action alongside the action and its timestamp (auditList
renders four-part entries), so who did what is legible without leaving the record. Tech
feedback details carry the same trail: submitted-by (the sender’s name + email) plus every
triage action taken by the PM.
Canon notes
Section titled “Canon notes”-
A status pill covers its own text:
.badgecarrieswhite-space:nowrap;flex:0 0 auto, so a multi-word pill never wraps and never gets squeezed by its flex siblings. Ratcheted into the lint asweb-badge-wrapsafter being raised twice as design feedback. -
Detail always opens on interaction — a centred
.dlgfor KPI detail, team scorecards and row detail. There is no side drawer. -
Rankings render as
.hbarbars, never bare digit columns; everylineCharthas a hover target feeding the shared.chart-tip. -
A table whose rows open nothing declares it. Three do, each
data-canon="allow-static-rows": the operations report (a read surface — the figure’s own page is the record), the Teams trail (a log of what happened, and the record it names is reached from its own page) and the approval-levels table (the select in the row is the interaction). Everywhere else a row opens the record it names, and the runtime probe enforces the difference. -
Read-only stance: work-order rows toast “plan & monitor” rather than exposing operating controls, matching the PM’s authority.
-
Left-nav
.sb-countbadges are reserved for statuses that need the PM, never plain counts. Only four carry one — Maintenance (immobilised · safety), Inventory (critical items), Requests (urgent · open) and Tech feedback (new · untriaged) — each computed (sidebarBadges()), grey, and hidden when zero. No badge on Teams, Reports, etc. -
Every vehicle table carries a Rider column immediately after the plate, matching the Head of Maintenance canon —
riderOf()resolves the plate andplateCell()emits the plate + rider cells in the maintenance and retrofit tables; each work-order/case detail also lists the rider. -
Count distributions read over their total (board-wide) — every count-distribution
.hbarvalue showsn / total: work orders by fault class, open requests by business unit, and production output by line. (Days-of-cover is a per-item measure, not a share, so it keeps itsdunit.) -
Overview KPI tiles name their source business unit — each north-star
.kpicarries a.kpi-butag (e.g. Fleet-ready → Maintenance, Days of cover → Inventory). Only the Overview band setsbu; the per-unit domain pages don’t (they’re already scoped). -
The page date range has a Reset —
dateRange()shows a Reset button once a from/to is set (resetDates()clearsGFROM/GTO), alongside each table’s own Clear control. -
Important tables carry a status-summary card row (board-wide,
statCards()) — one.sumcper state above the table, each carrying the state and a toned icon on its top line (.sumc-h,.sumc-ic), the count set large beneath (.sumc-n) and a line saying what the count is of (.sumc-s). The card is the filter, and the state in force carries the selected border. Maintenance cases by status (all, open work, open, in repair, blocked, resolved, closed); Production and Retrofit by Behind / On track; Inventory orders by In transit / Shipped; Tech feedback by queue status. A count is a number a reader acts on, and it used to sit in a chip the size of a filter pill with nothing saying what it counted. -
Every trend line chart carries a Day / Week / Month granularity toggle — a compact
.pseg.pseg-smin the chart’s card head (granToggle()/drawTrend()over theTRENDSconfig). Week is the date-range-windowed weekly series (trendChart); Day and Month swap in their own series with their own axis labels. Each chart shows a running total for the current view (.ch-total), recomputed on every toggle or date change.
Target cards at working scale
Section titled “Target cards at working scale”A live target renders as a compact card — stats at table scale (16px numerals, .gcard .mstat-n),
a tighter bar, and two live targets sit side by side in a .grid2 — because a target is one
line of work, not the page’s headline. A 2×2 stat grid was tried and measured taller than the
four-up at half width, so the four-up stays.
Three tabs per unit
Section titled “Three tabs per unit”Monitor was one endless scroll, so each unit reads in three tabs, split by what belongs
together (unitSeg/mtViewSeg, UVIEW[key] = monitor · more · analyse): Production —
Work orders · Output · Analyse; Maintenance — Cases · Parts & spend · Analyse;
Retrofit — Work orders · Vehicles & kits · Analyse; Inventory — Procurement · Stock &
catalogue · New items · Analyse (invProcSections/invStockSections/invNewItemSections
· unitSeg maps a third label onto the extra view). The breach row stays on the first
tab — it is the glance layer, not a section. Requisitions stay procurement-side vocabulary:
purchases raised on Procurement, never requests to procurement or to inventory.
Records, not dialogs
Section titled “Records, not dialogs”Back walks the trail. Records open from records here: an item leads to a serialised
unit, a unit to the batch it came in on, a batch to the kit it fed, a kit to the keke it
went on, a keke to a case. Back went to the page the reader started from, so four clicks in
was one click out and everything between was lost. Every opener registers itself on a stack
(trailEnter · trailBack · trailLabel) with a key, a label and a way to reopen itself,
and the button reads where it goes: ‹ BT-118, then ‹ MOC-X-SAN-260523-0055, then
‹ Cable fuse 80A, then ‹ Inventory. Reopening the same record (a filter inside it)
replaces the top of the stack rather than growing it, the stack caps at twelve, and choosing
a page in the sidebar clears it, because that is a new journey and not a step back.
A record longer than a screen reads as tabs. Six stacked sections put the serial a
reader came for two scrolls below the fold, so every record over about a screen carries a
.pseg strip and shows one group at a time (pgTabs · pgTabGo): the item record (The
item · On vehicles · Serialised units · Supply), the production work order (The work
order · Units), the retrofit work order (The work order · Units), the maintenance case
(The case · Parts replaced · Vehicle history), the batch (The batch · Every item), the kit
(The kit · What is in it · Where it has been · The vehicle it is on, when it is fitted)
and the vehicle (Rider and vehicle · Timing · Kit history · Parts on this vehicle). The
tab a reader is on survives a filter inside it — paging the units of a work order
keeps them on Units — and resets when a different record is opened. The short records
(#pg-sord, #pg-newreq, #pg-titem) stay as single pages: they fit.
A work order was a row that opened a .dlg listing the same six fields the row already showed,
and you could see the work order but not the units under it — which is what the work is
actually made of. This console cuts across every business unit and is worked in rather than
watched, so a record is a page: #pg-prwo (production work order), #pg-rtwo (retrofit
work order), #pg-mtcase (maintenance case) and #pg-sord (supply order). The old
openPrWo/openRtWo/openMtWo/openSpOrd dialogs are gone; the same names now open pages.
Every work-order record states its expected completion (pmDueCell · a seeded due day per
WO) — that date is what Behind is judged against, so a status the record cannot explain never
ships; a date already passed reads red. The record’s only action is Open investigation —
an Expedite button promised a lever the console does not have. A retrofit work order creates
units, exactly like production: the record lists the vehicles built under it — plate,
rider, kit, stage — and each unit ties to its own rider (TUNIT.wo); the work-orders table
counts units per WO instead of naming one plate, because a rider is a fact about a unit,
never about the work order. A maintenance case carries Vehicle history,
and the case seeds repeat two plates so a repeat visit is visible rather than hypothetical.
A history row opens the case it names (trDet takes a per-row click), because the
repeat is the reason to read the row.
Units on a work order. WOUNITS gives every production work order a row per unit — serial,
state, step, who has it, started, closed — searchable, filterable by state and paged, with a
Units by state rollup beside it. The unit count and the closed count are derived from the
row’s own progress, so the record can never disagree with the list it was opened from. A
retrofit work order is one vehicle, so it shows its conversion steps and its lifecycle instead of
a units table, and both cross-link into the Traceability spine: the kits built under that work
order, and the serialised parts on that keke.
The case record carries the fleet’s vocabulary and its actions. Status (Open · In repair · Blocked · Resolved · Closed), stage, ageing in red while it runs, and complaint separate from fault — the complaint is the note the case arrived with, the fault is what was selected on Argo at triage. Mark as resolved takes a required resolution note; Remind Maintenance Supervisor is offered only while the case sits on a Maintenance Supervisor gate.
It opens on open work. The case table defaults to Open work — Open, In repair and Blocked —
with Resolved, individual statuses and Every case a click away. Three seeded cases are
terminal, because without them the default would filter nothing and could not be told apart from
no filter at all.
No Line column. A line is an attribute of the unit, not a column on five tables; the header and its cell came off together.
Traceability
Section titled “Traceability”Every other page on this console reports a rate or a duration. None of them could answer a question about one thing: what is in this kit, which batch that part came from, whose store it was collected out of, whose keke it is on now, or how long that keke took from inspection pending to retrofit complete. That is the nitty-gritty the console was missing, and no amount of rate reporting substitutes for it.
One spine answers all of it, and every id in one table resolves in the others: a batch
receives serialised items into a store, a kit claims at most one item per line out of
those stores, the kit is assembled against a work order and fitted to a vehicle, and that
vehicle walks a lifecycle with a timestamp per stage. There is no Traceability page — the
spine’s tables live with the unit that owns them: the kit log on Production, Riders & vehicles
on Retrofit, and the item catalogue and batches on Inventory — holdings and the serialised
units read on the item record (#pg-item: who is holding it, vehicles carrying the part,
every serialised unit), never as their own tables on the stock tab, because they are facts
about an item. An id
is the link: the same kit opens the same record (#pg-tkit) whether it was clicked from a work
order, an item, a batch or a rider, and trBack/_trFrom return the reader to whichever unit
page they came from. Related tables read inside the record they hang off, never stacked
beside each other — a rider’s kit, the items that made it up, the batches those came from and
the retrofit legs are all facts about that rider, so they are on the rider (#pg-trider), and
each unit page keeps one table of work.
The clock starts at inspection pending. The stage sequence is the one the Retrofit Station
Manager console already uses — Inspection pending, Inspection completed, Retrofit queued, In
progress, Awaiting QA, Rework, Retrofit complete. Inspection completed was missing from the
first cut, which measured two stages as one leg. trUnitStageRows builds the stage, its stamp
and the gap since the previous stage from one list, so a leg can never be labelled with the
wrong stamp, and the legs sum exactly to the total rather than approximately.
A kit claims one item per line. Items are handed out kit-first, oldest stock first, and only
from stock received before the kit was assembled — so a kit cannot hold two of the same part,
its line count is a real ceiling, and Lines not yet in this kit means something. The first
cut matched items to kits by part alone and produced kits reading 5 of 3.
Inventory, in full
Section titled “Inventory, in full”Inventory answers the questions the page is for, not just orders in transit: the item
catalogue (INVCAT — every item that exists: SKU, category, lead time — the vendor’s
quoted delivery days for the item, read against the lead-time target — on hand, usage per
week and days of cover; the vendor is named on the record, never a table column; a row opens
#pg-item with who holds it, the vehicles carrying the part, every serialised unit, its
requisitions and its batches — stock health lives in this table, not beside it: the Cover cell carries
the days and a bar against twice the cover target (coverCell), a stocked-out item says so as
its status (coverBadge), the rows sort thinnest-first, and the card sub-line counts the
stocked-out and under-cover items; the separate hand-seeded health card is gone, because two
seeds answering one question is how a page disagrees with itself), requisitions in flight oldest first, and where the time goes
(PROC_CYCLE, procStages, timeGoesCard) — the median days a requisition spends being
approved and ordered, in transit from the vendor, and being received into a location, with the
longest stage carrying the amber bar. It replaces a vendor-by-vendor quoted-against-actual
table, which restated the lead times the item records already carry and answered a question
about vendors rather than about the pipeline. The stage lengths are derived from the received
batches and each vendor’s own lead time, so a stage cannot be typed. Catalogue on-hand is summed from the
same holdings the stores report, so the two cannot disagree. An item can be edited from its record
(itemEditDlg, itemSave): its picture, its name, description, category, unit of measure and its
reorder thresholds — the facts that describe an item and the two levels it is watched
against, never the figures the operation computes for it. The thresholds are the two the
Inventory Manager sets (Procurement & Inventory FR-U1, FR-U4): a low threshold that
raises a low-stock alert and a critical threshold that raises a critical one, set per item
and suggested from usage — low is what the item is consumed at over its vendor’s lead time, so
an order placed there lands before the shelf empties, and critical is half of it. Same heading,
same words, same meaning as the console that owns them.
Status reads the thresholds, cover reads the days (stockState, underCover): the badge on
the catalogue says stocked out, critical, low or in stock, from what is on the shelf against
that item’s own levels, and the cover column keeps days of cover against the console’s cover
target. Two questions, two numbers, one badge each — and the page counts the alert once:
the counts card reads items stocked out and items at a threshold, the catalogue sub-line reads
the same, and days of cover is left to its column and to Analytics. It used to answer “is this
item in trouble” twice, once on thresholds and once on cover. The Critical items headline counts what is
stocked out or at its critical level, so the tile and the table it drills into read the same
rule. The name is
composed rather than typed (itemName, itemNamePaint): describe, type and key specs form
it in the order the Inventory Manager’s own composer uses, the formed name reads back live, and
a name the catalogue already holds is called out before it can be saved twice; an item that was
never composed keeps its name until a type is entered. A rename walks every record that names
the item — holdings, serialised units, batches, the parts list and the kit types (renameItem)
— so the joins survive it, and the edit lands in the Teams trail. The picture is read in the
browser and shown on the record beside the title, with initials standing in until there is one.
New item requests get their own
tab (NEWREQ, opening #pg-newreq with the case a line manager decides on — why, industry
standard, where held, quantity — and Approve/Reject), with the approval-levels card beside
the queue it governs.
SLAs and breach targets
Section titled “SLAs and breach targets”A status lifecycle with no clock on it is a list of words, so every transition a record can
make carries an SLA (SLA, hours per step, per unit — open to closed is the example shape,
each unit keeps its own vocabulary). Stage tables carry an Against SLA column with the
verdict per leg (slaVerdict), and the four-up row of breach KPIs (unitBreachKpis) heads
the selected unit on Targets — the card turns red and says Breached with the target it
broke, because a KPI that only reports a level makes the reader do the comparison the target was
set to stop. It sits directly above the thresholds it is judged against, so the number that
produced a verdict is the next thing on the page.
Nothing may call itself breached against a number the PM cannot move: every threshold lives in
TGT, every step SLA in SLA, and both are set on Targets (#pg-targets), a unit at a
time (tgtEditor, slaEditor), beside the health band each driver is read against
(bandsEditor).
Unit health · band, score and trend
Section titled “Unit health · band, score and trend”Two questions, two computations. Is anything wrong? is the band — the worst of the unit’s
drivers, because a breach averaged against three healthy numbers stops being a breach. How far
off are we? is the score — days, percentages and days-of-cover cannot be averaged as they
stand, so each driver is first scored against its own target as a unitless attainment ratio
(mAttain, direction-aware: a floor metric divides one way, a ceiling metric the other), then
averaged with the lead weighted ×3 and the second ×2 (unitScore). The score is a
snapshot, never a line: a trendline of a weighted composite has an axis nobody can name — it
was asked about twice and answered nothing, so it is gone. Trend reads per driver, in the
driver’s own units (mRawDelta): Fleet-ready carries +14% · 8 weeks, time-to-resolve
carries −0.6h · 8 weeks, a zero-target count reads steady, and the chip’s direction is
whether the driver is improving against its own target — so a falling repair time reads as
up. The card reads: band, then each driver with its band dot and own-unit trend chip
(cardDrivers), beside a ring carrying the score (healthDonut). The ring is one segment per driver
(drvShares), each as long as that driver contributes and coloured by its own band, and each
driver row carries its share of the score as a number — so the card says which driver is
holding the unit back, not only by how much. A contribution is the driver’s weighted
attainment, so the lead counts three times and the second twice; the shares sum to the score,
and largest-remainder rounding keeps the printed shares summing to the printed score rather
than to 99 or 101. A driver beating its target cannot carry one below its own, so attainment
caps at the target (unitScore): 100% means every driver is at or above target, and there
is no reading above it. The driver values and their targets used to print as two columns of
numbers on the card, which asked the reader to compare three pairs before knowing whether the
unit was ahead; the ring answers that in one shape, and each driver’s own value, target and
band read on the health dialog behind the badge. The Analytics unit cards read identically.
The console fits the screen it is on
Section titled “The console fits the screen it is on”A page that scrolls sideways as a whole is broken; a table that scrolls inside its own
.tscroll card is not. Every page used to push about 300px past a phone viewport, because the
sidebar held 248px at any width and the bands held their column counts. Three steps now:
below 1100px the sidebar collapses to its icons, which the shell already knew how to do
(body.sb-collapsed), and the top bar tightens; below 900px the KPI band folds to two
columns, the summary bands to two, the two-up record grid to one, and the top bar drops the
scope pill and the user’s name, which repeat what the page already says; below 640px
everything is a single column, the search boxes lose their 200px floor and the page padding
comes in. Measured at 390, 768, 1024 and 1440: no page overflows its viewport.
Where the keke was last seen
Section titled “Where the keke was last seen”RadR reports a position with every trip and every swap, so a record can say where the vehicle
was last seen and when. The case record and the rider and vehicle record both carry it
(lastPos, lastPosCard): the place, how it got there (signed in at a bay, marked in for
retrieval, on its route), when it was reported and how long ago, the plate, the rider and the
source. On the case it fills the column beside triage and timing, where a reader was looking at
half a page of white and asking exactly that question.
A bay is called the same thing everywhere (bayLbl): the case data holds the short name, and
the display says Honda bay the way the roster and the inventory locations do.
One set of parts, one cover target
Section titled “One set of parts, one cover target”The Analyse tab used to run on its own seeded list of ten parts — a motor controller, a front
fork, a brake lever set — while the catalogue, the holdings, the batches and every count ran on
the real one. The cover distribution, the top movers, the repeat stockouts and the Parts below
cover driver all described items a reader could not open and could not find anywhere else.
skuSet() is the catalogue now: cover is the item’s cover, consumption is its own per-week
draw, a repeat stockout is derived from the item’s record (skuOuts), and every row on those
cards opens the item it names.
Days of cover had two targets as well: the SLA the PM sets on Targets and the green band on the
health metric, so an item could be thin on one page and covered on the next. The SLA is the
target (syncCover): the metric’s bands are set from it, the analysis reads it, the driver
counts the items the catalogue badge calls thin, and the metric’s own value is the typical item
rather than a number nobody set.
One window for every chart
Section titled “One window for every chart”widx() is the single chart window: the From/To range when one is set, otherwise the last N
weeks the comparison-window setting names — the same anaWeeks() the case analysis uses, so a
chart and a rate can never quietly measure different periods. The KPI dialog cuts its sparkline
to that window and titles it with the count it actually drew (4-week trend), never a
hardcoded thirteen.
Maintenance reads like the console it reports on
Section titled “Maintenance reads like the console it reports on”The Maintenance page carries the same reading refined on the Head of Maintenance console: a
status card row that partitions the cases (mtCards — All · Open · In repair · Blocked ·
Resolved · Closed, summing to the total, each card filtering on click), the table titled
Cases with the open-work default as a filter rather than a truncation, and the case record
(#pg-mtcase) with complaint separate from fault, ageing in red, resolve and remind.
Analytics leads with the answer
Section titled “Analytics leads with the answer”The Analyse tab opens on the four units scored against target — a card per unit with band,
score and trendline — then the drivers behind those scores worst-first, and nothing else. Open requests by location moved to the Requests page it counts
(reqByLocCard), where a bar sets that page’s own location filter instead of navigating to the
page it was already describing. The Targets tab reads one unit at a time (tgtTab, a .pseg — General · Production ·
Maintenance · Retrofit · Inventory): pick a unit and everything the PM has set for it reads
together — its drivers and who leads, the bands for exactly those drivers, its breach targets
and its step SLAs side by side — instead of five mechanisms interleaved across four units down
one long page. General holds the comparison window. An edit re-renders in place and stays on
the unit being edited.
Items, bundles, kits · who owns which layer
Section titled “Items, bundles, kits · who owns which layer”Items are Production’s, kits are Retrofit’s. Production receives individual items and builds
bundles from them (trBundleSection, BDL- ids — e.g. a main-harness bundle); a QA-passed
bundle ships to Retrofit as an item itself. Retrofit assembles bundles and items into
kits (trKitSection, KIT- ids) and only a kit is ever fitted to a keke — the generator
enforces it, so no bundle can carry a plate. The Bundle log reads on Production, the Kit log on
Retrofit, and the Bundle log is the one bundle table — the work-order
record lists its units and nothing else. A bundle table’s column is Part, not Type — a bundle of items still creates an item, and the bundle’s name
is that item — and a finished bundle’s state is Completed, nothing more: where it went is
Retrofit’s record to tell. Bundles tie to Production’s own work-order ids and
kits to Retrofit’s, so a record opened from either end lists what was actually built under it.
Work orders read whole, worst first
Section titled “Work orders read whole, worst first”Both unit tables show every work order — the card is titled Work orders, not a behind-only list — sorted Behind above On track and P1 above P3 within each, so the worst is at the top without hiding the rest. The Investigations card head carries filters then actions (search · status · severity · dates · clear · export · open); the investigation alert period is configuration, not a filter, so it lives in Analytics › Targets › General beside the comparison window.
The rider lifecycle, as the document states it
Section titled “The rider lifecycle, as the document states it”The rider walks LEAD_CAPTURED → PENDING_VERIFICATION → VERIFIED → PAYMENT_PENDING →
INSPECTION_PENDING → INSPECTION_IN_PROGRESS → INSPECTION_COMPLETED → RETROFIT_QUEUED →
RETROFIT_COMPLETED, and the retrofit lifecycle on this console is now exactly those five
retrofit-side statuses (TUNIT: pending · inspStart · inspected · queued · done) — In
progress / Awaiting QA / Rework were this console’s invention, work-order internals rather
than rider statuses, and they are gone from the lifecycle, its legs and its SLAs. The retrofit
breach card counts vehicles awaiting retrofit instead of a rework state that no longer
exists. Retrofit’s concern starts at inspection pending, which is where this
console’s clock already starts; the rider record shows the four onboarding statuses before that
as context (riderOnboardRows — who moves each, per the doc), then the timed stages. The one
nuance stated on the record because it bites: Retrofit queued fires when the PCA marks
onboarding complete · it does not wait on inspection completed.
The cases table answers where and how long
Section titled “The cases table answers where and how long”Open cases, eight columns: Case ID with the work order id beneath it (a case id and a
work-order id are different things, and an untriaged case has no WO yet, so the sub-line is
simply absent rather than a dash); Vehicle, the plate with its rider beneath; Type
on its own, since the type is what a reader scans for and the origin is a fact for the
record; the fault with the repeat count beside it and the
complaint clipped beneath (Not selected yet when untriaged); priority · stage in one cell
as pill-plus-text (mcWord — Awaiting triage · With technician · With Maintenance Supervisor ·
Blocked · Resolved · Closed); the bay; status on its own axis; and Raised, with
the age running red beneath it and who raised it under that. Eight columns before and
eight after: the work order joined the case id and the rider joined the plate to pay for the
the type, the repeat and the bay. The status filter speaks the same words.
A repeat is seeded. Every case carried a different fault from every other case on the same vehicle, so the repeat cell read “first time” on all ten rows and a reader could not tell whether the console counts repeats at all. QQ771KD now carries the throttle fault twice, which is the recurrence the throttle investigation is about.
The case record carries what Argo records. Five tabs. The case is the case and its
triage side by side: id, work order, type, origin, complaint, fault selected on Argo,
secondary fault observed, repeat, priority, status, stage and bay · then triage state,
is the tricycle at the bay (with who confirmed it and when), raised by with the role
beneath, raised, fault captured, last update and waiting. The vehicle is the vehicle
and who is on it: plate, rider, chassis number, VCU (the serial the traceability spine
already holds, a dash where the kit carries no VCU), battery serial, kit revision,
odometer, cohort, station, bay, the kit fitted with its type · then technician,
assigned with the supervisor who triaged it, department, bay, reminded and the resolution
note. Parts replaced reads Collected · Part · Qty, the stamp carrying who collected it.
Vehicle history is every earlier case on the plate, and a row opens the case it names
(trDet takes a per-row click), because the repeat is the reason to read the row. There is no
validation gate row: the supervisor works the repair rather than receiving a submission
for it.
A stamp is a time, not a date at midnight. A case id carries only a day, so every raise stamp read 00:00 and looked like missing data; the record derives a working-hours time and fault capture and the last update follow it.
The record no longer carries the retrofit lifecycle — that is a retrofit fact. The summary cards carry real glyphs per status, not a bare circle.
One name, one role, and the source console’s name. The Head of Maintenance rosters Ibrahim Musa as the Maintenance Supervisor at Metro and Grace Eze at Honda, so this console names them too; it had a third name at Metro while also using it for the Warehouse Lead who owns Inventory investigations.
One recurrence measure, computed here. A tile once claimed “vehicles back for the same fault within 30 days” and said it came ready from the Head of Maintenance, while the Analyse rows computed something else and called it first-time fix: a unit seen before at all, any fault, no window. A repeat is now one thing — the same unit, the same fault class, inside 30 days — computed from the case records this console already holds, and the tile reads that computation rather than carrying its own number. First-time fix is its complement, shown as the sub of the same card, so two numbers for one measure cannot drift apart.
Anything raised or opened names who did it. The two repeat tables on Analyse printed a last-opened date with nobody against it: a QA fail is opened by the inspector who failed the unit, a rework by the technician who sent the vehicle back, and a maintenance case by the supervisor on that bay, the rider who called it in, or Argo itself for a service or a pack alert. Requests and Tech feedback keep the actor as its own column (requester and email, sender and email), which is the split those tables are built on, so the stamp beside it is not repeated.
Functional copy, derived trends
Section titled “Functional copy, derived trends”No explainer, provenance or process copy in the UI — the standing rule, re-applied after a
slip: card subs carry a count or nothing (trCard skips an empty sub), notes that narrated the
mechanism (“minted at Production assembly”, “only a kit is fitted to a keke”, “the rest are
secondary”) are gone; the mechanism lives in design.md and the PRD, not on the screen.
The unit trends derive from record stamps (realTrend): Production’s throughput chart
counts units completed per week from their own closed stamps (verified: chart total = closed
units, 27 of 27, with the final week open-ended so a stamp after the grid’s last Monday still
counts), Retrofit counts completed vehicles, Maintenance counts cases opened — zeros where
nothing happened. A driver’s trend chip renders only where a real weekly series exists
(time-to-resolve, weekly medians from the case records); a driver with no weekly source shows
no chip — never an invented line, never a “steady” against a target of nothing. Remaining
seeded series, on the ledger until real feeds exist: the overview KPI sparklines (the
Reports page no longer renders sparks at all · its every line is computed at render time).
Audited rules, custody, and files that download
Section titled “Audited rules, custody, and files that download”Every change to a measurement rule is a record (TGT_AUDIT, audit()): moving a target,
band, step SLA, driver, lead/second, the comparison window or the display currency lands in an
Audit trail at the foot of every Targets view — when, who, what moved, from → to — because
a threshold that moves silently rewrites what breached meant on every report before it. Who is
the Polaris session: permissions are Polaris’s, and only a Tricycle Hardware PM reaches this
console at all. Converted money names its rate: when the display currency is not naira,
fxNote() states 1 USD = ₦1,500 · rate as at <day> on the currency card and the spend card.
A bundle is serialised at Production assembly and keeps that serial into Retrofit — its
record says so. Kits get reassigned and both records show it (KIT_MOVES,
kitHistoryRows): the kit record carries Where this kit has been (fitted → retrieved with
the reason → reassigned, each with who), the rider record carries Kit history on this
vehicle, and a never-moved kit reads fitted new · never reassigned. The case record carries
Vehicle history — every earlier case on the same plate, current case left out. Export
means a file: the Export buttons on Maintenance, Requests, Tech feedback, Teams, Analytics
and Investigations download a real CSV of that page’s primary dataset (exportCsv, RFC 4180
quoting, markup stripped); Reports exports its whole report through the same helper. Announcement audiences
state that everyone-on-the-app includes the reporter.
The Assistant
Section titled “The Assistant”A floating helper (.asst-fab, brand-green 52px circle, bottom right) opens a panel
(.asst-panel) with a message thread, suggestion chips and an input. Every answer is
computed from the stores at the moment it is asked, through the same functions the pages
render with — rpBreachRows, rpSpendRows, rpSpeedRows, rpHealthRows, rpWorkRows,
the vendor and case sets — so the assistant can never disagree with a page, and never
fabricates. It does two jobs:
- Locator — “where is …” (or any bare id) resolves against serials, plates, kits and bundles, batches, catalogue items (with who holds them and how many), requisitions, new-item requests, production, retrofit and maintenance work orders, and vendors. Every hit carries an action chip that opens the owning record — the id is the link, as everywhere else.
- Analysis — a cross-unit digest (weakest unit and its lead driver, breach count with the worst measures, spend vs budget, deepest queue), breaches vs targets, spend, cycle speed, vendors over lead, unit health, open queues, and a patterns read that carries real analysis: repeat vehicles with their case sequence and day span, case mix with the top category’s share, P1 and bay concentration, parts spend pressure (top part’s share of draw spend), and ageing against the case-age target — each line derived live. The repeat-vehicles action opens the plate’s latest maintenance case, whose record carries the Vehicle history — a maintenance fact lands on the maintenance side, never on the retrofit vehicle record.
Nothing persists. Closing the panel or ten quiet minutes (ASST_IDLE) ends the
session and clears the transcript; reopening starts fresh. Copy puts the transcript
on the clipboard and Export downloads it as assistant-chat.txt — the only two ways
a chat leaves the panel. User input is HTML-escaped before rendering. The input is its
own .asst-in field (never the dormant login’s .inp), and the whole surface reads on
tokens so it holds in dark mode.
The responders are the tool surface, not the answer surface. In the prototype a keyword router picks the answer, so only coded question shapes resolve. In production the same panel runs a model over the console’s data through the Messages API with tool use — each responder here becomes a tool the model may call (locate a record, read breaches, spend, cycle times, case sets), and the reasoning across them is open-ended: ad-hoc analysis, pattern and anomaly recognition, correlation across business units, comparison over any window, follow-up questions, and why a number moved. The tools bound what it can read; nothing bounds what it can be asked.
Money, drivers, links · the polish rules
Section titled “Money, drivers, links · the polish rules”Drivers were never static — the overview cards read BUDRV, which Targets edits — but the
path was invisible, so every domain card carries Configure drivers ›, landing on that
unit’s Targets tab; an add or remove reflects on the card immediately. Money is stored in
naira and displayed in the currency the PM picks (NGN · USD · EUR · GBP, Targets › General):
one money() feeds the spend card, the priced tables, the item record and the committed-spend
KPI, so a switch moves every figure or none. The targets editors speak one type size —
13px body (.tgt-note, .bnd-lbl normalised), weight for emphasis, never size. Seeds cover
states, not length: requisitions are six rows with all four statuses guaranteed by rotation.
KPI quick links are derived, never seeded: an overview KPI links to the units named on its
own card, a unit KPI links by what its label is about, Analytics is the floor, and a link never
points at the page it is already on. Production’s last kit strings are bundles.
What a column is allowed to be
Section titled “What a column is allowed to be”A date carries who. A stamp and the person behind it are one fact, so the actor reads
in a .td-sub under the date rather than in a column of its own: the kit and bundle logs
(assembled, with the assembler), new item requests (raised, with the requester),
requisitions (raised, with the store that raised it), batches (received, with the holder
who took it in), the parts drawn table (last drawn, with who drew it) and the parts on a
case record. Four tables lost a column each that way.
A quantity carries its unit. The unit of measure rides the number, never the part
name (uomOf · qtyU): the catalogue’s on-hand, requisition and batch quantities, units
drawn, and the stock a holder is sitting on. The units are the Inventory Manager
catalogue’s own — pcs and each — not a local abbreviation.
A work order carries no severity. A P-level is case vocabulary — a work order is late or
it is not — so Sev/Priority came off both units’ work-order tables and records; Behind/On
track and the pace note carry the urgency. A production unit walks exactly three steps:
created → in progress → completed (PM_USTATE). The invented Kitting/QA/Rework step chain
is gone from the unit rows, the states rollup, the pipeline strip, the step SLAs (created→in
progress, in progress→completed) and the cycle-time wording alike.
Speed · start to finish
Section titled “Speed · start to finish”Pace against a promised date says late or not late; it says nothing about how fast the thing
itself moves. Production and Retrofit each carry a Start to finish card (cycleCard) above
their throughput chart: typical (the median — 27 finished units at 12 hr on Production, 8
vehicles at 4.8 days on Retrofit), the average beside it with the tail included, the
slowest 10% and the fastest. The durations are computed from stamps the records already
carry — a production unit’s started/closed, a retrofit vehicle’s inspection-to-complete — cut
to the analysis window like every other figure (cycleDurs), and a window in which nothing
finished says so instead of showing a stale number.
Parts cost and replacement spend
Section titled “Parts cost and replacement spend”Every part carries a unit price (PART_PRICE — an explicit naira map, so a VCU costs like a
VCU and a fuse like a fuse, never a random number). The item catalogue shows Unit cost as a
column, the item record adds Spend all round (units drawn × price), and Maintenance carries
the key KPI: Spend on parts replacement (spendCard) — the window total against a
PM-settable budget (TGT.mtSpend, edited in Analytics › Targets › Maintenance), reading
Breached · budget ₦900,000 with the overrun stated when it is over, above a week-by-week
trendline cut to the same analysis window every other figure uses — narrowing the date range
narrows the spend total and the trend together. Under it, What was replaced, priced: each
part’s units drawn, unit cost and spend, biggest spend first, each row opening the item.
Spend is computed from the same TITEM draw events every other parts number reads
(spendEvents), so the money can never disagree with the counts — the card, the trend series
and the table sum to the same naira. Mounted on Maintenance only: a draw into a kit is a build
elsewhere, but a draw out of a bay store is a replacement.
What the PM sets, and what is derived
Section titled “What the PM sets, and what is derived”A measurement rule the console applies everywhere belongs in Analytics › Targets, beside
the bands — not hardcoded in the view that happens to use it. Two more moved there:
how many weeks an Analyse view compares over (ANAWK, the trailing N of the weeks on
record, capped by how much data exists) and the lead and second driver per business unit.
Both were previously fixed in code. The window setting states its own precedence in the card:
a From/To range in the page head overrides it, because naming two dates is the more explicit
of the two statements, and the card says which is in force.
The analysis window
Section titled “The analysis window”The bands used to announce “over 8 weeks” because the seeded case set happens to run
eight weeks. Nobody chose it: the string was typed into thirteen places, weekSeries had
eight hardcoded buckets, and the case records carry a wk of 0 to 7. Meanwhile the page
head already had a From/To range that the charts honoured and the case analysis ignored,
so the console ran two windows at once.
There is now one. cwIdx() is the eight-week data span narrowed by that same From/To
range; cwFilter applies it to the case records and every unit set, cwCut trims the
chart series to match, and cwLbl/cwSpan name it. Every count, rate, cause ranking and
label derives from it, and the Analyse view states the window it measured beside the
toggle — 23 Jun to 20 Jul · 4 weeks — so the number is never bare. The chip names the
day the window closes, not the day its last bucket opens (cwEndLbl): the labels in
MWK are week starts, so ending the span on the last of them made an eight-week window
read as 26 May to 14 Jul, which is 49 days. The dates and the count have to reconcile or
the chip undermines the figure it is there to qualify. An open-case
count taken over a narrowed window says of those opened in the window instead of
right now, because over a past window it is no longer a present-tense figure.
Targets reach the people measured by them
Section titled “Targets reach the people measured by them”Inventory and Maintenance are measured by targets; Production and Retrofit by goals. The two are different questions and the consoles ask only their own. Inventory and Maintenance run continuously — there is no finish line, only a service level to hold — so those consoles carry a read-only Targets card: the band the PM set on Analytics · Health targets, their current value, and the gap when they are off it. Production and Retrofit run finite delivery pushes — 5,000 conversions by 31 August — so those consoles carry the goal cards and the finished-goal history instead, and no Targets card. Giving a console both would have it answer to two scorecards at once.
The PM still sets every band in one place, and still measures all four units against them in their own Analyse views. What changed is which of those numbers is pushed out to the person doing the work.
Production and Retrofit measure pace, not a target. Those two units answer to their
goals, so the rates band leads on Delivering to pace — delivered against where the
live goals should be by now, rolled up so a unit running two pushes still reads as one
number, green at 100% or better, amber from 90, red below. The sub-line says how many
goals are behind. When no goal is running there is no pace to state, so the cell is
absent rather than showing a placeholder. The slow-rework evidence lost its target
threshold with it and now flags the unit’s slowest 10%, which needs no target to be
true. Rework time was removed from the PM’s target list in the same pass: pace replaced
its only consumer, and a band nothing measures against is a band that drifts unnoticed.
Maintenance and Inventory keep their targets — they have no finish line to pace against.
Where the persona sees it. Targets and Goals each have their own page under Overview in the consoles that carry them, with a one-line strip left on the dashboard naming whether anything is off and which one is worst. The PM still sets every band and every goal here; those two surfaces are how it arrives.
One name: Targets
Section titled “One name: Targets”A delivery push and a standing service level are both the thing you are held to, so a persona does not have to learn two words for it. The nav item, the page, the strip and the page copy read Targets on every console that carries either. What each console measures is unchanged: Production and Retrofit still track delivery against pace, and a finished one still moves to Finished targets with its result and, where the PM voided it, the reason.
Days of cover is a rule, not a score
Section titled “Days of cover is a rule, not a score”Cover is measured per item, and the parts run from 6 to 30 days. Scoring a single
median against a 21-day rule read “slightly under” while 14 of 24 parts were actually
below it and two were a week from running out — a median hides exactly the tail that
stops the line. The old figure was also stale: METRIC_DEF carried 24d where the records
compute 18.5d.
So the two jobs are separated.
- Days of cover stays as the per-item rule the PM sets (
rule:true). It defines what counts as below cover and drivestgtMin, and it is no longer scored against a fleet-wide number, because there isn’t one. - Parts below cover is what gets scored: how many items sit under that rule, counted
from the item records by
syncCover(). Move the rule and the count moves with it — 10 days gives 5 parts, 21 gives 14, 28 gives 22. - Typical cover is not a tile. It reads as one figure in the Inventory Analyse stat row, beside the count under the cover target and the count out of stock, which are the numbers that get acted on. A descriptive median is fine there; a median on a KPI tile invites comparison with a target, and there is no fleet-wide number to compare it to. Committed spend came off Inventory with it: spend is Procurement’s measure and reads on their scorecard.
One word: target
Section titled “One word: target”The PM sets what the personas are shown, so the console says target throughout — the New target button, the Targets and Past targets sections, Finished targets, the add, edit, save and void dialogs, and every toast and alert. Voiding still requires a reason and the reason still travels to the persona’s own console.
What a target delivers is chosen, not typed
Section titled “What a target delivers is chosen, not typed”“What are we delivering” was a free-text box, so a target could name something nobody counts — and its progress would then only ever move because someone typed a new number. It is now two selects: the work item and the model or cohort it applies to.
DELIV holds what each unit can be asked to deliver, and every entry names the record
set its count comes from: production delivers units built (units closed on the line,
serialized at QA), work orders closed (Forge work-order records) or kits
assembled; retrofit delivers conversions, harness upgrades or retrofit orders
closed. DMODEL holds the model or cohort — any model, Battery pack, Controller unit,
Retrofit kit for production; any cohort, Bundle R2, Kit-Rev-B, Supplier-MX for retrofit.
The stored label is composed from the two (delivLabel), never typed, so a card reads
400 units built · Battery pack by 31 Aug. The source travels with the record and is
shown under the picker as it is chosen, on the target detail, and on the persona’s own
card as Counted from units closed on the line · serialized at QA — so the person
delivering it knows which records move the number. Quantity and dates are still typed;
the deliverable cannot be.
Progress is counted, not typed
Section titled “Progress is counted, not typed”done was a seeded number, so nothing about it answered to the window, the model, or any
work actually recorded. DELIVLOG is now an output log of closed work orders — 678
records — each carrying what it delivered, for which model, on what date and how many
units. delivDone sums the records matching a target’s deliverable, its model and its
window up to TODAY_ISO; delivElapsed derives the days elapsed from the same dates. So
narrowing a window drops the figure (G-114 goes 321 to 132 when its start moves to
1 August), switching the model changes it, and Any model sums both.
The segments make the unit ramp across the year, which is why the same deliverable pays out differently in different windows — and it is why a target that was authored to look on pace can come out behind once the number is actually counted.
Approval levels
Section titled “Approval levels”Who signs a new-item request is the org chart above whoever raised it, so it is never
picked. What the PM sets is how far up it has to go, per business unit: the line manager
only, up to the VP, or all the way to the CEO. ORG holds the reporting line per unit and
APPRLV the required depth; apprRungs slices one from the other, so changing a reporting
line changes every request that follows it.
It lives on Inventory’s New items tab, beside the request queue the policy governs — business unit, the depth, and the chain that depth produces, numbered in signing order. There is no Raised-by column: anyone in the unit’s raising role can raise, so a named person would be a fiction. The head badge counts how many units currently escalate to the CEO.
An earlier pass had the requester picking an approver from a list of people. That was wrong twice over: it let a request bypass the reporting line, and it made the variable the person rather than the number of levels, which is the thing that actually differs between a cheap part and one that needs the CEO.
The station rungs, and figures that say what they leave out
Section titled “The station rungs, and figures that say what they leave out”The conversion rungs are the station own, taken whole. Two consoles describing one journey in different words cannot be read against each other, which is the whole point of the PM having them.
Neither hold counts as rework. A keke held because the wrong tricycle arrived and a keke sent back because its own frame is cracked are not the station building badly, and counting either would make a station look worse for catching a problem early. First-pass yield and rework rate both state that they exclude them, on the figure, because a qualified number carrying no visible qualification is read as unqualified.
Batches, and the funnel that reads the lifecycle
Section titled “Batches, and the funnel that reads the lifecycle”A batch is the unit of arrival: a quantity of one item, into one warehouse, on one
day, from one source. The register (trBatchRows) carries Batch with its source
folded beneath it, Item, Warehouse with its custodian, Quantity, Serialised,
Arrived, Created by and Status, filters by location, source and status, and
exports the rows on screen.
Status is read, never typed (batchStatus): Void where it was voided, Serials
pending while fewer serials are recorded than the quantity, Active otherwise. The
serial count is its own field, because itemsFromBatch materialises a sample of a
batch’s units rather than all of them, and deriving the shortfall from that would
have marked every batch pending.
What the record lets you do depends on the batch, not on the reader. Add
serials while any are outstanding, and never more than are outstanding. Edit the
quantity only on a batch you created, and never below the serials already against
it. Void only on a batch you created, with a reason, and only while nothing from
it has been collected into a kit (batchMoved) — a unit that has moved cannot be
unmade. Each of them writes to the batch’s own history.
The serial box is the record (nbSerSync). It parses on every keystroke, so
the count beside it cannot claim serials the form has not taken, a repeated line
is one unit rather than two and says so, and a mistyped serial is fixed by editing
the line. The CSV appends into the same box. There is no scan here: label scanning
is Path A on the Inventory app’s tablet, with a camera frame and a Code-128 read,
and a browser on a desk has no reader to call.
The funnel is one card
Section titled “The funnel is one card”Seven stages (CONV_STAGES) over a period: Inspection pending, Inspection
completed, Conversion queued, Conversion in progress, Final checks, Ready for
pickup, Collected. The stage is the row, so its name is written once, and the
period is the columns. The other way round, those seven names were the column
headers, and .tbl th is uppercase, tracked and nowrap, so they set the table
wider than its card and squeezed the period column until its dates broke over four
lines.
The grain follows the period (convGrain), which is what bounds the column
count: a column per day inside a week, per week inside a month, per month inside
anything longer, and none at all for a single day. A toggle let a month ask for
thirty columns, and the one that was there drove the Throughput chart rather than
this table. Buckets are clamped to the period, so the columns sum to the figure
beside them rather than counting days before it began.
Today is the last day the data covers (convToday), not the wall clock. The
lifecycle runs to June, so a window anchored on the clock put every stage outside
it and drew seven zeros.
Four of the seven stages are derived. The lifecycle holds pending, inspection
started, inspected, queued and done; Conversion in progress, Final checks, Ready
for pickup and Collected are read off those two ends, while done and stage are
untouched because the rest of the console reads them. Ready for pickup is done,
so the funnel and the Units closed today tile (convCompletedToday) cannot
disagree.
Every count opens the riders behind it (convOpenRiders): the plate set becomes
the register’s filter, and the chip names the stage and the window it came from.