The analytics headers on the admin tables
Every KPI tile and chart on the six store tables that carry an Analytics tab — what each figure counts, which ones are snapshots rather than windows, and where a multi-currency catalogue makes a total meaningless.
Six of the store's seven admin tables carry a second tab beside the list. It draws KPI tiles and charts computed straight from that table — money, backlog, liability, SLA — and none of it appears anywhere else in the admin panel. The store dashboard is a different, much smaller surface, but it is not narrower in what it reads: it joins products, order items, categories, orders and customers, and it is where the units-sold, top-product and top-customer figures these analytics tabs cannot produce actually live.
| Table | Analytics tab |
|---|---|
Orders — /admin/ecommerce/order |
Yes |
Products — /admin/ecommerce/product |
Yes |
Discounts — /admin/ecommerce/discount |
Yes |
Shipping — /admin/ecommerce/shipping |
Yes |
Reviews — /admin/ecommerce/review |
Yes |
Wishlist — /admin/ecommerce/wishlist |
Yes |
Categories — /admin/ecommerce/category |
No — the category table has no analytics config |
Opening it
The tab pair sits at the top right of the table header: Overview (the list) and Analytics. Switching to Analytics reveals a timeframe selector — 24 Hours, 7 Days, 30 Days, 3 Months, 6 Months, 1 Year — which opens on 1 Year.
The tiles are computed by POST /api/admin/analysis, which is gated on
access.admin — not on the table's own view.ecommerce.* key. A role that
can open and read the orders table but has no access.admin sees the tab and
gets an error where the tiles should be. See
Permissions and roles.
Results are cached in the browser for 5 minutes per timeframe. Changing a figure in the database and reloading the page will not move a tile inside that window; switch timeframe and back, or wait it out.
Two kinds of figure, and why they will not reconcile
This is the single thing to understand before reading any tile, because two tiles on the same screen can legitimately disagree.
- Window figures are measured over the timeframe you selected, bucketed by the row's creation date. "Orders Placed" on the 7-day view means orders created in the last 7 days.
- Snapshot figures ignore the timeframe entirely and measure every matching
row that exists right now. "Open Backlog Value" is every
PENDINGorder ever, because an order stuck since March is precisely the one a 7-day window hides.
Snapshot tiles are called out in the tables below. Charts are window figures by default, but a ranked bar chart can declare itself all-time, and three on these screens do: Average Price by Type and Inventory Value by Currency on Products, and Most-Issued Depths (% Off) on Discounts. Those three describe the estate as it stands and do not move with the timeframe selector. Every other chart, including all the time-series and the pies, is a window figure.
The delta chip (the small percentage under a figure) appears on window tiles
only, and it compares the second half of the selected window against the first
half — not against the previous window of the same length. On the 1-Year view
that is roughly the last six months against the six before them. When the
earlier half is zero there is no baseline and the chip is omitted rather than
printed as +100%.
Snapshot tiles carry no delta chip at all. There is nothing to compare a "right now" figure to.
Tiles where a rise is bad — backlog, overdue, lost revenue, negative reviews — have their chip colours flipped, so red always means "worse".
Money tiles and your currency mix
A product carries its own price and its own currency, and checkout
copies both onto the order row. A catalogue priced in more than one currency
therefore produces order rows in more than one currency, and a naive
SUM(total) across them is nonsense — three 0.01 BTC sales beside one $40 sale
add to "40.03".
The tiles handle this in one of two ways, and which one you are looking at is visible on the card:
- Converted totals. Sums grouped by the currency column, priced into USD one denomination at a time, and rendered with a currency symbol. Gross Revenue, Open Backlog Value, Lost Revenue, Discount Given, Inventory Value on Hand and Fixed-Value Liability are all of these.
- Undenominated figures. Averages cannot be converted — the mean of 0.01 BTC and $40 is not a price in any currency — so those tiles drop the symbol and show the raw mean of the column. Average Order Value, Average price, and Average Value (Fixed Codes) are all bare numbers.
If the platform has no exchange rate for a currency on the card, that currency
is excluded rather than counted as zero, and the card prints
Excludes <CODE> — no exchange rate under the figure. When you see that line,
the number is a lower bound, not the total.
The ranked bar charts (Settled Revenue by Currency, Inventory Value by Currency) are the opposite view: each bar is in its own units and does not convert, so the bars are not expected to add up to the converted headline beside them.
Orders
/admin/ecommerce/order → Analytics. This is the money screen.
| Tile | What it measures | Scope |
|---|---|---|
| Gross Revenue | SUM(total) over COMPLETED orders, converted to USD |
Window |
| Average Order Value | Mean total over COMPLETED orders — no currency, see above |
Window |
| completion rate | COMPLETED orders ÷ Orders Placed, both measured over the same window |
Window |
| Orders Placed | Count of orders created | Window |
| Open Backlog Value | SUM(total) over everything still PENDING, converted to USD |
Snapshot |
| Average Backlog Age | Mean hours since creation over everything still PENDING |
Snapshot |
| Stalled Over 48 Hours | PENDING orders created more than 48 hours ago |
Snapshot |
| Lost Revenue (Cancelled + Rejected) | SUM(total) over orders whose status is CANCELLED or REJECTED, converted to USD |
Window |
| Time to Completion (All Time) | Mean hours from creation to last write, over COMPLETED orders |
Snapshot |
| Discount Given | SUM(discount) over COMPLETED orders, converted to USD |
Window |
Charts: Orders Placed by Status (Selected Period) (pie over all four
statuses), Revenue vs Lost Revenue (stacked bar), Settled Revenue by
Currency and Settled Revenue by Settlement Rail (ranked bars over
currency and walletType).
Three things about this screen specifically
Lost Revenue is an explicit list, not a negation. It counts status
CANCELLED or REJECTED by name. It does not mean "everything that is not
completed", so PENDING orders are not in it, and if a fifth status is ever
added it will not silently start counting that either.
Time to Completion is an upper bound. The order table has no completedAt
column. The tile measures creation → updatedAt, and updatedAt is the last
write of any kind. On an order that was marked COMPLETED and never touched
again, that is exactly the completion time. On one that was edited afterwards —
a shipment assigned, an address corrected — the figure is longer than the truth.
Read it as a ceiling.
The two duration tiles are all-time on purpose. Average Backlog Age and Time to Completion both ignore the timeframe selector, which is why their titles say so. Backlog age is a "right now" question anyway; and an average duration re-folded over a window would divide the wrong pair of numbers and print a meaningless figure.
completion rate needs Orders Placed. The rate divides completed orders by orders placed in the same window. That is why Orders Placed sits beside it as its own tile rather than being taken from the page's generic row count — the generic count is replaced by an all-time snapshot on any page carrying a snapshot tile, and this one carries four (Open Backlog Value, Average Backlog Age, Stalled Over 48 Hours and Time to Completion).
Products
/admin/ecommerce/product → Analytics. Stock and price are state, not flow,
so every tile on this screen is a snapshot — the timeframe selector moves
only the charts.
| Tile | What it measures |
|---|---|
| Catalogue Size | Every product row that is not soft-deleted |
| Purchasable | Products with status on |
| Out of Stock | Live products at inventoryQuantity = 0 — listed and unbuyable |
| Low Stock (1–5 Left) | Live products with 1 to 5 units, strictly. A product at zero is counted by the tile to its left, never by both |
| Inventory Value on Hand | SUM(price × inventoryQuantity) over purchasable products, converted to USD |
| Average price | Mean price over purchasable products — no currency |
| Priced at Zero | Live, purchasable products with price = 0. The product form accepts zero, so this is a real thing to check |
Charts: Listings Created by Type (Selected Period), Average Price by Type, Inventory Value by Currency, New Listings by Type Over Time.
Only the first and last of those move with the timeframe; the two ranked bars describe the shelf as it stands.
There is no sales or velocity figure on this screen. Units sold live on the order-item table, which this page does not read. The store dashboard does read it, and that is where units sold and the top-products ranking are.
Discounts
/admin/ecommerce/discount → Analytics. A coupon estate is state, so every
tile here is a snapshot.
| Tile | What it measures |
|---|---|
| Active | Codes with status on |
| Redeemable Right Now | status on and validUntil still in the future |
| Expired but Still Marked Live | status on and validUntil already passed |
| Live Codes With No Usage Cap | status on and maxUses empty — unbounded exposure |
| Fixed-Value Liability | SUM(amount) over redeemable FIXED codes, converted to USD through the attached product's currency |
| Average Depth (Percentage Codes) | Mean percentage over redeemable PERCENTAGE codes |
| Average Value (Fixed Codes) | Mean amount over redeemable FIXED codes — no currency |
| Redeemable 100% Codes | Redeemable PERCENTAGE codes at exactly 100 — codes that make an order free |
Charts: Codes Issued by Type (Selected Period), Codes Issued by Type Over Time, Most-Issued Depths (% Off).
Every code on that tile looks usable in the discount table — its status badge
says active — and fails at checkout, because checkout also requires
validUntil >= now. The gap between Active and Redeemable Right Now is
dead configuration for you to clear, and it is the reason a customer can report
that "your code does not work" while the admin screen says it is on.
Two limits worth knowing:
- Redeemable does not check the start date. A code with a
validFromin the future is counted as redeemable here. Only expiry is read. - Nothing on this screen counts redemptions. Usage against
maxUseslives in a separate table the analytics engine cannot join to from here. Fixed-Value Liability is the value of codes outstanding, not of codes taken.
Shipping
/admin/ecommerce/shipping → Analytics.
| Tile | What it measures | Scope |
|---|---|---|
| In transit | Loads at loadStatus = TRANSIT |
Snapshot |
| Awaiting Dispatch | Loads at PENDING |
Snapshot |
| Overdue Deliveries | PENDING or TRANSIT with a deliveryDate already passed — the call list |
Snapshot |
| On-Time Delivery Rate | Delivered On Time ÷ Deliveries With a Promised Date | Window |
| Shipping Spend | SUM(cost) over loads not CANCELLED |
Window |
| Shipping Tax | SUM(tax) over loads not CANCELLED |
Window |
| Tonnage Delivered | SUM(weight) over DELIVERED loads |
Window |
| Volume Delivered | SUM(volume) over DELIVERED loads |
Window |
| Delivered On Time | DELIVERED loads whose last write was on or before the promised date |
Window |
| Deliveries With a Promised Date | DELIVERED loads that carried a deliveryDate at all |
Window |
| Average Transit Time (All Time) | Mean hours from booking to last write, over DELIVERED loads |
Snapshot |
Charts: Shipping Spend by Carrier, Late Deliveries by Carrier (both
ranked on the free-text transporter field, top five plus "Other"), and
Shipping Spend Over Time.
The shipment table has no currency column. cost and tax are whatever an
operator typed off a carrier invoice, in whatever currency that invoice was in.
Rendering them with a dollar sign would be a claim the data cannot support, so
they are shown as plain numbers. If you run one carrier in one currency, the
figures are that currency; if you do not, they are a sum of unlike things and
you should read the per-carrier bars instead.
The on-time rate is measured only against loads that carried a promised
date — a shipment with no deliveryDate is in neither the numerator nor the
denominator. Both of those tiles are on the page so you can see how much of your
traffic the rate actually covers. And, as with orders, there is no deliveredAt
column: updatedAt stands in for the delivery write, so a shipment edited after
it landed reads as late.
Cross-check Shipping Spend here against what the order desk billed customers
for shipping. Shipping is sold at one flat store-wide fee
(ecommerceDefaultShippingCost, on Store settings), so if
carrier spend exceeds what you collected, every physical order is losing money.
Reviews
/admin/ecommerce/review → Analytics.
| Tile | What it measures | Scope |
|---|---|---|
| Average Rating | Mean rating over published reviews only |
Snapshot |
| Negative Rate (1–2★) | Negative ÷ Published, over the window | Window |
| Moderation Queue (Hidden) | Reviews with status off — the queue |
Snapshot |
| Products With a Review | Distinct products carrying at least one published review | Snapshot |
| Published This Period | Published reviews created in the window | Window |
| Negative (1–2★) | Published reviews rated 1 or 2 | Window |
| Neutral (3★) | Published reviews rated exactly 3 | Window |
| Positive (4–5★) | Published reviews rated 4 or 5 | Window |
Chart: Reviews Over Time, a stacked bar of the three bands. The bands stack to the period's published total, so the shape of the curve is readable without a second chart.
Average Rating counts published reviews only, deliberately: hiding a review in moderation must not be able to move the storefront's score, and a moderator should not be able to lift a product's rating by unpublishing its critics.
Negative Rate is a rate rather than a count because a spike in 1★ reviews during a spike in orders may be nothing at all.
Not available here: worst-rated products, and review coverage against catalogue size. Both need a join to the product table that this screen cannot make.
Wishlist
/admin/ecommerce/wishlist → Analytics. The smallest of the six, because
the table it sits on is small — a wishlist row is a container with an id, an
owner and a creation date, and everything commercially interesting (which
product, how many people want it) lives in a child table this screen does not
read.
| Tile | What it measures | Scope |
|---|---|---|
| Total Wishlists | Every wishlist container that exists | Snapshot |
| Customers With a Wishlist | Distinct owners | Snapshot |
| Average Wishlist Age | Mean hours since creation across the whole estate | Snapshot |
| Opened This Period | Containers created in the window | Window |
| Opened Over a Year Ago | Containers older than one year | Snapshot |
Chart: Wishlists Over Time.
Read Opened Over a Year Ago against Total Wishlists. If most of the estate is on that tile and Opened This Period is flat, the wishlist feature is an archive rather than a signal, and nothing else on the platform will tell you that.
There is no most-wishlisted-product ranking and no wishlist-to-purchase conversion figure. Ranking this table by customer would give every bar a value of 1, because it holds one container per customer.
Reading durations
Any tile formatted as a duration is computed in hours. Below one hour it prints minutes, up to 48 hours it prints hours, and past that it prints days. A "2.5d" backlog age is 60 hours.
When a tile shows an em dash
— means the figure is unknown, not zero. Either the request failed, or that
card's configuration is broken and the engine returned an error for it in place
of a number. A genuine zero renders as 0. That distinction is deliberate — a
confident 0 on a broken card is the harder failure to notice.
If the whole tab fails to load, check access.admin on the role first; that is
the common cause.
Next: Orders and fulfilment for what the order states actually mean, or Where store money lands in the platform ledger when a revenue figure needs reconciling against the transaction table.