Two separate floors were collapsing a live cartridge into a spent one, and
removing either alone changes nothing.
EMPTY_LEVEL was 5, so everything at or below 5% was assigned daysleft 0
outright. The reasoning was that a printer at 1% is out of toner as far as
anyone standing at it is concerned. But the row exists to say how long is
left, and 2% draining a point a day has two days in it. Flooring put that
cartridge beside ones that genuinely are empty with no way back to the
difference. It is 0 now: empty means empty.
int(level / rate) then truncated the division, so anything under a full day
arrived as 0 whatever the floor did - a cartridge with six hours in it was
indistinguishable from one with nothing, and the band read it as empty.
daysleft is fractional now, rounded to two places, which is about a quarter of
an hour: finer than the estimate deserves, but it costs nothing and keeps the
ordering of two nearly-spent cartridges meaningful.
daysText reads in whatever unit carries meaning: "6 hours", "1 hour", "29 min",
"4 days". Below an hour it goes to minutes with a floor of one, because
rounding hours would land back on "empty" - the same bug one rung down.
BEHAVIOUR CHANGE worth knowing: a cartridge at 1-5% with NO history used to get
daysleft 0 from the floor and land on the order list. A rate needs two
readings; with none there is nothing to divide, and the old answer was right by
accident - it said "empty" about a level nobody had watched move. It now
reports 'no history' and shows under "No estimate yet". This reaches only
printers newly added to Zabbix; anything that got to 3% the ordinary way has
the history to forecast from.
Five existing tests pinned the old rule. They recorded a real decision, so they
are rewritten to the new one rather than deleted. One of them was passing for
the wrong reason: its series (2.0, 1.7, 1.4, 1.1) drops 0.9, under
MIN_DROP_FOR_ESTIMATE, so it never had a rate at all and only passed because
the floor short-circuited ahead of the rate check. It now uses a real 20-day
drop at a tenth of a point a day and asserts the ten days its docstring always
described.
The forecast fixture's black cartridge moves from 1% to 0% so the empty band
keeps its API-level coverage, and a magenta at 3% covers hours-left end to end.
The forecast identified a cartridge by its part number alone. 'W2020X' only
tells you which toner it is once you already know, and both places that name a
supply - the Order now list and the Part column - showed nothing else, so the
report read as a list of codes.
Both now show marketingname, which is what the box says: '414X Black'. The
part number moves to the title attribute, one hover away, because it is still
the thing an order is placed against. A supply with no name on file falls back
to the number rather than showing an empty cell; marketingname is nullable.
'Copy list' is deliberately untouched. It writes the part number verbatim,
which is what purchasing pastes into an order, and a spec pins that the name
does NOT reach that text.
firstPart() now returns the whole part row rather than its number, since the
cell needs both halves. It has one caller.
Dropped the .partnumber style with its .unmapped variant: nothing renders that
class any more, and the variant was already dead - the unmapped block lists no
part number by definition.
Verified by mounting the component against a fixture. The dev box has no
Zabbix, so the page itself has no rows to look at there.